2026-06-29 10:51:04 +05:30
|
|
|
#include "parser/pdfium_internal.hpp"
|
|
|
|
|
|
|
|
|
|
namespace pdfengine::parser {
|
|
|
|
|
|
|
|
|
|
std::expected<PageModel, EngineError> PdfiumPage::extractDocumentModel() const {
|
|
|
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
|
|
|
if (!page_) {
|
|
|
|
|
return std::unexpected(EngineError::Unknown);
|
|
|
|
|
}
|
|
|
|
|
ensureTextPageLoaded();
|
|
|
|
|
if (!textPage_) {
|
|
|
|
|
return std::unexpected(EngineError::Unknown);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PageModel model;
|
|
|
|
|
model.width = width();
|
|
|
|
|
model.height = height();
|
|
|
|
|
model.pageIndex = pageIndex_;
|
|
|
|
|
|
|
|
|
|
int charCount = FPDFText_CountChars(textPage_);
|
|
|
|
|
if (charCount <= 0) {
|
|
|
|
|
return model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unordered_map<std::string, FontInfo> fontMap;
|
|
|
|
|
if (auto fontsRes = getFonts()) {
|
|
|
|
|
for (const auto& f : *fontsRes) {
|
|
|
|
|
fontMap[f.fontName] = f;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<Glyph> documentGlyphs;
|
|
|
|
|
documentGlyphs.reserve(charCount);
|
|
|
|
|
|
|
|
|
|
std::unordered_map<FPDF_PAGEOBJECT, int> objToIndex;
|
|
|
|
|
int objCount = FPDFPage_CountObjects(page_);
|
|
|
|
|
for (int i = 0; i < objCount; ++i) {
|
|
|
|
|
objToIndex[FPDFPage_GetObject(page_, i)] = i;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < charCount; ++i) {
|
|
|
|
|
unsigned int codeUnit = FPDFText_GetUnicode(textPage_, i);
|
|
|
|
|
unsigned int cp = codeUnit;
|
|
|
|
|
|
|
|
|
|
if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF && i + 1 < charCount) {
|
|
|
|
|
unsigned int nextUnit = FPDFText_GetUnicode(textPage_, i + 1);
|
|
|
|
|
if (nextUnit >= 0xDC00 && nextUnit <= 0xDFFF) {
|
|
|
|
|
cp = 0x10000 + ((codeUnit - 0xD800) << 10) + (nextUnit - 0xDC00);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string utf8_char = code_point_to_utf8(cp);
|
|
|
|
|
if (utf8_char.empty() || cp == '\r' || cp == '\n') {
|
|
|
|
|
if (cp > 0xFFFF) ++i;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Glyph g;
|
|
|
|
|
g.text = std::move(utf8_char);
|
|
|
|
|
g.unicode = cp;
|
|
|
|
|
g.srcIndex = i;
|
|
|
|
|
|
|
|
|
|
double left, right, bottom, top;
|
|
|
|
|
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
|
|
|
|
|
g.bboxX = (std::min)(left, right);
|
|
|
|
|
g.bboxY = (std::min)(bottom, top);
|
|
|
|
|
g.bboxW = std::abs(right - left);
|
|
|
|
|
g.bboxH = std::abs(top - bottom);
|
|
|
|
|
|
|
|
|
|
FPDFText_GetCharOrigin(textPage_, i, &g.originX, &g.originY);
|
|
|
|
|
g.angle = FPDFText_GetCharAngle(textPage_, i);
|
|
|
|
|
|
|
|
|
|
g.fontSize = FPDFText_GetFontSize(textPage_, i);
|
|
|
|
|
int flags = 0;
|
|
|
|
|
unsigned long len = FPDFText_GetFontInfo(textPage_, i, nullptr, 0, &flags);
|
|
|
|
|
if (len > 0) {
|
|
|
|
|
std::vector<char> buf(len);
|
|
|
|
|
if (FPDFText_GetFontInfo(textPage_, i, buf.data(), len, &flags) > 0) {
|
|
|
|
|
g.fontName = std::string(buf.data());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.flags = flags;
|
|
|
|
|
|
|
|
|
|
FPDF_PAGEOBJECT textObj = FPDFText_GetTextObject(textPage_, i);
|
|
|
|
|
if (textObj) {
|
|
|
|
|
auto it = objToIndex.find(textObj);
|
|
|
|
|
if (it != objToIndex.end()) {
|
|
|
|
|
g.pageObjectIndex = it->second;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
documentGlyphs.push_back(g);
|
|
|
|
|
|
|
|
|
|
if (cp > 0xFFFF) ++i;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::sort(documentGlyphs.begin(), documentGlyphs.end(), [](const Glyph& a, const Glyph& b) {
|
|
|
|
|
if (std::abs(a.originY - b.originY) > 1.0) {
|
|
|
|
|
return a.originY > b.originY;
|
|
|
|
|
}
|
|
|
|
|
return a.originX < b.originX;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
std::vector<TextLine> lines;
|
|
|
|
|
if (!documentGlyphs.empty()) {
|
|
|
|
|
TextLine currentLine;
|
|
|
|
|
currentLine.angle = documentGlyphs[0].angle;
|
|
|
|
|
double currentOriginY = documentGlyphs[0].originY;
|
|
|
|
|
|
|
|
|
|
for (const auto& g : documentGlyphs) {
|
|
|
|
|
if (currentLine.glyphs.empty()) {
|
|
|
|
|
currentLine.glyphs.push_back(g);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::abs(g.angle - currentLine.angle) < 0.1 &&
|
|
|
|
|
std::abs(g.originY - currentOriginY) < 1.0) {
|
|
|
|
|
currentLine.glyphs.push_back(g);
|
|
|
|
|
} else {
|
|
|
|
|
lines.push_back(std::move(currentLine));
|
|
|
|
|
currentLine = TextLine();
|
|
|
|
|
currentLine.angle = g.angle;
|
|
|
|
|
currentOriginY = g.originY;
|
|
|
|
|
currentLine.glyphs.push_back(g);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!currentLine.glyphs.empty()) {
|
|
|
|
|
lines.push_back(std::move(currentLine));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (auto& line : lines) {
|
|
|
|
|
std::sort(line.glyphs.begin(), line.glyphs.end(), [](const Glyph& a, const Glyph& b) {
|
|
|
|
|
if (a.originX != b.originX) return a.originX < b.originX;
|
|
|
|
|
return a.srcIndex < b.srcIndex;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
std::vector<double> gaps;
|
|
|
|
|
for (size_t i = 1; i < line.glyphs.size(); ++i) {
|
|
|
|
|
double gap = line.glyphs[i].bboxX - (line.glyphs[i-1].bboxX + line.glyphs[i-1].bboxW);
|
|
|
|
|
if (gap > 0) {
|
|
|
|
|
gaps.push_back(gap);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
double p25Gap = 0.0;
|
|
|
|
|
if (!gaps.empty()) {
|
|
|
|
|
std::sort(gaps.begin(), gaps.end());
|
|
|
|
|
p25Gap = gaps[gaps.size() / 4];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double lineEm = 0.0;
|
|
|
|
|
{
|
|
|
|
|
std::vector<double> hs;
|
|
|
|
|
for (const auto& gg : line.glyphs) if (gg.bboxH > 0.1) hs.push_back(gg.bboxH);
|
|
|
|
|
if (!hs.empty()) {
|
|
|
|
|
std::sort(hs.begin(), hs.end());
|
|
|
|
|
double capH = hs[(hs.size() * 9) / 10];
|
|
|
|
|
lineEm = capH / 0.7;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TextRun currentRun;
|
|
|
|
|
if (!line.glyphs.empty()) {
|
|
|
|
|
const Glyph* firstG = &line.glyphs[0];
|
|
|
|
|
currentRun.fontName = firstG->fontName;
|
|
|
|
|
currentRun.fontSize = firstG->fontSize;
|
|
|
|
|
currentRun.flags = firstG->flags;
|
|
|
|
|
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
|
|
|
|
|
currentRun.internalFontId = it->second.internalFontId;
|
|
|
|
|
currentRun.isEmbedded = it->second.isEmbedded;
|
|
|
|
|
currentRun.type = it->second.type;
|
|
|
|
|
currentRun.fontFidelity = classifyFontFidelity(it->second);
|
|
|
|
|
}
|
|
|
|
|
currentRun.glyphs.push_back(*firstG);
|
|
|
|
|
currentRun.text += firstG->text;
|
|
|
|
|
|
|
|
|
|
for (size_t i = 1; i < line.glyphs.size(); ++i) {
|
|
|
|
|
const auto& prevG = line.glyphs[i-1];
|
|
|
|
|
const auto& currG = line.glyphs[i];
|
|
|
|
|
|
|
|
|
|
double gap = currG.bboxX - (prevG.bboxX + prevG.bboxW);
|
|
|
|
|
double em = (std::max)(static_cast<double>(currG.fontSize), lineEm);
|
|
|
|
|
double spaceThreshold = em * 0.2;
|
|
|
|
|
if (p25Gap > spaceThreshold) {
|
|
|
|
|
spaceThreshold = (std::min)(p25Gap * 1.5, em * 0.38);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool addSpace = gap > spaceThreshold && prevG.text != " " && currG.text != " ";
|
|
|
|
|
bool breakRun = currG.fontName != currentRun.fontName ||
|
|
|
|
|
std::abs(currG.fontSize - currentRun.fontSize) > 0.1 ||
|
|
|
|
|
currG.flags != currentRun.flags;
|
|
|
|
|
|
|
|
|
|
if (addSpace) {
|
|
|
|
|
Glyph spaceGlyph;
|
|
|
|
|
spaceGlyph.text = " ";
|
|
|
|
|
spaceGlyph.unicode = ' ';
|
|
|
|
|
spaceGlyph.fontSize = currG.fontSize;
|
|
|
|
|
spaceGlyph.fontName = currG.fontName;
|
|
|
|
|
spaceGlyph.flags = currG.flags;
|
|
|
|
|
spaceGlyph.originX = prevG.bboxX + prevG.bboxW;
|
|
|
|
|
spaceGlyph.originY = currG.originY;
|
|
|
|
|
spaceGlyph.angle = currG.angle;
|
|
|
|
|
spaceGlyph.bboxX = spaceGlyph.originX;
|
|
|
|
|
spaceGlyph.bboxY = currG.bboxY;
|
|
|
|
|
spaceGlyph.bboxW = gap;
|
|
|
|
|
spaceGlyph.bboxH = currG.bboxH;
|
|
|
|
|
|
|
|
|
|
if (breakRun) {
|
|
|
|
|
for (const auto& g : currentRun.glyphs) {
|
|
|
|
|
if (g.pageObjectIndex != -1 && std::find(currentRun.objectIndices.begin(), currentRun.objectIndices.end(), g.pageObjectIndex) == currentRun.objectIndices.end()) {
|
|
|
|
|
currentRun.objectIndices.push_back(g.pageObjectIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
line.runs.push_back(std::move(currentRun));
|
|
|
|
|
currentRun = TextRun();
|
|
|
|
|
currentRun.fontName = currG.fontName;
|
|
|
|
|
currentRun.fontSize = currG.fontSize;
|
|
|
|
|
currentRun.flags = currG.flags;
|
|
|
|
|
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
|
|
|
|
|
currentRun.internalFontId = it->second.internalFontId;
|
|
|
|
|
currentRun.isEmbedded = it->second.isEmbedded;
|
|
|
|
|
currentRun.type = it->second.type;
|
|
|
|
|
currentRun.fontFidelity = classifyFontFidelity(it->second);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
currentRun.glyphs.push_back(spaceGlyph);
|
|
|
|
|
currentRun.text += spaceGlyph.text;
|
|
|
|
|
} else if (breakRun) {
|
|
|
|
|
for (const auto& g : currentRun.glyphs) {
|
|
|
|
|
if (g.pageObjectIndex != -1 && std::find(currentRun.objectIndices.begin(), currentRun.objectIndices.end(), g.pageObjectIndex) == currentRun.objectIndices.end()) {
|
|
|
|
|
currentRun.objectIndices.push_back(g.pageObjectIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
line.runs.push_back(std::move(currentRun));
|
|
|
|
|
currentRun = TextRun();
|
|
|
|
|
currentRun.fontName = currG.fontName;
|
|
|
|
|
currentRun.fontSize = currG.fontSize;
|
|
|
|
|
currentRun.flags = currG.flags;
|
|
|
|
|
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
|
|
|
|
|
currentRun.internalFontId = it->second.internalFontId;
|
|
|
|
|
currentRun.isEmbedded = it->second.isEmbedded;
|
|
|
|
|
currentRun.type = it->second.type;
|
|
|
|
|
currentRun.fontFidelity = classifyFontFidelity(it->second);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentRun.glyphs.push_back(currG);
|
|
|
|
|
currentRun.text += currG.text;
|
|
|
|
|
}
|
|
|
|
|
if (!currentRun.glyphs.empty()) {
|
|
|
|
|
for (const auto& g : currentRun.glyphs) {
|
|
|
|
|
if (g.pageObjectIndex != -1 && std::find(currentRun.objectIndices.begin(), currentRun.objectIndices.end(), g.pageObjectIndex) == currentRun.objectIndices.end()) {
|
|
|
|
|
currentRun.objectIndices.push_back(g.pageObjectIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
line.runs.push_back(std::move(currentRun));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (auto& line : lines) {
|
|
|
|
|
for (auto& run : line.runs) {
|
|
|
|
|
std::string repaired = pdfengine::parser::repairMojibake(run.text);
|
|
|
|
|
if (repaired != run.text) run.text = std::move(repaired);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<Paragraph> paragraphs;
|
|
|
|
|
auto lineStyleKey = [](const TextLine& ln) -> std::pair<bool,bool> {
|
|
|
|
|
for (const auto& r : ln.runs) {
|
|
|
|
|
if (r.text.empty()) continue;
|
|
|
|
|
std::string n = r.fontName + "|" + r.internalFontId;
|
|
|
|
|
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
bool bold = n.find("bold") != std::string::npos;
|
|
|
|
|
bool sans = n.find("arial") != std::string::npos || n.find("helvetica") != std::string::npos;
|
|
|
|
|
return {bold, sans};
|
|
|
|
|
}
|
|
|
|
|
return {false, false};
|
|
|
|
|
};
|
|
|
|
|
if (!lines.empty()) {
|
|
|
|
|
Paragraph currentPara;
|
|
|
|
|
currentPara.lines.push_back(std::move(lines[0]));
|
|
|
|
|
|
|
|
|
|
for (size_t i = 1; i < lines.size(); ++i) {
|
|
|
|
|
auto& prevLine = currentPara.lines.back();
|
|
|
|
|
auto& currLine = lines[i];
|
|
|
|
|
|
|
|
|
|
double prevY = prevLine.glyphs.empty() ? 0 : prevLine.glyphs[0].originY;
|
|
|
|
|
double currY = currLine.glyphs.empty() ? 0 : currLine.glyphs[0].originY;
|
|
|
|
|
double fontSize = currLine.runs.empty() ? 12.0 : currLine.runs[0].fontSize;
|
|
|
|
|
double capH = 0.0;
|
|
|
|
|
for (const auto& g : currLine.glyphs) if (g.bboxH > capH) capH = g.bboxH;
|
|
|
|
|
if (capH > 0.0) fontSize = (std::max)(fontSize, capH / 0.7);
|
|
|
|
|
double vGap = std::abs(prevY - currY);
|
|
|
|
|
|
|
|
|
|
bool styleChanged = lineStyleKey(currLine) != lineStyleKey(prevLine);
|
|
|
|
|
if (vGap > fontSize * 1.5 || styleChanged) {
|
|
|
|
|
paragraphs.push_back(std::move(currentPara));
|
|
|
|
|
currentPara = Paragraph();
|
|
|
|
|
}
|
|
|
|
|
currentPara.lines.push_back(std::move(currLine));
|
|
|
|
|
}
|
|
|
|
|
if (!currentPara.lines.empty()) {
|
|
|
|
|
paragraphs.push_back(std::move(currentPara));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto computeRunBBox = [](TextRun& r) {
|
|
|
|
|
if (r.glyphs.empty()) return;
|
|
|
|
|
double minX = r.glyphs[0].bboxX;
|
|
|
|
|
double minY = r.glyphs[0].bboxY;
|
|
|
|
|
double maxX = r.glyphs[0].bboxX + r.glyphs[0].bboxW;
|
|
|
|
|
double maxY = r.glyphs[0].bboxY + r.glyphs[0].bboxH;
|
|
|
|
|
for (size_t i = 1; i < r.glyphs.size(); ++i) {
|
|
|
|
|
minX = (std::min)(minX, r.glyphs[i].bboxX);
|
|
|
|
|
minY = (std::min)(minY, r.glyphs[i].bboxY);
|
|
|
|
|
maxX = (std::max)(maxX, r.glyphs[i].bboxX + r.glyphs[i].bboxW);
|
|
|
|
|
maxY = (std::max)(maxY, r.glyphs[i].bboxY + r.glyphs[i].bboxH);
|
|
|
|
|
}
|
|
|
|
|
r.x = minX;
|
|
|
|
|
r.y = minY;
|
|
|
|
|
r.w = maxX - minX;
|
|
|
|
|
r.h = maxY - minY;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto computeLineBBox = [](TextLine& l) {
|
|
|
|
|
if (l.runs.empty()) return;
|
|
|
|
|
double minX = l.runs[0].x;
|
|
|
|
|
double minY = l.runs[0].y;
|
|
|
|
|
double maxX = l.runs[0].x + l.runs[0].w;
|
|
|
|
|
double maxY = l.runs[0].y + l.runs[0].h;
|
|
|
|
|
for (size_t i = 1; i < l.runs.size(); ++i) {
|
|
|
|
|
minX = (std::min)(minX, l.runs[i].x);
|
|
|
|
|
minY = (std::min)(minY, l.runs[i].y);
|
|
|
|
|
maxX = (std::max)(maxX, l.runs[i].x + l.runs[i].w);
|
|
|
|
|
maxY = (std::max)(maxY, l.runs[i].y + l.runs[i].h);
|
|
|
|
|
}
|
|
|
|
|
l.x = minX;
|
|
|
|
|
l.y = minY;
|
|
|
|
|
l.w = maxX - minX;
|
|
|
|
|
l.h = maxY - minY;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto computeParaBBox = [](Paragraph& p) {
|
|
|
|
|
if (p.lines.empty()) return;
|
|
|
|
|
double minX = p.lines[0].x;
|
|
|
|
|
double minY = p.lines[0].y;
|
|
|
|
|
double maxX = p.lines[0].x + p.lines[0].w;
|
|
|
|
|
double maxY = p.lines[0].y + p.lines[0].h;
|
|
|
|
|
for (size_t i = 1; i < p.lines.size(); ++i) {
|
|
|
|
|
minX = (std::min)(minX, p.lines[i].x);
|
|
|
|
|
minY = (std::min)(minY, p.lines[i].y);
|
|
|
|
|
maxX = (std::max)(maxX, p.lines[i].x + p.lines[i].w);
|
|
|
|
|
maxY = (std::max)(maxY, p.lines[i].y + p.lines[i].h);
|
|
|
|
|
}
|
|
|
|
|
p.x = minX;
|
|
|
|
|
p.y = minY;
|
|
|
|
|
p.w = maxX - minX;
|
|
|
|
|
p.h = maxY - minY;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto hex2 = [](unsigned int c) -> std::string {
|
|
|
|
|
static const char* h = "0123456789abcdef";
|
|
|
|
|
c &= 0xFF;
|
|
|
|
|
return std::string{h[(c >> 4) & 0xF], h[c & 0xF]};
|
|
|
|
|
};
|
|
|
|
|
auto computeRunColor = [&](TextRun& r) {
|
|
|
|
|
if (r.objectIndices.empty()) return;
|
|
|
|
|
std::vector<int> idxs = r.objectIndices;
|
|
|
|
|
std::sort(idxs.begin(), idxs.end());
|
|
|
|
|
for (int idx : idxs) {
|
|
|
|
|
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, idx);
|
|
|
|
|
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
|
|
|
|
|
unsigned int cr = 0, cg = 0, cb = 0, ca = 0;
|
|
|
|
|
bool got = (FPDFPageObj_GetFillColor(obj, &cr, &cg, &cb, &ca) && ca != 0) ||
|
|
|
|
|
(FPDFPageObj_GetStrokeColor(obj, &cr, &cg, &cb, &ca) && ca != 0);
|
|
|
|
|
if (got) {
|
|
|
|
|
r.fillColor = "#" + hex2(cr) + hex2(cg) + hex2(cb);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto computeRunParaId = [&](TextRun& r) {
|
|
|
|
|
if (r.objectIndices.empty()) return;
|
|
|
|
|
int minIdx = r.objectIndices[0];
|
|
|
|
|
for (int idx : r.objectIndices) if (idx < minIdx) minIdx = idx;
|
|
|
|
|
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, minIdx);
|
|
|
|
|
if (obj) r.paraId = repagGetParaId(obj);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (auto& p : paragraphs) {
|
|
|
|
|
for (auto& l : p.lines) {
|
|
|
|
|
l.baselineY = l.glyphs.empty() ? 0.0 : l.glyphs[0].originY;
|
|
|
|
|
for (auto& r : l.runs) {
|
|
|
|
|
computeRunBBox(r);
|
|
|
|
|
computeRunColor(r);
|
|
|
|
|
computeRunParaId(r);
|
|
|
|
|
}
|
|
|
|
|
computeLineBBox(l);
|
|
|
|
|
}
|
|
|
|
|
computeParaBBox(p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
model.paragraphs = std::move(paragraphs);
|
|
|
|
|
|
|
|
|
|
return model;
|
|
|
|
|
#else
|
|
|
|
|
return std::unexpected(EngineError::Unknown);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::expected<std::vector<std::string>, EngineError> PdfiumPage::extractAnnotationsText() const {
|
|
|
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
|
|
|
if (!page_) {
|
|
|
|
|
return std::unexpected(EngineError::Unknown);
|
|
|
|
|
}
|
|
|
|
|
std::vector<std::string> result;
|
|
|
|
|
int count = FPDFPage_GetAnnotCount(page_);
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page_, i);
|
|
|
|
|
if (!annot) continue;
|
|
|
|
|
|
|
|
|
|
if (FPDFAnnot_GetSubtype(annot) == FPDF_ANNOT_FREETEXT) {
|
|
|
|
|
unsigned long len = FPDFAnnot_GetStringValue(annot, "Contents", nullptr, 0);
|
|
|
|
|
if (len > 2) {
|
|
|
|
|
std::vector<uint8_t> buf(len);
|
|
|
|
|
FPDFAnnot_GetStringValue(annot, "Contents", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
|
|
|
|
|
while (!text.empty() && text.back() == '\0') {
|
|
|
|
|
text.pop_back();
|
|
|
|
|
}
|
|
|
|
|
if (!text.empty()) {
|
|
|
|
|
result.push_back(text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
#else
|
|
|
|
|
return std::unexpected(EngineError::Unknown);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::expected<std::vector<PdfPage::AnnotationInfo>, EngineError> PdfiumPage::extractAnnotations() const {
|
|
|
|
|
#ifdef PDFENGINE_WITH_PDFIUM
|
|
|
|
|
if (!page_) {
|
|
|
|
|
return std::unexpected(EngineError::Unknown);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FPDF_FORMFILLINFO formInfo;
|
|
|
|
|
memset(&formInfo, 0, sizeof(formInfo));
|
|
|
|
|
formInfo.version = 1;
|
|
|
|
|
FPDF_FORMHANDLE formHandle = FPDFDOC_InitFormFillEnvironment(doc_, &formInfo);
|
|
|
|
|
|
|
|
|
|
std::vector<PdfPage::AnnotationInfo> result;
|
|
|
|
|
int count = FPDFPage_GetAnnotCount(page_);
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
FPDF_ANNOTATION annot = FPDFPage_GetAnnot(page_, i);
|
|
|
|
|
if (!annot) continue;
|
|
|
|
|
|
|
|
|
|
PdfPage::AnnotationInfo info;
|
|
|
|
|
info.pageIndex = pageIndex_;
|
|
|
|
|
|
|
|
|
|
unsigned long len = FPDFAnnot_GetStringValue(annot, "NM", nullptr, 0);
|
|
|
|
|
if (len > 2) {
|
|
|
|
|
std::vector<uint8_t> buf(len);
|
|
|
|
|
FPDFAnnot_GetStringValue(annot, "NM", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
|
|
|
|
|
while (!text.empty() && text.back() == '\0') text.pop_back();
|
|
|
|
|
info.id = text;
|
|
|
|
|
}
|
|
|
|
|
if (info.id.empty()) {
|
|
|
|
|
info.id = "anno_" + std::to_string(pageIndex_) + "_" + std::to_string(i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int subtype = FPDFAnnot_GetSubtype(annot);
|
|
|
|
|
if (subtype == FPDF_ANNOT_HIGHLIGHT) {
|
|
|
|
|
info.type = "highlight";
|
|
|
|
|
} else if (subtype == FPDF_ANNOT_FREETEXT || subtype == FPDF_ANNOT_TEXT) {
|
|
|
|
|
info.type = "comment";
|
|
|
|
|
} else if (subtype == FPDF_ANNOT_INK) {
|
|
|
|
|
info.type = "ink";
|
|
|
|
|
} else if (subtype == FPDF_ANNOT_STRIKEOUT) {
|
|
|
|
|
info.type = "strikeout";
|
|
|
|
|
} else if (subtype == FPDF_ANNOT_UNDERLINE) {
|
|
|
|
|
info.type = "underline";
|
|
|
|
|
} else if (subtype == FPDF_ANNOT_SQUIGGLY) {
|
|
|
|
|
info.type = "squiggly";
|
|
|
|
|
} else if (subtype == FPDF_ANNOT_WIDGET) {
|
|
|
|
|
info.type = "widget";
|
|
|
|
|
|
|
|
|
|
int fieldType = FPDFAnnot_GetFormFieldType(formHandle, annot);
|
|
|
|
|
|
|
|
|
|
if (fieldType == 1 || fieldType == 2 || fieldType == 3) {
|
|
|
|
|
info.fieldType = "Btn";
|
|
|
|
|
} else if (fieldType == 4 || fieldType == 5) {
|
|
|
|
|
info.fieldType = "Ch";
|
|
|
|
|
} else if (fieldType == 6) {
|
|
|
|
|
info.fieldType = "Tx";
|
|
|
|
|
} else if (fieldType == 7) {
|
|
|
|
|
info.fieldType = "Sig";
|
|
|
|
|
} else {
|
|
|
|
|
info.fieldType = "Unknown";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.fieldFlags = FPDFAnnot_GetFormFieldFlags(formHandle, annot);
|
|
|
|
|
|
|
|
|
|
unsigned long nameLen = FPDFAnnot_GetFormFieldName(formHandle, annot, nullptr, 0);
|
|
|
|
|
if (nameLen > 2) {
|
|
|
|
|
std::vector<FPDF_WCHAR> nameBuf(nameLen / 2);
|
|
|
|
|
FPDFAnnot_GetFormFieldName(formHandle, annot, nameBuf.data(), nameLen);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(nameBuf.data()), nameBuf.size());
|
|
|
|
|
while (!text.empty() && text.back() == '\0') text.pop_back();
|
|
|
|
|
info.fieldName = text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unsigned long valLen = FPDFAnnot_GetFormFieldValue(formHandle, annot, nullptr, 0);
|
|
|
|
|
if (valLen > 2) {
|
|
|
|
|
std::vector<FPDF_WCHAR> valBuf(valLen / 2);
|
|
|
|
|
FPDFAnnot_GetFormFieldValue(formHandle, annot, valBuf.data(), valLen);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(valBuf.data()), valBuf.size());
|
|
|
|
|
while (!text.empty() && text.back() == '\0') text.pop_back();
|
|
|
|
|
info.fieldValue = text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (info.fieldType == "Ch") {
|
|
|
|
|
int optCount = FPDFAnnot_GetOptionCount(formHandle, annot);
|
|
|
|
|
for (int o = 0; o < optCount; ++o) {
|
|
|
|
|
unsigned long optLen = FPDFAnnot_GetOptionLabel(formHandle, annot, o, nullptr, 0);
|
|
|
|
|
if (optLen > 2) {
|
|
|
|
|
std::vector<FPDF_WCHAR> optBuf(optLen / 2);
|
|
|
|
|
FPDFAnnot_GetOptionLabel(formHandle, annot, o, optBuf.data(), optLen);
|
|
|
|
|
std::string optText = utf16le_to_utf8(reinterpret_cast<const char16_t*>(optBuf.data()), optBuf.size());
|
|
|
|
|
while (!optText.empty() && optText.back() == '\0') optText.pop_back();
|
|
|
|
|
info.fieldOptions.push_back(optText);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
info.type = "unknown";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (info.type != "unknown") {
|
|
|
|
|
FS_RECTF rect;
|
|
|
|
|
if (FPDFAnnot_GetRect(annot, &rect)) {
|
|
|
|
|
int dw = static_cast<int>(std::round(width()));
|
|
|
|
|
int dh = static_cast<int>(std::round(height()));
|
|
|
|
|
DevicePoint topLeft = pageToDevice({rect.left, rect.top}, dw, dh, 0);
|
|
|
|
|
DevicePoint bottomRight = pageToDevice({rect.right, rect.bottom}, dw, dh, 0);
|
|
|
|
|
|
|
|
|
|
info.x = (std::min)(topLeft.x, bottomRight.x);
|
|
|
|
|
info.y = (std::min)(topLeft.y, bottomRight.y);
|
|
|
|
|
info.width = std::abs(bottomRight.x - topLeft.x);
|
|
|
|
|
info.height = std::abs(bottomRight.y - topLeft.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
len = FPDFAnnot_GetStringValue(annot, "T", nullptr, 0);
|
|
|
|
|
if (len > 2) {
|
|
|
|
|
std::vector<uint8_t> buf(len);
|
|
|
|
|
FPDFAnnot_GetStringValue(annot, "T", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
|
|
|
|
|
while (!text.empty() && text.back() == '\0') text.pop_back();
|
|
|
|
|
info.author = text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
len = FPDFAnnot_GetStringValue(annot, "Contents", nullptr, 0);
|
|
|
|
|
if (len > 2) {
|
|
|
|
|
std::vector<uint8_t> buf(len);
|
|
|
|
|
FPDFAnnot_GetStringValue(annot, "Contents", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
|
|
|
|
|
while (!text.empty() && text.back() == '\0') text.pop_back();
|
|
|
|
|
info.content = text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
len = FPDFAnnot_GetStringValue(annot, "M", nullptr, 0);
|
|
|
|
|
if (len <= 2) {
|
|
|
|
|
len = FPDFAnnot_GetStringValue(annot, "CreationDate", nullptr, 0);
|
|
|
|
|
if (len > 2) {
|
|
|
|
|
std::vector<uint8_t> buf(len);
|
|
|
|
|
FPDFAnnot_GetStringValue(annot, "CreationDate", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
|
|
|
|
|
while (!text.empty() && text.back() == '\0') text.pop_back();
|
|
|
|
|
info.timestamp = text;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
std::vector<uint8_t> buf(len);
|
|
|
|
|
FPDFAnnot_GetStringValue(annot, "M", reinterpret_cast<FPDF_WCHAR*>(buf.data()), len);
|
|
|
|
|
std::string text = utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), len / sizeof(char16_t));
|
|
|
|
|
while (!text.empty() && text.back() == '\0') text.pop_back();
|
|
|
|
|
info.timestamp = text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unsigned int R = 0, G = 0, B = 0, A = 0;
|
|
|
|
|
if (FPDFAnnot_GetColor(annot, FPDFANNOT_COLORTYPE_Color, &R, &G, &B, &A)) {
|
|
|
|
|
char hex[10];
|
|
|
|
|
snprintf(hex, sizeof(hex), "#%02x%02x%02x", R, G, B);
|
|
|
|
|
info.color = hex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (subtype == FPDF_ANNOT_INK) {
|
|
|
|
|
const double pageH = height();
|
|
|
|
|
unsigned long strokeCount = FPDFAnnot_GetInkListCount(annot);
|
|
|
|
|
for (unsigned long s = 0; s < strokeCount; ++s) {
|
|
|
|
|
unsigned long ptCount = FPDFAnnot_GetInkListPath(annot, s, nullptr, 0);
|
|
|
|
|
if (ptCount == 0) continue;
|
|
|
|
|
std::vector<FS_POINTF> pts(ptCount);
|
|
|
|
|
FPDFAnnot_GetInkListPath(annot, s, pts.data(), ptCount);
|
|
|
|
|
std::vector<Point2D> stroke;
|
|
|
|
|
stroke.reserve(ptCount);
|
|
|
|
|
for (const auto& p : pts) {
|
|
|
|
|
stroke.push_back(Point2D{static_cast<double>(p.x), pageH - static_cast<double>(p.y)});
|
|
|
|
|
}
|
|
|
|
|
if (!stroke.empty()) info.paths.push_back(std::move(stroke));
|
|
|
|
|
}
|
2026-07-07 19:03:11 +05:30
|
|
|
} else if (subtype == FPDF_ANNOT_HIGHLIGHT || subtype == FPDF_ANNOT_STRIKEOUT || subtype == FPDF_ANNOT_UNDERLINE || subtype == FPDF_ANNOT_SQUIGGLY) {
|
|
|
|
|
const double pageH = height();
|
|
|
|
|
size_t quadCount = FPDFAnnot_CountAttachmentPoints(annot);
|
|
|
|
|
for (size_t q = 0; q < quadCount; ++q) {
|
|
|
|
|
FS_QUADPOINTSF quad;
|
|
|
|
|
if (FPDFAnnot_GetAttachmentPoints(annot, q, &quad)) {
|
|
|
|
|
std::array<Point2D, 4> pts = {{
|
|
|
|
|
{static_cast<double>(quad.x1), pageH - static_cast<double>(quad.y1)},
|
|
|
|
|
{static_cast<double>(quad.x2), pageH - static_cast<double>(quad.y2)},
|
|
|
|
|
{static_cast<double>(quad.x3), pageH - static_cast<double>(quad.y3)},
|
|
|
|
|
{static_cast<double>(quad.x4), pageH - static_cast<double>(quad.y4)}
|
|
|
|
|
}};
|
|
|
|
|
info.quadPoints.push_back(pts);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-29 10:51:04 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.push_back(info);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FPDFPage_CloseAnnot(annot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FPDFDOC_ExitFormFillEnvironment(formHandle);
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
#else
|
|
|
|
|
return std::unexpected(EngineError::Unknown);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|