Files
pdf/engine/src/parser/pdfium_page_model.cpp
T
2026-07-30 16:48:46 +05:30

677 lines
28 KiB
C++

#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;
}
FS_MATRIX mtx;
if (FPDFPageObj_GetMatrix(textObj, &mtx)) {
double scaleY1 = std::hypot(static_cast<double>(mtx.c), static_cast<double>(mtx.d));
double scaleY2 = std::hypot(static_cast<double>(mtx.a), static_cast<double>(mtx.b));
double scaleY = (std::max)(scaleY1, scaleY2);
if (scaleY > 0.001) {
double trueSize = g.fontSize * scaleY;
if (trueSize > 0.1) g.fontSize = trueSize;
}
}
}
if (g.bboxH > g.fontSize * 0.8) {
g.fontSize = (std::max)(g.fontSize, g.bboxH);
}
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) {
float h_radius = 0, v_radius = 0, border_width = 1.0f;
info.thickness = 2.0; // default
unsigned long ctLen = FPDFAnnot_GetStringValue(annot, "CustomThickness", nullptr, 0);
if (ctLen > 2) {
std::vector<char16_t> ctBuf(ctLen / 2);
FPDFAnnot_GetStringValue(annot, "CustomThickness", reinterpret_cast<FPDF_WCHAR*>(ctBuf.data()), ctLen);
std::string ctStr = utf16le_to_utf8(ctBuf.data(), ctBuf.size());
try {
info.thickness = std::stod(ctStr);
} catch (...) {}
} else if (FPDFAnnot_GetBorder(annot, &h_radius, &v_radius, &border_width)) {
info.thickness = border_width;
}
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));
}
} 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);
}
}
}
result.push_back(info);
}
FPDFPage_CloseAnnot(annot);
}
FPDFDOC_ExitFormFillEnvironment(formHandle);
return result;
#else
return std::unexpected(EngineError::Unknown);
#endif
}
}