Files
pdf/engine/src/parser/pdfium_document.cpp
T
2026-06-06 11:21:31 +05:30

1574 lines
57 KiB
C++

#include "parser/pdfium_document.hpp"
#ifdef PDFENGINE_WITH_PDFIUM
#include <fpdfview.h>
#include <fpdf_text.h>
#include <fpdf_save.h>
#include <fpdf_doc.h>
#include <fpdf_edit.h>
#include <fpdf_annot.h>
#include <png.h>
#include "parser/pdfium_loader.hpp"
#endif
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <csetjmp>
namespace pdfengine::parser {
std::vector<unsigned short> utf8_to_utf16le(const std::string& utf8) {
std::vector<unsigned short> utf16;
utf16.reserve(utf8.size());
for (size_t i = 0; i < utf8.size(); ) {
unsigned char c = utf8[i];
unsigned int cp = 0;
size_t extra = 0;
if (c < 0x80) { cp = c; extra = 0; }
else if ((c & 0xE0) == 0xC0) { cp = c & 0x1F; extra = 1; }
else if ((c & 0xF0) == 0xE0) { cp = c & 0x0F; extra = 2; }
else if ((c & 0xF8) == 0xF0) { cp = c & 0x07; extra = 3; }
else { i++; continue; }
if (i + extra >= utf8.size()) break;
bool invalid = false;
for (size_t j = 1; j <= extra; ++j) {
unsigned char next = utf8[i + j];
if ((next & 0xC0) != 0x80) { invalid = true; break; }
cp = (cp << 6) | (next & 0x3F);
}
if (invalid) { i++; continue; }
i += 1 + extra;
if (cp < 0x10000) {
utf16.push_back(static_cast<unsigned short>(cp));
} else {
cp -= 0x10000;
utf16.push_back(static_cast<unsigned short>((cp >> 10) + 0xD800));
utf16.push_back(static_cast<unsigned short>((cp & 0x3FF) + 0xDC00));
}
}
utf16.push_back(0);
return utf16;
}
std::string utf16le_to_utf8(const char16_t* utf16, size_t length) {
std::string utf8;
for (size_t i = 0; i < length; ++i) {
char16_t c = utf16[i];
if (c == 0) break;
uint32_t cp = c;
if (c >= 0xD800 && c <= 0xDBFF) { // High surrogate
if (i + 1 < length) {
char16_t low = utf16[i + 1];
if (low >= 0xDC00 && low <= 0xDFFF) { // Low surrogate
cp = 0x10000 + (((c - 0xD800) << 10) | (low - 0xDC00));
i++; // Consume the low surrogate
}
}
}
if (cp < 0x80) {
utf8 += static_cast<char>(cp);
} else if (cp < 0x800) {
utf8 += static_cast<char>(0xC0 | (cp >> 6));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
utf8 += static_cast<char>(0xE0 | (cp >> 12));
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
} else {
utf8 += static_cast<char>(0xF0 | (cp >> 18));
utf8 += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
}
}
return utf8;
}
std::string code_point_to_utf8(unsigned int cp) {
std::string utf8;
if (cp == 0) return "";
if (cp < 0x80) {
utf8 += static_cast<char>(cp);
} else if (cp < 0x800) {
utf8 += static_cast<char>(0xC0 | (cp >> 6));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
utf8 += static_cast<char>(0xE0 | (cp >> 12));
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x200000) {
utf8 += static_cast<char>(0xF0 | (cp >> 18));
utf8 += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
}
return utf8;
}
} // namespace pdfengine::parser
namespace {
#ifdef PDFENGINE_WITH_PDFIUM
struct VectorWriter : public FPDF_FILEWRITE {
std::vector<uint8_t> buffer;
static int WriteBlockCallback(FPDF_FILEWRITE* pThis, const void* pData, unsigned long size) {
auto* self = static_cast<VectorWriter*>(pThis);
const auto* bytes = static_cast<const uint8_t*>(pData);
self->buffer.insert(self->buffer.end(), bytes, bytes + size);
return 1;
}
VectorWriter() {
this->version = 1;
this->WriteBlock = &VectorWriter::WriteBlockCallback;
}
};
struct PngWriteState {
std::vector<uint8_t>* buffer;
};
void pngWriteCallback(png_structp png_ptr, png_bytep data, png_size_t length) {
auto* state = reinterpret_cast<PngWriteState*>(png_get_io_ptr(png_ptr));
state->buffer->insert(state->buffer->end(), data, data + length);
}
void pngFlushCallback(png_structp png_ptr) {
(void)png_ptr;
}
std::vector<uint8_t> encodeBgraToPng(const uint8_t* bgra, int width, int height, int stride) {
std::vector<uint8_t> pngBytes;
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png) return {};
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_write_struct(&png, nullptr);
return {};
}
#pragma warning(push)
#pragma warning(disable: 4611)
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
return {};
}
#pragma warning(pop)
PngWriteState state{&pngBytes};
png_set_write_fn(png, &state, pngWriteCallback, pngFlushCallback);
png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
png_set_bgr(png);
std::vector<png_bytep> rowPointers(height);
for (int y = 0; y < height; ++y) {
rowPointers[y] = const_cast<png_bytep>(bgra + y * stride);
}
png_write_image(png, rowPointers.data());
png_write_end(png, nullptr);
png_destroy_write_struct(&png, &info);
return pngBytes;
}
pdfengine::EngineError mapPdfiumError(unsigned long err, bool passwordProvided) {
switch (err) {
case FPDF_ERR_SUCCESS:
return pdfengine::EngineError::Unknown;
case FPDF_ERR_FILE:
return pdfengine::EngineError::FileNotFound;
case FPDF_ERR_FORMAT:
return pdfengine::EngineError::InvalidFormat;
case FPDF_ERR_PASSWORD:
return passwordProvided ? pdfengine::EngineError::InvalidPassword
: pdfengine::EngineError::PasswordRequired;
default:
return pdfengine::EngineError::Unknown;
}
}
#endif
#ifdef PDFENGINE_WITH_PDFIUM
struct PdfiumGlobalInit {
PdfiumGlobalInit() {
pdfengine::parser::pdfiumInitLibrary();
}
~PdfiumGlobalInit() {
pdfengine::parser::pdfiumDestroyLibrary();
}
};
void ensure_pdfium_initialized() {
static PdfiumGlobalInit init;
}
std::string normalizeFamilyName(const std::string& fontName) {
// 1. Remove subset tag if present
std::string name = fontName;
if (name.size() > 7 && name[6] == '+') {
name = name.substr(7);
}
// 2. Strip standard suffixes
size_t sep = name.find_first_of("-,");
if (sep != std::string::npos) {
name = name.substr(0, sep);
}
// 3. Clean up common postfixes
auto cleanName = name;
auto lower = name;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
std::vector<std::string> suffixes = {"bold", "italic", "oblique", "regular", "medium", "light", "heavy", "black", "condensed", "mt", "ps"};
for (const auto& s : suffixes) {
size_t pos = lower.rfind(s);
if (pos != std::string::npos && pos + s.size() == lower.size()) {
cleanName = cleanName.substr(0, pos);
lower = lower.substr(0, pos);
}
}
// Strip trailing punctuation
while (!cleanName.empty() && (cleanName.back() == '-' || cleanName.back() == ' ' || cleanName.back() == '_')) {
cleanName.pop_back();
}
if (cleanName.empty()) return fontName;
return cleanName;
}
void deduceFontMetadata(pdfengine::FontInfo& f) {
auto lowerName = f.fontName;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
// 1. Subset Tag & Family Normalization
if (f.fontName.size() > 7 && f.fontName[6] == '+') {
f.isSubset = true;
f.subsetTag = f.fontName.substr(0, 6);
// Validate: must be exactly 6 uppercase letters
bool validTag = true;
for (int ti = 0; ti < 6; ++ti) {
if (!std::isupper(static_cast<unsigned char>(f.fontName[ti]))) {
validTag = false; break;
}
}
if (!validTag) { f.isSubset = false; f.subsetTag = ""; }
} else {
f.isSubset = false;
f.subsetTag = "";
}
f.normalizedFamily = normalizeFamilyName(f.fontName);
// 2. Encoding & CMap Identification (name-based heuristic; may be overridden by caller)
if (f.fontName.find("Identity-H") != std::string::npos) {
f.encoding = "Identity-H";
f.cmapName = "Identity-H";
} else if (f.fontName.find("Identity-V") != std::string::npos) {
f.encoding = "Identity-V";
f.cmapName = "Identity-V";
f.isVertical = true;
} else if (lowerName.find("symbol") != std::string::npos) {
f.encoding = "Symbol";
f.cmapName = "None";
} else {
f.encoding = "WinAnsiEncoding";
f.cmapName = "None";
}
// 3. ToUnicode Map Availability
// Subset embedded fonts almost always have ToUnicode; symbol fonts rarely do.
if (lowerName.find("symbol") != std::string::npos) {
f.hasToUnicode = false;
} else {
f.hasToUnicode = true;
}
// 4. CID System Info Registry — expanded patterns
if (lowerName.find("simsun") != std::string::npos ||
lowerName.find("simhei") != std::string::npos ||
lowerName.find("heiti") != std::string::npos ||
lowerName.find("fangsong") != std::string::npos ||
lowerName.find("kaiti") != std::string::npos ||
lowerName.find("song") != std::string::npos ||
lowerName.find("gb") != std::string::npos) {
f.cidSystemInfo = "Adobe-GB1";
} else if (lowerName.find("gothic") != std::string::npos ||
lowerName.find("ms-gothic") != std::string::npos ||
lowerName.find("msgothic") != std::string::npos ||
lowerName.find("mincho") != std::string::npos ||
lowerName.find("kozuka") != std::string::npos ||
lowerName.find("hiragino") != std::string::npos ||
lowerName.find("japan") != std::string::npos ||
lowerName.find("heisei") != std::string::npos ||
lowerName.find("morisawa") != std::string::npos ||
lowerName.find("ryumin") != std::string::npos) {
f.cidSystemInfo = "Adobe-Japan1";
} else if (lowerName.find("malgun") != std::string::npos ||
lowerName.find("gulim") != std::string::npos ||
lowerName.find("batang") != std::string::npos ||
lowerName.find("dotum") != std::string::npos ||
lowerName.find("korea") != std::string::npos ||
lowerName.find("hangul") != std::string::npos ||
lowerName.find("korean") != std::string::npos) {
f.cidSystemInfo = "Adobe-Korea1";
} else if (lowerName.find("sung") != std::string::npos ||
lowerName.find("ming") != std::string::npos ||
lowerName.find("cns") != std::string::npos ||
lowerName.find("traditional") != std::string::npos) {
f.cidSystemInfo = "Adobe-CNS1";
} else {
f.cidSystemInfo = "None";
}
if (f.cidSystemInfo != "None" && f.cmapName == "None") {
f.cmapName = f.isVertical ? "UniJIS-UTF16-V" : "Identity-H";
}
// 5. Font Type Identification — improved heuristics
bool isCid = (f.encoding == "Identity-H" || f.encoding == "Identity-V" || f.cidSystemInfo != "None");
if (isCid) {
if (lowerName.find("bold") != std::string::npos || lowerName.find("italic") != std::string::npos) {
f.type = "CIDFontType0";
} else {
f.type = "CIDFontType2";
}
} else {
// Standard PDF Type 1 fonts
static const std::vector<std::string> type1Names = {
"times", "helvetica", "courier", "symbol", "zapfdingbats",
"liberation", "palatino", "bookman", "new century", "avant garde"
};
bool isType1 = false;
for (const auto& t1 : type1Names) {
if (lowerName.find(t1) != std::string::npos) { isType1 = true; break; }
}
f.type = isType1 ? "Type1" : "TrueType";
}
// 6. Source Type & Embedding Status
// PDFium font flags: bit 3 (value 4) = Symbolic, but NOT embed status.
// Embed status is indicated by subset prefix or by PDF font stream presence.
// We use the subset tag as definitive embed indicator, otherwise check known standard fonts.
if (f.isSubset) {
f.isEmbedded = true;
f.sourceType = "Embedded";
f.substitutedFrom = "";
f.substitutedTo = "";
} else {
// PDF standard 14 fonts are NEVER embedded
static const std::vector<std::string> standard14 = {
"helvetica", "times", "courier", "symbol", "zapfdingbats"
};
bool isStandard14 = false;
for (const auto& s14 : standard14) {
if (lowerName.find(s14) != std::string::npos) { isStandard14 = true; break; }
}
// Common system fonts also treated as non-embedded
bool isSystemFont = isStandard14 ||
lowerName.find("arial") != std::string::npos ||
lowerName.find("liberation") != std::string::npos ||
lowerName.find("dejavu") != std::string::npos ||
lowerName.find("freefont") != std::string::npos;
if (isSystemFont) {
f.isEmbedded = false;
f.sourceType = "SystemFallback";
f.substitutedFrom = "";
f.substitutedTo = "";
} else {
f.isEmbedded = false;
f.sourceType = "Substituted";
f.substitutedFrom = f.fontName;
#if defined(_WIN32)
f.substitutedTo = "Arial";
#else
f.substitutedTo = "Liberation Sans";
#endif
spdlog::warn("Font fallback occurred: '{}' -> '{}'", f.substitutedFrom, f.substitutedTo);
}
}
// 7. Stable Internal Font Identifier
// For subset fonts, fontName already contains the subset prefix (e.g. "ABCDEF+Arial").
// Use the full fontName directly — it already encodes both the subset tag and
// the base font name, separated by '+'. Concatenating subsetTag + "_" + fontName
// would duplicate the prefix ("ABCDEF_ABCDEF+Arial").
if (f.isSubset && !f.subsetTag.empty()) {
// fontName is "ABCDEF+Arial"; use it as-is for the stable ID.
f.internalFontId = f.fontName;
} else {
f.internalFontId = f.fontName + "_" + f.type + "_" + std::to_string(f.flags);
}
// 8. Descriptor Metrics — actual values from standard font specifications
if (lowerName.find("times") != std::string::npos) {
f.ascent = 891.0;
f.descent = -216.0;
f.capHeight = 662.0;
} else if (lowerName.find("courier") != std::string::npos) {
f.ascent = 629.0;
f.descent = -157.0;
f.capHeight = 562.0;
} else if (lowerName.find("symbol") != std::string::npos) {
f.ascent = 1010.0;
f.descent = -293.0;
f.capHeight = 673.0;
} else if (lowerName.find("helvetica") != std::string::npos) {
f.ascent = 905.0;
f.descent = -211.0;
f.capHeight = 728.0;
} else {
// Generic fallback metrics (reasonable defaults for TrueType/CID fonts)
f.ascent = 905.0;
f.descent = -211.0;
f.capHeight = 728.0;
}
}
// ---------------------------------------------------------------------------
// FontPdfData — real values extracted from the PDF font dictionary via PDFium.
// Populated by buildFontPdfDataMap() and used to override deduceFontMetadata()
// results in PdfiumPage::getFonts().
// ---------------------------------------------------------------------------
struct FontPdfData {
bool valid = false;
bool isEmbedded = false;
int flags = 0; // PDF /Flags from FontDescriptor
double ascent = 0.0; // in PDF 1000-unit space
double descent = 0.0; // in PDF 1000-unit space
bool hasUnicodeMapping = false; // at least one char decoded to valid Unicode
};
// Builds a map from font-name to FontPdfData by scanning page text objects.
//
// Phase 1 — page object walk:
// Uses FPDFTextObj_GetFont() + FPDFFont_Get*() to read the actual PDF font
// dictionary fields: embedded status, descriptor flags and metrics.
// These values replace the corresponding fields that deduceFontMetadata()
// previously guessed from the font name string.
//
// Phase 2 — text-page character scan:
// For each font encountered in Phase 1, tests whether FPDFText_GetUnicode()
// returns a valid codepoint for at least one character belonging to that font.
// If yes, the font has an active Unicode mapping (ToUnicode CMap or built-in
// encoding). This replaces the previous always-true heuristic.
static std::unordered_map<std::string, FontPdfData>
buildFontPdfDataMap(FPDF_PAGE page, FPDF_TEXTPAGE textPage) {
std::unordered_map<std::string, FontPdfData> result;
if (!page) return result;
// ---- Phase 1: page object walk to collect real font properties ----
int objectCount = FPDFPage_CountObjects(page);
for (int i = 0; i < objectCount; ++i) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, i);
if (!obj) continue;
if (FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
FPDF_FONT font = FPDFTextObj_GetFont(obj);
if (!font) continue;
// Retrieve font name — first call returns required buffer size.
unsigned long nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
if (nameLen == 0) continue;
std::vector<char> nameBuf(nameLen);
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) == 0) continue;
std::string fontName(nameBuf.data());
if (fontName.empty()) continue;
// Each font name is processed only once.
if (result.count(fontName)) continue;
FontPdfData data;
data.valid = true;
// FPDFFont_GetIsEmbedded returns 1 when the PDF contains an embedded
// font stream (/FontFile, /FontFile2, or /FontFile3 in the descriptor).
data.isEmbedded = (FPDFFont_GetIsEmbedded(font) != 0);
// FPDFFont_GetFlags returns the actual /Flags integer from the PDF
// FontDescriptor dictionary (not the text-rendering flags).
data.flags = FPDFFont_GetFlags(font);
// FPDFFont_GetAscent/Descent are scaled by font_size.
// Passing 1000.0 recovers the raw PDF 1000-unit-space values.
float rawAscent = 0.0f;
if (FPDFFont_GetAscent(font, 1000.0f, &rawAscent) && rawAscent != 0.0f) {
data.ascent = static_cast<double>(rawAscent);
}
float rawDescent = 0.0f;
if (FPDFFont_GetDescent(font, 1000.0f, &rawDescent) && rawDescent != 0.0f) {
data.descent = static_cast<double>(rawDescent);
}
result[fontName] = data;
}
// ---- Phase 2: Unicode mapping probe via text-page character scan ----
// For each known font, test whether PDFium can return a non-trivial Unicode
// value for at least one character that belongs to it. A valid decode
// implies the font has an active ToUnicode CMap or a built-in encoding map.
if (textPage && !result.empty()) {
int charCount = FPDFText_CountChars(textPage);
if (charCount > 0 && charCount < 1000000) {
for (int ci = 0; ci < charCount; ++ci) {
int fi = 0;
unsigned long flen =
FPDFText_GetFontInfo(textPage, ci, nullptr, 0, &fi);
if (flen == 0) continue;
std::vector<char> fbuf(flen);
if (FPDFText_GetFontInfo(textPage, ci, fbuf.data(), flen, &fi) == 0)
continue;
std::string fname(fbuf.data());
auto it = result.find(fname);
// Skip fonts not found in Phase 1, or already confirmed.
if (it == result.end() || it->second.hasUnicodeMapping) continue;
unsigned int cp = FPDFText_GetUnicode(textPage, ci);
// Require a printable codepoint — exclude NUL, control chars,
// and the Unicode replacement character (U+FFFD).
if (cp > 0x0020 && cp != 0xFFFD) {
it->second.hasUnicodeMapping = true;
}
}
}
}
return result;
}
#endif
void parseHexColor(const std::string& hex, unsigned int& r, unsigned int& g, unsigned int& b) {
r = 0; g = 0; b = 0;
if (hex.empty()) return;
std::string s = hex;
if (s[0] == '#') {
s = s.substr(1);
}
if (s.size() == 6) {
try {
r = std::stoul(s.substr(0, 2), nullptr, 16);
g = std::stoul(s.substr(2, 2), nullptr, 16);
b = std::stoul(s.substr(4, 2), nullptr, 16);
} catch (...) {
r = 0; g = 0; b = 0;
}
}
}
}
namespace pdfengine {
std::expected<std::shared_ptr<PdfDocument>, EngineError>
PdfDocument::loadFromFile(const std::string& path, const std::string& password) {
#ifdef PDFENGINE_WITH_PDFIUM
ensure_pdfium_initialized();
FPDF_DOCUMENT doc = FPDF_LoadDocument(path.c_str(), password.empty() ? nullptr : password.c_str());
if (!doc) {
auto err = FPDF_GetLastError();
spdlog::error("Failed to load PDF file from path: {} (error code: {})", path, err);
return std::unexpected(mapPdfiumError(err, !password.empty()));
}
return std::make_shared<parser::PdfiumDocument>(doc);
#else
(void)path;
(void)password;
spdlog::error("loadFromFile failed: PDFEngine compiled without PDFium support.");
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::shared_ptr<PdfDocument>, EngineError>
PdfDocument::loadFromMemory(const std::vector<uint8_t>& data, const std::string& password) {
#ifdef PDFENGINE_WITH_PDFIUM
ensure_pdfium_initialized();
if (data.empty()) {
return std::unexpected(EngineError::InvalidFormat);
}
std::vector<uint8_t> buffer_copy = data;
FPDF_DOCUMENT doc = FPDF_LoadMemDocument(buffer_copy.data(), static_cast<int>(buffer_copy.size()),
password.empty() ? nullptr : password.c_str());
if (!doc) {
auto err = FPDF_GetLastError();
spdlog::error("Failed to load PDF from memory (error code: {})", err);
return std::unexpected(mapPdfiumError(err, !password.empty()));
}
return std::make_shared<parser::PdfiumDocument>(doc, std::move(buffer_copy));
#else
(void)data;
(void)password;
spdlog::error("loadFromMemory failed: PDFEngine compiled without PDFium support.");
return std::unexpected(EngineError::Unknown);
#endif
}
}
namespace pdfengine::parser {
PdfiumPage::PdfiumPage(NativePageHandle pageHandle, int pageIndex)
: page_(pageHandle), pageIndex_(pageIndex) {}
PdfiumPage::~PdfiumPage() {
#ifdef PDFENGINE_WITH_PDFIUM
std::lock_guard<std::mutex> lock(textMutex_);
if (textPage_) {
FPDFText_ClosePage(textPage_);
}
if (page_) {
FPDF_ClosePage(page_);
}
#endif
}
PdfiumPage::PdfiumPage(PdfiumPage&& other) noexcept {
*this = std::move(other);
}
PdfiumPage& PdfiumPage::operator=(PdfiumPage&& other) noexcept {
if (this != &other) {
#ifdef PDFENGINE_WITH_PDFIUM
std::lock_guard<std::mutex> lock(textMutex_);
if (textPage_) FPDFText_ClosePage(textPage_);
if (page_) FPDF_ClosePage(page_);
#endif
page_ = other.page_;
textPage_ = other.textPage_;
pageIndex_ = other.pageIndex_;
other.page_ = nullptr;
other.textPage_ = nullptr;
other.pageIndex_ = 0;
}
return *this;
}
double PdfiumPage::width() const noexcept {
#ifdef PDFENGINE_WITH_PDFIUM
return page_ ? FPDF_GetPageWidthF(page_) : 0.0;
#else
return 0.0;
#endif
}
double PdfiumPage::height() const noexcept {
#ifdef PDFENGINE_WITH_PDFIUM
return page_ ? FPDF_GetPageHeightF(page_) : 0.0;
#else
return 0.0;
#endif
}
std::expected<PageImage, EngineError> PdfiumPage::render(int dpi) const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
double scale = dpi / 72.0;
int w = static_cast<int>(width() * scale);
int h = static_cast<int>(height() * scale);
FPDF_BITMAP bitmap = FPDFBitmap_Create(w, h, 1);
if (!bitmap) {
return std::unexpected(EngineError::RenderFailed);
}
FPDFBitmap_FillRect(bitmap, 0, 0, w, h, 0xFFFFFFFF);
FPDF_RenderPageBitmap(bitmap, page_, 0, 0, w, h, 0, 0);
const auto* buffer = static_cast<const uint8_t*>(FPDFBitmap_GetBuffer(bitmap));
int stride = FPDFBitmap_GetStride(bitmap);
std::vector<uint8_t> pngBytes = encodeBgraToPng(buffer, w, h, stride);
FPDFBitmap_Destroy(bitmap);
if (pngBytes.empty()) {
return std::unexpected(EngineError::RenderFailed);
}
return PageImage{w, h, std::move(pngBytes)};
#else
(void)dpi;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::string, EngineError> PdfiumPage::extractText() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
ensureTextPageLoaded();
if (!textPage_) {
return std::unexpected(EngineError::Unknown);
}
int charCount = FPDFText_CountChars(textPage_);
if (charCount <= 0) {
return "";
}
std::vector<unsigned short> buffer(charCount + 1, 0);
int written = FPDFText_GetText(textPage_, 0, charCount, buffer.data());
if (written <= 0) {
return "";
}
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buffer.data()), written);
#else
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::vector<GlyphBounds>, EngineError> PdfiumPage::extractTextWithBounds() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
ensureTextPageLoaded();
if (!textPage_) {
return std::unexpected(EngineError::Unknown);
}
int charCount = FPDFText_CountChars(textPage_);
std::vector<GlyphBounds> result;
if (charCount <= 0) {
return result;
}
result.reserve(charCount);
for (int i = 0; i < charCount; ++i) {
unsigned int codeUnit = FPDFText_GetUnicode(textPage_, i);
unsigned int cp = codeUnit;
double left = 0, right = 0, bottom = 0, top = 0;
double fontSize = FPDFText_GetFontSize(textPage_, i);
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);
double l1 = 0, r1 = 0, b1 = 0, t1 = 0;
FPDFText_GetCharBox(textPage_, i, &l1, &r1, &b1, &t1);
double l2 = 0, r2 = 0, b2 = 0, t2 = 0;
FPDFText_GetCharBox(textPage_, i + 1, &l2, &r2, &b2, &t2);
left = (std::min)(l1, l2);
right = (std::max)(r1, r2);
bottom = (std::min)(b1, b2);
top = (std::max)(t1, t2);
++i;
} else {
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
}
} else {
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
}
std::string utf8_char = code_point_to_utf8(cp);
if (utf8_char.empty() || cp == '\r' || cp == '\n') {
continue;
}
double x = (std::min)(left, right);
double y = (std::min)(bottom, top);
double w = std::abs(right - left);
double h = std::abs(top - bottom);
GlyphBounds gb;
gb.text = std::move(utf8_char);
gb.x = x;
gb.y = y;
gb.w = w;
gb.h = h;
gb.fontSize = fontSize;
result.push_back(gb);
}
return result;
#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);
}
}
}
FPDFPage_CloseAnnot(annot);
}
return result;
#else
return std::unexpected(EngineError::Unknown);
#endif
}
DevicePoint PdfiumPage::pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) return {0, 0};
int dx = 0, dy = 0;
FPDF_PageToDevice(page_, 0, 0, deviceWidth, deviceHeight, rotate, pagePoint.x, pagePoint.y, &dx, &dy);
return {dx, dy};
#else
(void)pagePoint; (void)deviceWidth; (void)deviceHeight; (void)rotate;
return {0, 0};
#endif
}
Point2D PdfiumPage::deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) return {0.0, 0.0};
double px = 0.0, py = 0.0;
FPDF_DeviceToPage(page_, 0, 0, deviceWidth, deviceHeight, rotate, devicePoint.x, devicePoint.y, &px, &py);
return {px, py};
#else
(void)devicePoint; (void)deviceWidth; (void)deviceHeight; (void)rotate;
return {0.0, 0.0};
#endif
}
std::expected<double, EngineError> PdfiumPage::getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
FPDF_FONT font = nullptr;
{
std::lock_guard<std::mutex> lock(textMutex_);
auto cached = fontHandleCache_.find(fontName);
if (cached != fontHandleCache_.end()) {
font = cached->second;
} else {
int objectCount = FPDFPage_CountObjects(page_);
for (int i = 0; i < objectCount; ++i) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, i);
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
FPDF_FONT pageFont = FPDFTextObj_GetFont(obj);
if (!pageFont) continue;
size_t nameLen = FPDFFont_GetBaseFontName(pageFont, nullptr, 0);
if (nameLen > 0) {
std::vector<char> nameBuf(nameLen);
if (FPDFFont_GetBaseFontName(pageFont, nameBuf.data(), nameLen) > 0) {
std::string currentName(nameBuf.data());
if (currentName == fontName) {
font = pageFont;
fontHandleCache_[fontName] = font;
break;
}
}
}
}
}
}
if (!font) {
return std::unexpected(EngineError::Unknown);
}
float width = 0.0f;
if (!FPDFFont_GetGlyphWidth(font, charcode, static_cast<float>(fontSize), &width)) {
return std::unexpected(EngineError::Unknown);
}
return static_cast<double>(width);
#else
(void)fontName; (void)charcode; (void)fontSize;
return std::unexpected(EngineError::Unknown);
#endif
}
void PdfiumPage::ensureTextPageLoaded() const {
#ifdef PDFENGINE_WITH_PDFIUM
std::lock_guard<std::mutex> lock(textMutex_);
if (!textPage_ && page_) {
textPage_ = FPDFText_LoadPage(page_);
}
#endif
}
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle)
: doc_(docHandle) {}
PdfiumDocument::PdfiumDocument(NativeDocHandle docHandle, std::vector<uint8_t> memoryBuffer)
: doc_(docHandle), memoryBuffer_(std::move(memoryBuffer)) {}
PdfiumDocument::~PdfiumDocument() {
#ifdef PDFENGINE_WITH_PDFIUM
if (doc_) {
FPDF_CloseDocument(doc_);
}
#endif
}
PdfiumDocument::PdfiumDocument(PdfiumDocument&& other) noexcept {
*this = std::move(other);
}
PdfiumDocument& PdfiumDocument::operator=(PdfiumDocument&& other) noexcept {
if (this != &other) {
#ifdef PDFENGINE_WITH_PDFIUM
if (doc_) FPDF_CloseDocument(doc_);
#endif
doc_ = other.doc_;
other.doc_ = nullptr;
memoryBuffer_ = std::move(other.memoryBuffer_);
}
return *this;
}
int PdfiumDocument::pageCount() const noexcept {
#ifdef PDFENGINE_WITH_PDFIUM
return doc_ ? FPDF_GetPageCount(doc_) : 0;
#else
return 0;
#endif
}
DocumentMetadata PdfiumDocument::metadata() const noexcept {
DocumentMetadata meta;
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) return meta;
auto fetchMeta = [this](const char* key) -> std::string {
unsigned long len = FPDF_GetMetaText(doc_, key, nullptr, 0);
if (len <= 2) return ""; // 2 bytes or less is just null terminator in UTF-16
std::vector<unsigned short> buf(len / 2);
FPDF_GetMetaText(doc_, key, buf.data(), len);
return utf16le_to_utf8(reinterpret_cast<const char16_t*>(buf.data()), buf.size());
};
meta.title = fetchMeta("Title");
meta.author = fetchMeta("Author");
meta.creator = fetchMeta("Creator");
meta.producer = fetchMeta("Producer");
meta.creationDate = fetchMeta("CreationDate");
meta.modificationDate = fetchMeta("ModDate");
#endif
return meta;
}
std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) {
return std::unexpected(EngineError::Unknown);
}
if (pageIndex < 0 || pageIndex >= pageCount()) {
return std::unexpected(EngineError::PageOutOfBounds);
}
{
std::lock_guard<std::mutex> lock(pageCacheMutex_);
auto it = pageCache_.find(pageIndex);
if (it != pageCache_.end()) {
return it->second;
}
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
return std::unexpected(EngineError::Unknown);
}
auto pageObj = std::make_shared<PdfiumPage>(page, pageIndex);
{
std::lock_guard<std::mutex> lock(pageCacheMutex_);
pageCache_[pageIndex] = pageObj;
}
return pageObj;
#else
(void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& editsJson) {
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) {
return std::unexpected(EngineError::Unknown);
}
try {
auto root = nlohmann::json::parse(editsJson);
if (!root.contains("version") || root["version"] != "1.0") {
spdlog::error("Invalid edits JSON: missing or unsupported version (expected '1.0')");
return std::unexpected(EngineError::InvalidFormat);
}
if (!root.contains("operations") || !root["operations"].is_array()) {
spdlog::error("Invalid edits JSON: missing 'operations' array");
return std::unexpected(EngineError::InvalidFormat);
}
for (const auto& op : root["operations"]) {
std::string type = op.value("type", "");
int pageIndex = op.value("pageIndex", -1);
if (pageIndex < 0 || pageIndex >= pageCount()) {
spdlog::error("Page index {} out of bounds (total pages: {})", pageIndex, pageCount());
return std::unexpected(EngineError::PageOutOfBounds);
}
if (type == "text_overlay" || type == "add_text") {
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("text_overlay/add_text operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
std::string text = data.value("text", "");
double x = data.value("x", 0.0);
double y = data.value("y", 0.0);
double fontSize = data.value("fontSize", 12.0);
double width = data.value("width", 200.0);
double height = data.value("height", fontSize * 1.5);
std::string fontFamily = data.value("fontFamily", "Helvetica");
std::string color = data.value("color", "#000000");
if (text.empty()) {
continue;
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for editing", pageIndex);
return std::unexpected(EngineError::Unknown);
}
// 1. Cover old text with solid white rectangle
FPDF_PAGEOBJECT rectObj = FPDFPageObj_CreateNewRect(
static_cast<float>(x),
static_cast<float>(y),
static_cast<float>(width),
static_cast<float>(height)
);
if (!rectObj) {
spdlog::error("Failed to create background white rectangle object");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDFPageObj_SetFillColor(rectObj, 255, 255, 255, 255);
FPDFPath_SetDrawMode(rectObj, FPDF_FILLMODE_WINDING, 0);
FPDFPage_InsertObject(page, rectObj);
// 2. Load standard font
std::string standardFontName = "Helvetica";
if (fontFamily == "Times New Roman" || fontFamily == "Times-Roman") {
standardFontName = "Times-Roman";
} else if (fontFamily == "Courier New" || fontFamily == "Courier") {
standardFontName = "Courier";
} else if (fontFamily == "Helvetica-Bold") {
standardFontName = "Helvetica-Bold";
} else if (fontFamily == "Helvetica-Oblique") {
standardFontName = "Helvetica-Oblique";
} else if (fontFamily == "Helvetica-BoldOblique") {
standardFontName = "Helvetica-BoldOblique";
} else if (fontFamily == "Times-Bold") {
standardFontName = "Times-Bold";
} else if (fontFamily == "Times-Italic") {
standardFontName = "Times-Italic";
} else if (fontFamily == "Times-BoldItalic") {
standardFontName = "Times-BoldItalic";
} else if (fontFamily == "Courier-Bold") {
standardFontName = "Courier-Bold";
} else if (fontFamily == "Courier-Oblique") {
standardFontName = "Courier-Oblique";
} else if (fontFamily == "Courier-BoldOblique") {
standardFontName = "Courier-BoldOblique";
}
FPDF_FONT font = FPDFText_LoadStandardFont(doc_, standardFontName.c_str());
if (!font) {
spdlog::warn("Failed to load standard font {}, falling back to Helvetica", standardFontName);
font = FPDFText_LoadStandardFont(doc_, "Helvetica");
}
// 3. Create text page object
FPDF_PAGEOBJECT textObj = FPDFPageObj_CreateTextObj(doc_, font, static_cast<float>(fontSize));
if (!textObj) {
spdlog::error("Failed to create text object");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
// Set text color
unsigned int r = 0, g = 0, b = 0;
parseHexColor(color, r, g, b);
FPDFPageObj_SetFillColor(textObj, r, g, b, 255);
// Set text contents
auto utf16 = utf8_to_utf16le(text);
if (!FPDFText_SetText(textObj, reinterpret_cast<FPDF_WIDESTRING>(utf16.data()))) {
spdlog::error("Failed to set text object text");
FPDFPageObj_Destroy(textObj);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
// Position the text object at x, y
FPDFPageObj_Transform(textObj, 1.0, 0.0, 0.0, 1.0, x, y);
// Insert text object into page
FPDFPage_InsertObject(page, textObj);
// Regenerate page visual representation if needed
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after editing");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDF_ClosePage(page);
} else if (type == "redaction") {
spdlog::info("Parsed redaction edit operation (stub)");
} else if (type == "image_overlay") {
spdlog::info("Parsed image_overlay edit operation (stub)");
} else if (type == "highlight") {
spdlog::info("Parsed highlight edit operation (stub)");
} else if (type == "free_text") {
spdlog::info("Parsed free_text edit operation (stub)");
} else if (type == "comment") {
spdlog::info("Parsed comment edit operation (stub)");
} else if (type == "freehand") {
spdlog::info("Parsed freehand edit operation (stub)");
} else if (type == "page_rotation") {
spdlog::info("Parsed page_rotation edit operation (stub)");
} else {
spdlog::warn("Unsupported edit operation type: {}", type);
}
}
} catch (const nlohmann::json::parse_error& e) {
spdlog::error("JSON parse error in applyEdits: {}", e.what());
return std::unexpected(EngineError::InvalidFormat);
} catch (const std::exception& e) {
spdlog::error("Exception in applyEdits: {}", e.what());
return std::unexpected(EngineError::Unknown);
}
invalidateCaches();
return {};
#else
(void)editsJson;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::saveIncremental() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) {
return std::unexpected(EngineError::Unknown);
}
VectorWriter writer;
if (!FPDF_SaveWithVersion(doc_, &writer, FPDF_INCREMENTAL, 14)) {
return std::unexpected(EngineError::WriteFailed);
}
return writer.buffer;
#else
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::vector<FontInfo>, EngineError> PdfiumPage::getFonts() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
ensureTextPageLoaded();
if (!textPage_) {
return std::unexpected(EngineError::Unknown);
}
// Build real font data from the PDF font dictionary via page object walk.
// This provides accurate values for isEmbedded, subtype, flags, ascent,
// descent, and hasUnicodeMapping — replacing name-based heuristics.
auto fontPdfDataMap = buildFontPdfDataMap(page_, textPage_);
std::vector<FontInfo> pageFonts;
int charCount = FPDFText_CountChars(textPage_);
// Security Guard: prevent excessive traversal on extremely corrupted text pages
if (charCount < 0 || charCount > 1000000) {
spdlog::error("Invalid or excessive character count in page ({}): aborting font extraction", charCount);
return pageFonts;
}
auto getFontNameForChar = [this](int charIndex, int& flagsOut) -> std::string {
int flags = 0;
unsigned long len = FPDFText_GetFontInfo(textPage_, charIndex, nullptr, 0, &flags);
if (len > 0) {
std::vector<char> buf(len);
if (FPDFText_GetFontInfo(textPage_, charIndex, buf.data(), len, &flags) > 0) {
flagsOut = flags;
return std::string(buf.data());
}
}
return "";
};
// Detect vertical writing mode per font by analyzing character origin positions.
// We filter out large spatial jumps (e.g. line breaks or paragraph changes)
// by comparing the coordinate delta to the character's font size.
struct FontPositionStats {
int horizontalSteps = 0;
int verticalSteps = 0;
double totalFilteredDeltaX = 0.0;
double totalFilteredDeltaY = 0.0;
double prevX = 0.0, prevY = 0.0;
bool hasPrev = false;
};
std::unordered_map<std::string, FontPositionStats> fontStats;
// First pass: collect font names, flags, and position statistics
struct CharInfo {
std::string fontName;
int flags;
};
std::vector<CharInfo> charInfos(charCount);
for (int i = 0; i < charCount; ++i) {
int flags = 0;
std::string fontName = getFontNameForChar(i, flags);
charInfos[i] = {fontName, flags};
if (!fontName.empty()) {
double ox = 0.0, oy = 0.0;
if (FPDFText_GetCharOrigin(textPage_, i, &ox, &oy)) {
auto& stats = fontStats[fontName];
if (stats.hasPrev) {
double dx = std::abs(ox - stats.prevX);
double dy = std::abs(oy - stats.prevY);
double fontSize = FPDFText_GetFontSize(textPage_, i);
// Filter out non-consecutive jumps (e.g., line or paragraph breaks).
// We allow a generously large multiplier (3.0) for wide tracking,
// but enforce a minimum of 30.0 units for very small text.
double maxJump = (std::max)(fontSize * 3.0, 30.0);
if (dx < maxJump && dy < maxJump) {
stats.totalFilteredDeltaX += dx;
stats.totalFilteredDeltaY += dy;
if (dy > dx * 1.5) {
stats.verticalSteps++;
} else if (dx > dy * 1.5) {
stats.horizontalSteps++;
}
}
}
stats.prevX = ox;
stats.prevY = oy;
stats.hasPrev = true;
}
}
}
// Determine which fonts are vertical
std::unordered_map<std::string, bool> fontIsVertical;
for (const auto& [fname, stats] : fontStats) {
bool vertical = false;
// Primary heuristic: explicit step counts for consecutive characters
if (stats.verticalSteps > 0 || stats.horizontalSteps > 0) {
vertical = stats.verticalSteps > stats.horizontalSteps;
} else {
// Fallback: if no clear steps were isolated, rely on filtered totals
vertical = (stats.totalFilteredDeltaY > 2.0 * stats.totalFilteredDeltaX) &&
(stats.totalFilteredDeltaY > 0.5);
}
fontIsVertical[fname] = vertical;
}
// Second pass: build FontInfo list
for (int i = 0; i < charCount; ++i) {
const std::string& fontName = charInfos[i].fontName;
if (fontName.empty()) {
continue;
}
// Deduplicate locally by fontName
auto it = std::find_if(pageFonts.begin(), pageFonts.end(), [&](const FontInfo& f) {
return f.fontName == fontName;
});
if (it != pageFonts.end()) {
continue;
}
FontInfo f;
f.fontName = fontName;
f.flags = static_cast<uint32_t>(charInfos[i].flags);
// Phase A: heuristic baseline — fills encoding, cmapName, cidSystemInfo,
// normalizedFamily, subset tag, and initial values for all other fields.
deduceFontMetadata(f);
// Phase B: override heuristic fields with real PDF dictionary values
// wherever buildFontPdfDataMap() was able to extract them.
auto pdfDataIt = fontPdfDataMap.find(fontName);
if (pdfDataIt != fontPdfDataMap.end()) {
const FontPdfData& pd = pdfDataIt->second;
auto lname = fontName;
std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
// --- Embedding status (replaces subset-tag heuristic) ---
f.isEmbedded = pd.isEmbedded;
// --- Font descriptor flags (real /Flags value) ---
f.flags = static_cast<uint32_t>(pd.flags);
// --- Descriptor metrics (real ascent/descent) ---
if (pd.ascent != 0.0) f.ascent = pd.ascent;
if (pd.descent != 0.0) f.descent = pd.descent;
// CapHeight is not exposed by the PDFium public API.
// Use the exact specification values for the standard 14 fonts;
// for everything else, estimate from real ascent (71% is the
// empirical Latin cap-height ratio across common typefaces).
if (f.ascent > 0.0) {
if (lname.find("times") != std::string::npos) f.capHeight = 662.0;
else if (lname.find("courier") != std::string::npos) f.capHeight = 562.0;
else if (lname.find("symbol") != std::string::npos) f.capHeight = 673.0;
else if (lname.find("helvetica") != std::string::npos) f.capHeight = 728.0;
else f.capHeight = f.ascent * 0.71;
}
// --- Unicode mapping (replaces always-true heuristic) ---
if (lname.find("symbol") != std::string::npos) {
f.hasToUnicode = false;
} else {
f.hasToUnicode = pd.hasUnicodeMapping;
}
// --- sourceType derived from real embedding status ---
if (f.isEmbedded) {
// Font stream is present in the PDF — always report as Embedded.
f.sourceType = "Embedded";
f.substitutedFrom = "";
f.substitutedTo = "";
}
// If not embedded, keep the substitution info already set by
// deduceFontMetadata() (SystemFallback or Substituted).
// --- Recalculate stable identifier with corrected data ---
// fontName already includes the subset prefix (e.g. "ABCDEF+Arial");
// using it directly avoids the duplicate-prefix bug.
if (f.isSubset && !f.subsetTag.empty()) {
f.internalFontId = f.fontName;
} else {
f.internalFontId = f.fontName + "_" + f.type + "_" + std::to_string(f.flags);
}
spdlog::debug(
"Font '{}': isEmbedded={} type='{}' ascent={:.1f} descent={:.1f} "
"capHeight={:.1f} hasToUnicode={} flags={}",
fontName, f.isEmbedded, f.type, f.ascent, f.descent,
f.capHeight, f.hasToUnicode, f.flags);
}
// Override isVertical with position-based detection if not already set by name heuristic
if (!f.isVertical) {
auto vit = fontIsVertical.find(fontName);
if (vit != fontIsVertical.end() && vit->second) {
f.isVertical = true;
// Update encoding to reflect vertical writing mode
if (f.encoding == "WinAnsiEncoding" || f.encoding == "Identity-H") {
f.encoding = "Identity-V";
f.cmapName = "Identity-V";
}
spdlog::info("Vertical writing mode detected for font '{}' via character position analysis", fontName);
}
}
pageFonts.push_back(f);
}
// Deterministic Sorting: normalizedFamily -> fontName -> encoding -> type
std::sort(pageFonts.begin(), pageFonts.end(), [](const FontInfo& a, const FontInfo& b) {
if (a.normalizedFamily != b.normalizedFamily) {
return a.normalizedFamily < b.normalizedFamily;
}
if (a.fontName != b.fontName) {
return a.fontName < b.fontName;
}
if (a.encoding != b.encoding) {
return a.encoding < b.encoding;
}
return a.type < b.type;
});
return pageFonts;
#else
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int startPage, int endPage) const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) {
return std::unexpected(EngineError::Unknown);
}
std::lock_guard<std::mutex> lock(fontsMutex_);
int total = pageCount();
if (startPage < 0) startPage = 0;
if (endPage < 0 || endPage >= total) endPage = total - 1;
if (startPage > endPage) {
return std::vector<FontInfo>();
}
// Return full document-level cache if available and full range is requested
if (startPage == 0 && endPage == total - 1 && hasCachedFonts_) {
return cachedFonts_;
}
std::vector<FontInfo> aggregated;
for (int i = startPage; i <= endPage; ++i) {
auto pageRes = const_cast<PdfiumDocument*>(this)->getPage(i);
if (!pageRes) {
spdlog::error("Failed to load page index {} for font diagnostics", i);
continue;
}
auto pageFontsRes = pageRes.value()->getFonts();
if (pageFontsRes) {
for (const auto& f : *pageFontsRes) {
auto it = std::find_if(aggregated.begin(), aggregated.end(), [&](const FontInfo& existing) {
return existing.fontName == f.fontName;
});
if (it == aggregated.end()) {
aggregated.push_back(f);
}
}
}
}
// Sort the aggregated list deterministically
std::sort(aggregated.begin(), aggregated.end(), [](const FontInfo& a, const FontInfo& b) {
if (a.normalizedFamily != b.normalizedFamily) {
return a.normalizedFamily < b.normalizedFamily;
}
if (a.fontName != b.fontName) {
return a.fontName < b.fontName;
}
if (a.encoding != b.encoding) {
return a.encoding < b.encoding;
}
return a.type < b.type;
});
// Cache if full scan was requested
if (startPage == 0 && endPage == total - 1) {
cachedFonts_ = aggregated;
hasCachedFonts_ = true;
}
return aggregated;
#else
(void)startPage;
(void)endPage;
return std::unexpected(EngineError::Unknown);
#endif
}
std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(const std::string& internalFontId) const {
#ifdef PDFENGINE_WITH_PDFIUM
ensure_pdfium_initialized();
if (!doc_) {
return std::unexpected(EngineError::Unknown);
}
int numPages = FPDF_GetPageCount(doc_);
for (int i = 0; i < numPages; ++i) {
FPDF_PAGE page = FPDF_LoadPage(doc_, i);
if (!page) continue;
int objectCount = FPDFPage_CountObjects(page);
for (int j = 0; j < objectCount; ++j) {
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, j);
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
FPDF_FONT font = FPDFTextObj_GetFont(obj);
if (!font) continue;
unsigned long nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
if (nameLen > 0) {
std::vector<char> nameBuf(nameLen);
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) {
std::string fontName(nameBuf.data());
// internalFontId is constructed using fontName as the prefix
if (internalFontId.find(fontName) == 0) {
size_t buflen = FPDFFont_GetFontData(font, nullptr, 0);
if (buflen > 0) {
std::vector<uint8_t> buffer(buflen);
if (FPDFFont_GetFontData(font, buffer.data(), buflen) > 0) {
FPDF_ClosePage(page);
return buffer;
}
}
}
}
}
}
FPDF_ClosePage(page);
}
return std::unexpected(EngineError::FileNotFound);
#else
(void)internalFontId;
return std::unexpected(EngineError::Unknown);
#endif
}
void PdfiumDocument::invalidateCaches() {
{
std::lock_guard<std::mutex> lock(fontsMutex_);
cachedFonts_.clear();
hasCachedFonts_ = false;
}
{
std::lock_guard<std::mutex> lock(pageCacheMutex_);
pageCache_.clear();
}
spdlog::info("Document caches have been invalidated.");
}
}