diff --git a/engine/include/pdfengine/pdf_document.hpp b/engine/include/pdfengine/pdf_document.hpp index 5d53e3b..b624f8b 100644 --- a/engine/include/pdfengine/pdf_document.hpp +++ b/engine/include/pdfengine/pdf_document.hpp @@ -77,6 +77,48 @@ struct FontInfo { double capHeight = 0.0; // Font descriptor CapHeight metric }; +struct Glyph { + std::string text; + uint32_t unicode = 0; + + std::string fontName; + uint32_t flags = 0; + double fontSize = 0.0; + + double originX = 0.0; + double originY = 0.0; + double bboxX = 0.0, bboxY = 0.0, bboxW = 0.0, bboxH = 0.0; + double angle = 0.0; +}; + +struct TextRun { + std::string text; + std::string fontName; + uint32_t flags = 0; + double fontSize = 0.0; + std::string internalFontId; + std::vector glyphs; + double x = 0.0, y = 0.0, w = 0.0, h = 0.0; +}; + +struct TextLine { + std::vector runs; + double baselineY = 0.0; + double x = 0.0, y = 0.0, w = 0.0, h = 0.0; +}; + +struct Paragraph { + std::vector lines; + double x = 0.0, y = 0.0, w = 0.0, h = 0.0; +}; + +struct PageModel { + std::vector paragraphs; + double width = 0.0; + double height = 0.0; + int pageIndex = 0; +}; + class PdfPage { public: virtual ~PdfPage() = default; @@ -89,6 +131,8 @@ public: [[nodiscard]] virtual std::expected extractText() const = 0; [[nodiscard]] virtual std::expected, EngineError> extractTextWithBounds() const = 0; + + [[nodiscard]] virtual std::expected extractDocumentModel() const = 0; [[nodiscard]] virtual std::expected, EngineError> getFonts() const = 0; [[nodiscard]] virtual std::expected, EngineError> extractAnnotationsText() const = 0; @@ -99,6 +143,8 @@ public: [[nodiscard]] virtual Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0; }; +namespace fonts::pdf_fonts { class Font; } + class PdfDocument { public: virtual ~PdfDocument() = default; @@ -121,6 +167,9 @@ public: [[nodiscard]] virtual std::expected, EngineError> getFontData(const std::string& internalFontId) const = 0; + [[nodiscard]] virtual std::expected, std::string> + getResolvedFont(const FontInfo& fontInfo) = 0; + virtual std::expected applyEdits(const std::string& editsJson) = 0; [[nodiscard]] virtual std::expected, EngineError> diff --git a/engine/src/fonts/face/font_face.cpp b/engine/src/fonts/face/font_face.cpp index 7245894..cd81848 100644 --- a/engine/src/fonts/face/font_face.cpp +++ b/engine/src/fonts/face/font_face.cpp @@ -122,6 +122,8 @@ std::optional FontFace::renderGlyph(unsigned int glyphIndex, unsign return std::nullopt; } + std::lock_guard lock(*mutex_); + // Set font size in pixels. if (FT_Set_Pixel_Sizes(face_, 0, fontSize)) { return std::nullopt; diff --git a/engine/src/fonts/loader/font_resolver.cpp b/engine/src/fonts/loader/font_resolver.cpp index a103b67..a81e683 100644 --- a/engine/src/fonts/loader/font_resolver.cpp +++ b/engine/src/fonts/loader/font_resolver.cpp @@ -21,24 +21,28 @@ std::expected, std::string> FontResolver::resol const auto& bytes = dataRes.value(); + // Build FontDescriptor from FontInfo + auto descriptor = std::make_unique(); + descriptor->setFontName(fontInfo.fontName); + descriptor->setFlags(fontInfo.flags); + descriptor->setAscent(fontInfo.ascent); + descriptor->setDescent(fontInfo.descent); + descriptor->setCapHeight(fontInfo.capHeight); + // Route to the appropriate FontLoader method based on FontInfo type - if (fontInfo.type == "TrueType" || fontInfo.type == "Type1") { - // Treat embedded Type1/TrueType uniformly through the TrueType loader for now - // since FontLoader::loadTrueTypeFromMemory delegates to FreeType which handles both. - // A more strictly compliant PDF parser would differentiate, but FreeType is unified. - auto font = pdf_fonts::FontLoader::loadTrueTypeFromMemory(fontInfo.normalizedFamily, bytes); - if (font) { - return font; - } + if (fontInfo.type == "TrueType") { + auto font = pdf_fonts::FontLoader::loadTrueTypeFromMemory(fontInfo.normalizedFamily, bytes, std::move(descriptor)); + if (font) return font; + } else if (fontInfo.type == "Type1") { + auto font = pdf_fonts::FontLoader::loadType1FromMemory(fontInfo.normalizedFamily, bytes, std::move(descriptor)); + if (font) return font; } else if (fontInfo.type == "CIDFontType0" || fontInfo.type == "CIDFontType2") { pdf_fonts::FontType subtype = (fontInfo.type == "CIDFontType0") ? pdf_fonts::FontType::CIDFontType0 : pdf_fonts::FontType::CIDFontType2; - auto font = pdf_fonts::FontLoader::loadCIDFontFromMemory(fontInfo.normalizedFamily, subtype, bytes); - if (font) { - return font; - } + auto font = pdf_fonts::FontLoader::loadCIDFontFromMemory(fontInfo.normalizedFamily, subtype, bytes, std::move(descriptor)); + if (font) return font; } return std::unexpected("Failed to parse extracted font data"); @@ -46,14 +50,22 @@ std::expected, std::string> FontResolver::resol // Handle System Fallback spdlog::info("Resolving system fallback font for: {}", fontInfo.fontName); + // Build FontDescriptor from FontInfo + auto descriptor = std::make_unique(); + descriptor->setFontName(fontInfo.fontName); + descriptor->setFlags(fontInfo.flags); + descriptor->setAscent(fontInfo.ascent); + descriptor->setDescent(fontInfo.descent); + descriptor->setCapHeight(fontInfo.capHeight); + if (fontInfo.type == "CIDFontType0" || fontInfo.type == "CIDFontType2") { pdf_fonts::FontType subtype = (fontInfo.type == "CIDFontType0") ? pdf_fonts::FontType::CIDFontType0 : pdf_fonts::FontType::CIDFontType2; - auto font = pdf_fonts::FontLoader::loadCIDFontSystemFallback(fontInfo.normalizedFamily, subtype); + auto font = pdf_fonts::FontLoader::loadCIDFontSystemFallback(fontInfo.normalizedFamily, subtype, std::move(descriptor)); if (font) return font; } else { - auto font = pdf_fonts::FontLoader::loadType1SystemFallback(fontInfo.normalizedFamily); + auto font = pdf_fonts::FontLoader::loadType1SystemFallback(fontInfo.normalizedFamily, std::move(descriptor)); if (font) return font; } diff --git a/engine/src/fonts/pdf_fonts/font.hpp b/engine/src/fonts/pdf_fonts/font.hpp index 4c3eb8e..9fdcc2d 100644 --- a/engine/src/fonts/pdf_fonts/font.hpp +++ b/engine/src/fonts/pdf_fonts/font.hpp @@ -19,6 +19,19 @@ enum class FontType { Type3 }; +enum class FontSource { + Embedded, + SystemFallback, + Substituted +}; + +struct FontMetrics { + double ascent = 0.0; + double descent = 0.0; + double lineGap = 0.0; + double capHeight = 0.0; +}; + class Font { public: virtual ~Font() = default; @@ -37,9 +50,62 @@ public: // Gets the font encoding (returns nullptr if none exists) virtual const Encoding* getEncoding() const = 0; - // Gets subsetting details (returns nullptr if font is not subsetted) virtual const FontSubset* getSubsetInfo() const = 0; + // Advanced Layout APIs + virtual FontSource getSourceType() const { return source_type_; } + virtual void setSourceType(FontSource source) { source_type_ = source; } + + virtual bool hasGlyph(uint32_t unicode) const { + if (!getFontFace().getFace()) return false; + return FT_Get_Char_Index(getFontFace().getFace(), unicode) != 0; + } + + virtual double getAdvanceWidth(uint32_t unicode, double fontSize) const { + if (!getFontFace().getFace()) return 0.0; + + std::lock_guard lock(getFontFace().getMutex()); + FT_Face rawFace = getFontFace().getFace(); + FT_Set_Pixel_Sizes(rawFace, 0, static_cast(fontSize)); + + FT_UInt glyphIndex = FT_Get_Char_Index(rawFace, unicode); + if (glyphIndex == 0) return 0.0; + + if (FT_Load_Glyph(rawFace, glyphIndex, FT_LOAD_DEFAULT) == 0) { + return static_cast(rawFace->glyph->advance.x) / 64.0; + } + return 0.0; + } + + virtual FontMetrics getMetrics(double fontSize) const { + if (metrics_cached_ && last_metrics_size_ == fontSize) { + return metrics_cache_; + } + + if (getFontFace().getFace()) { + std::lock_guard lock(getFontFace().getMutex()); + FT_Face rawFace = getFontFace().getFace(); + FT_Set_Pixel_Sizes(rawFace, 0, static_cast(fontSize)); + + // Convert from 26.6 to double + metrics_cache_.ascent = static_cast(rawFace->size->metrics.ascender) / 64.0; + metrics_cache_.descent = static_cast(rawFace->size->metrics.descender) / 64.0; + metrics_cache_.lineGap = static_cast(rawFace->size->metrics.height - rawFace->size->metrics.ascender + rawFace->size->metrics.descender) / 64.0; + + // Heuristic for capHeight: height of 'H' + FT_UInt hIndex = FT_Get_Char_Index(rawFace, 'H'); + if (hIndex > 0 && FT_Load_Glyph(rawFace, hIndex, FT_LOAD_DEFAULT) == 0) { + metrics_cache_.capHeight = static_cast(rawFace->glyph->metrics.horiBearingY) / 64.0; + } else { + metrics_cache_.capHeight = metrics_cache_.ascent * 0.7; // Fallback + } + + metrics_cached_ = true; + last_metrics_size_ = fontSize; + } + return metrics_cache_; + } + // Translates a raw character code to a Unicode codepoint virtual uint32_t decodeToUnicode(uint32_t charCode) const = 0; @@ -119,6 +185,11 @@ protected: uint32_t last_char_ = 0; std::vector widths_; bool has_widths_ = false; + + FontSource source_type_ = FontSource::Embedded; + mutable FontMetrics metrics_cache_; + mutable bool metrics_cached_ = false; + mutable double last_metrics_size_ = 0.0; bool is_vertical_ = false; uint32_t first_vertical_char_ = 0; diff --git a/engine/src/fonts/pdf_fonts/font_fallback.cpp b/engine/src/fonts/pdf_fonts/font_fallback.cpp index 9c4364f..68e9cd9 100644 --- a/engine/src/fonts/pdf_fonts/font_fallback.cpp +++ b/engine/src/fonts/pdf_fonts/font_fallback.cpp @@ -121,6 +121,8 @@ std::string FontFallback::getFallbackFontPath(const std::string& fontName, bool stylePattern += "-italic"; } + std::lock_guard lock(rules_mutex_); + for (const auto& rule : custom_rules_) { if (stylePattern.find(rule.pattern) != std::string::npos || lowerName.find(rule.pattern) != std::string::npos) { for (const auto& path : rule.preferredPaths) { @@ -154,17 +156,37 @@ std::string FontFallback::getFallbackFontPath(const std::string& fontName, bool } } return "C:\\Windows\\Fonts\\arial.ttf"; + #elif defined(__APPLE__) + std::vector lastResort = { + "/Library/Fonts/Arial.ttf", + "/System/Library/Fonts/Helvetica.ttc", + "/System/Library/Fonts/Supplemental/Arial.ttf" + }; + for (const auto& path : lastResort) { + if (std::filesystem::exists(path)) return path; + } + return "/System/Library/Fonts/Helvetica.ttc"; #else - return ""; + std::vector lastResort = { + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/freefont/FreeSans.ttf" + }; + for (const auto& path : lastResort) { + if (std::filesystem::exists(path)) return path; + } + return "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; #endif } void FontFallback::registerFallback(const std::string& pattern, const std::string& systemFontPath) { std::string lowerPattern = toLower(pattern); + std::lock_guard lock(rules_mutex_); custom_rules_.insert(custom_rules_.begin(), {lowerPattern, {systemFontPath}}); } void FontFallback::resetToDefaults() { + std::lock_guard lock(rules_mutex_); custom_rules_.clear(); } diff --git a/engine/src/fonts/pdf_fonts/font_fallback.hpp b/engine/src/fonts/pdf_fonts/font_fallback.hpp index 4162438..1692ea4 100644 --- a/engine/src/fonts/pdf_fonts/font_fallback.hpp +++ b/engine/src/fonts/pdf_fonts/font_fallback.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace pdfengine::fonts::pdf_fonts { @@ -32,6 +33,7 @@ private: }; std::vector default_rules_; std::vector custom_rules_; + mutable std::mutex rules_mutex_; }; } // namespace pdfengine::fonts::pdf_fonts diff --git a/engine/src/fonts/pdf_fonts/font_subset.cpp b/engine/src/fonts/pdf_fonts/font_subset.cpp index 8adfe58..c2ab076 100644 --- a/engine/src/fonts/pdf_fonts/font_subset.cpp +++ b/engine/src/fonts/pdf_fonts/font_subset.cpp @@ -1,9 +1,85 @@ #include "fonts/pdf_fonts/font_subset.hpp" #include #include +#include +#include FT_FREETYPE_H +#include +#include namespace pdfengine::fonts::pdf_fonts { +void FontSubset::populateFromFace(FontSubset& subset, void* ftFace) { + if (!ftFace) return; + FT_Face face = static_cast(ftFace); + + FT_UInt gindex; + FT_ULong charcode = FT_Get_First_Char(face, &gindex); + while (gindex != 0) { + // In embedded subsets, the TrueType cmap typically maps the + // Original GID or CID (charcode) to the new Subset GID (gindex). + subset.addGlyphMapping(gindex, static_cast(charcode)); + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } +} + +std::vector FontSubset::buildSubset(const std::vector& originalStream, const std::vector& glyphIdsToKeep) { + std::vector result; + if (originalStream.empty() || glyphIdsToKeep.empty()) { + return result; + } + + hb_blob_t* blob = hb_blob_create( + reinterpret_cast(originalStream.data()), + static_cast(originalStream.size()), + HB_MEMORY_MODE_READONLY, + nullptr, + nullptr + ); + + hb_face_t* face = hb_face_create(blob, 0); + hb_blob_destroy(blob); + + if (!face) { + return result; + } + + hb_subset_input_t* input = hb_subset_input_create_or_fail(); + if (!input) { + hb_face_destroy(face); + return result; + } + + hb_set_t* glyph_set = hb_subset_input_glyph_set(input); + for (uint32_t gid : glyphIdsToKeep) { + hb_set_add(glyph_set, gid); + } + + // Always keep GID 0 (the .notdef glyph) + hb_set_add(glyph_set, 0); + + // Retain layout tables for complex text shaping + hb_subset_input_set_flags(input, HB_SUBSET_FLAGS_RETAIN_GIDS); + + hb_face_t* subset_face = hb_subset_or_fail(face, input); + hb_subset_input_destroy(input); + hb_face_destroy(face); + + if (subset_face) { + hb_blob_t* result_blob = hb_face_reference_blob(subset_face); + if (result_blob) { + unsigned int length = 0; + const char* data = hb_blob_get_data(result_blob, &length); + if (data && length > 0) { + result.assign(data, data + length); + } + hb_blob_destroy(result_blob); + } + hb_face_destroy(subset_face); + } + + return result; +} + bool FontSubset::hasSubsetPrefix(const std::string& fontName) { if (fontName.length() < 8) { return false; diff --git a/engine/src/fonts/pdf_fonts/font_subset.hpp b/engine/src/fonts/pdf_fonts/font_subset.hpp index b4a9443..7ab65dc 100644 --- a/engine/src/fonts/pdf_fonts/font_subset.hpp +++ b/engine/src/fonts/pdf_fonts/font_subset.hpp @@ -14,6 +14,12 @@ public: static std::string getSubsetPrefix(const std::string& fontName); + // Iterates the FT_Face cmap to populate the mapping dictionary + static void populateFromFace(FontSubset& subset, void* ftFace); + + // Rebuilds the TTF/CID stream keeping only the specified GIDs using HarfBuzz + static std::vector buildSubset(const std::vector& originalStream, const std::vector& glyphIdsToKeep); + explicit FontSubset(const std::string& fontName); ~FontSubset() = default; diff --git a/engine/src/fonts/pdf_fonts/types/cid_font.cpp b/engine/src/fonts/pdf_fonts/types/cid_font.cpp index ab1bb6a..f9faebe 100644 --- a/engine/src/fonts/pdf_fonts/types/cid_font.cpp +++ b/engine/src/fonts/pdf_fonts/types/cid_font.cpp @@ -54,7 +54,13 @@ CIDFont::CIDFont( CIDFont::~CIDFont() = default; bool CIDFont::loadFromStream(const std::vector& streamData) { - return font_face_.loadFromMemory(streamData); + if (!font_face_.loadFromMemory(streamData)) { + return false; + } + if (subset_info_) { + FontSubset::populateFromFace(*subset_info_, font_face_.getFace()); + } + return true; } bool CIDFont::loadFromFile(const std::string& filePath) { @@ -178,16 +184,14 @@ uint32_t CIDFont::decodeToUnicode(uint32_t charCode) const { gid = subset_info_->mapSubsetToOriginal(gid); } - FT_Face face = font_face_.getFace(); - if (face) { - FT_UInt gindex; - FT_ULong charcode = FT_Get_First_Char(face, &gindex); - while (gindex != 0) { - if (gindex == gid) { - return static_cast(charcode); - } - charcode = FT_Get_Next_Char(face, charcode, &gindex); - } + if (!is_gid_to_unicode_map_built_) { + buildGidToUnicodeMap(); + } + + std::lock_guard lock(gid_to_unicode_mutex_); + auto it = gid_to_unicode_map_.find(gid); + if (it != gid_to_unicode_map_.end()) { + return it->second; } return charCode; @@ -204,4 +208,22 @@ std::string CIDFont::decodeStringToUnicode(const std::vector& charCode return result; } +void CIDFont::buildGidToUnicodeMap() const { + std::lock_guard lock(gid_to_unicode_mutex_); + if (is_gid_to_unicode_map_built_) { + return; + } + + FT_Face face = font_face_.getFace(); + if (face) { + FT_UInt gindex; + FT_ULong charcode = FT_Get_First_Char(face, &gindex); + while (gindex != 0) { + gid_to_unicode_map_[gindex] = static_cast(charcode); + charcode = FT_Get_Next_Char(face, charcode, &gindex); + } + } + is_gid_to_unicode_map_built_ = true; +} + } // namespace pdfengine::fonts::pdf_fonts diff --git a/engine/src/fonts/pdf_fonts/types/cid_font.hpp b/engine/src/fonts/pdf_fonts/types/cid_font.hpp index 1135a8a..fe3a93a 100644 --- a/engine/src/fonts/pdf_fonts/types/cid_font.hpp +++ b/engine/src/fonts/pdf_fonts/types/cid_font.hpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace pdfengine::fonts::pdf_fonts { class FontSubset; @@ -60,6 +61,11 @@ private: bool is_identity_map_ = true; std::unordered_map cid_to_gid_map_; std::unique_ptr subset_info_; + mutable bool is_gid_to_unicode_map_built_ = false; + mutable std::mutex gid_to_unicode_mutex_; + mutable std::unordered_map gid_to_unicode_map_; + + void buildGidToUnicodeMap() const; }; } // namespace pdfengine::fonts::pdf_fonts diff --git a/engine/src/fonts/pdf_fonts/types/truetype_font.cpp b/engine/src/fonts/pdf_fonts/types/truetype_font.cpp index a52ab24..9e4cdc3 100644 --- a/engine/src/fonts/pdf_fonts/types/truetype_font.cpp +++ b/engine/src/fonts/pdf_fonts/types/truetype_font.cpp @@ -50,7 +50,13 @@ TrueTypeFont::TrueTypeFont( TrueTypeFont::~TrueTypeFont() = default; bool TrueTypeFont::loadFromStream(const std::vector& streamData) { - return font_face_.loadFromMemory(streamData); + if (!font_face_.loadFromMemory(streamData)) { + return false; + } + if (subset_info_) { + FontSubset::populateFromFace(*subset_info_, font_face_.getFace()); + } + return true; } std::string TrueTypeFont::getBaseFont() const { @@ -94,18 +100,8 @@ uint32_t TrueTypeFont::decodeToUnicode(uint32_t charCode) const { } if (subset_info_) { - uint32_t subsetGid = subset_info_->mapSubsetToOriginal(charCode); - - FT_Face face = font_face_.getFace(); - if (face) { - FT_UInt gindex; - FT_ULong charcode = FT_Get_First_Char(face, &gindex); - while (gindex != 0) { - if (gindex == subsetGid) { - return static_cast(charcode); - } - charcode = FT_Get_Next_Char(face, charcode, &gindex); - } + if (subset_info_->hasGlyphMapping(charCode)) { + return subset_info_->mapSubsetToOriginal(charCode); } } diff --git a/engine/src/fonts/pdf_fonts/types/type1_font.cpp b/engine/src/fonts/pdf_fonts/types/type1_font.cpp index 2d96dba..02c4144 100644 --- a/engine/src/fonts/pdf_fonts/types/type1_font.cpp +++ b/engine/src/fonts/pdf_fonts/types/type1_font.cpp @@ -50,7 +50,13 @@ Type1Font::Type1Font( Type1Font::~Type1Font() = default; bool Type1Font::loadFromStream(const std::vector& streamData) { - return font_face_.loadFromMemory(streamData); + if (!font_face_.loadFromMemory(streamData)) { + return false; + } + if (subset_info_) { + FontSubset::populateFromFace(*subset_info_, font_face_.getFace()); + } + return true; } bool Type1Font::loadFromFile(const std::string& filePath) { @@ -98,18 +104,8 @@ uint32_t Type1Font::decodeToUnicode(uint32_t charCode) const { } if (subset_info_) { - uint32_t subsetGid = subset_info_->mapSubsetToOriginal(charCode); - - FT_Face face = font_face_.getFace(); - if (face) { - FT_UInt gindex; - FT_ULong charcode = FT_Get_First_Char(face, &gindex); - while (gindex != 0) { - if (gindex == subsetGid) { - return static_cast(charcode); - } - charcode = FT_Get_Next_Char(face, charcode, &gindex); - } + if (subset_info_->hasGlyphMapping(charCode)) { + return subset_info_->mapSubsetToOriginal(charCode); } } diff --git a/engine/src/fonts/shaping/hb_shaper.cpp b/engine/src/fonts/shaping/hb_shaper.cpp index fb282b1..d1da0c7 100644 --- a/engine/src/fonts/shaping/hb_shaper.cpp +++ b/engine/src/fonts/shaping/hb_shaper.cpp @@ -91,6 +91,7 @@ std::vector HbShaper::shapeRun( sg.advanceY = static_cast(glyphPositions[i].y_advance) / 64.0; sg.offsetX = static_cast(glyphPositions[i].x_offset) / 64.0; sg.offsetY = static_cast(glyphPositions[i].y_offset) / 64.0; + sg.clusterIndex = glyphInfos[i].cluster; result.push_back(sg); } } diff --git a/engine/src/fonts/shaping/hb_shaper.hpp b/engine/src/fonts/shaping/hb_shaper.hpp index baec2eb..af39f0b 100644 --- a/engine/src/fonts/shaping/hb_shaper.hpp +++ b/engine/src/fonts/shaping/hb_shaper.hpp @@ -13,6 +13,7 @@ struct ShapedGlyph { double advanceY; double offsetX; double offsetY; + uint32_t clusterIndex; }; class HbShaper { diff --git a/engine/src/parser/pdfium_document.cpp b/engine/src/parser/pdfium_document.cpp index 72024bd..2ca3b3c 100644 --- a/engine/src/parser/pdfium_document.cpp +++ b/engine/src/parser/pdfium_document.cpp @@ -10,6 +10,9 @@ #include "parser/pdfium_loader.hpp" #endif +#include "fonts/loader/font_resolver.hpp" +#include "fonts/pdf_fonts/font.hpp" + #include #include #include @@ -806,6 +809,229 @@ std::expected, EngineError> PdfiumPage::extractTextWith #endif } +std::expected 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::vector documentGlyphs; + documentGlyphs.reserve(charCount); + + // Pass 1: Extract all glyphs + for (int i = 0; i < charCount; ++i) { + unsigned int codeUnit = FPDFText_GetUnicode(textPage_, i); + unsigned int cp = codeUnit; + + // Handle surrogate pairs + 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; // Skip low surrogate + continue; + } + + Glyph g; + g.text = std::move(utf8_char); + g.unicode = cp; + + // Geometry + 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); + + // Style + g.fontSize = FPDFText_GetFontSize(textPage_, i); + int flags = 0; + unsigned long len = FPDFText_GetFontInfo(textPage_, i, nullptr, 0, &flags); + if (len > 0) { + std::vector buf(len); + if (FPDFText_GetFontInfo(textPage_, i, buf.data(), len, &flags) > 0) { + g.fontName = std::string(buf.data()); + } + } + g.flags = flags; + + documentGlyphs.push_back(g); + + if (cp > 0xFFFF) ++i; // Skip low surrogate + } + + // Phase 5E: Line Builder + 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; // Top to bottom + } + return a.originX < b.originX; // Left to right + }); + + std::vector 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) { + return a.originX < b.originX; + }); + + std::vector 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 medianGap = 0.0; + if (!gaps.empty()) { + std::sort(gaps.begin(), gaps.end()); + medianGap = gaps[gaps.size() / 2]; + } + + // Phase 5F: Run Builder + TextRun currentRun; + if (!line.glyphs.empty()) { + const Glyph* firstG = &line.glyphs[0]; + currentRun.fontName = firstG->fontName; + currentRun.fontSize = firstG->fontSize; + currentRun.flags = firstG->flags; + currentRun.glyphs.push_back(*firstG); + + 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 spaceThreshold = (std::max)(currG.fontSize * 0.25, medianGap * 2.0); + + 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) { + line.runs.push_back(std::move(currentRun)); + currentRun = TextRun(); + currentRun.fontName = currG.fontName; + currentRun.fontSize = currG.fontSize; + currentRun.flags = currG.flags; + } + currentRun.glyphs.push_back(spaceGlyph); + } else if (breakRun) { + line.runs.push_back(std::move(currentRun)); + currentRun = TextRun(); + currentRun.fontName = currG.fontName; + currentRun.fontSize = currG.fontSize; + currentRun.flags = currG.flags; + } + + currentRun.glyphs.push_back(currG); + } + if (!currentRun.glyphs.empty()) { + line.runs.push_back(std::move(currentRun)); + } + } + } + + // Phase 5G: Paragraph Builder + std::vector paragraphs; + 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 vGap = std::abs(prevY - currY); + + if (vGap > fontSize * 1.5) { + 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)); + } + } + + model.paragraphs = std::move(paragraphs); + + return model; +#else + return std::unexpected(EngineError::Unknown); +#endif +} + std::expected, EngineError> PdfiumPage::extractAnnotationsText() const { #ifdef PDFENGINE_WITH_PDFIUM if (!page_) { @@ -1516,10 +1742,46 @@ std::expected, EngineError> PdfiumDocument::getFontData(con return std::unexpected(EngineError::Unknown); } + // Reconstruct the exact expected font name from the internalFontId. + // Non-subset format: {fontName}_{type}_{flags} + std::string expectedFontName = internalFontId; + size_t lastUnderscore = expectedFontName.rfind('_'); + if (lastUnderscore != std::string::npos && lastUnderscore > 0) { + size_t secondLastUnderscore = expectedFontName.rfind('_', lastUnderscore - 1); + if (secondLastUnderscore != std::string::npos) { + std::string typePart = expectedFontName.substr(secondLastUnderscore + 1, lastUnderscore - secondLastUnderscore - 1); + if (typePart == "TrueType" || typePart == "Type1" || typePart == "CIDFontType0" || typePart == "CIDFontType2") { + expectedFontName = expectedFontName.substr(0, secondLastUnderscore); + } + } + } + int numPages = FPDF_GetPageCount(doc_); - for (int i = 0; i < numPages; ++i) { + + // Resume scanning from where we left off + int startPage = 0; + { + std::lock_guard lock(fontsMutex_); + + // Return immediately if already cached + if (fontDataCache_.count(expectedFontName)) { + const auto& cachedBuf = fontDataCache_[expectedFontName]; + if (!cachedBuf.empty()) { + return cachedBuf; + } else { + return std::unexpected(EngineError::FileNotFound); + } + } + startPage = fontDataScannedPages_; + } + + for (int i = startPage; i < numPages; ++i) { FPDF_PAGE page = FPDF_LoadPage(doc_, i); - if (!page) continue; + if (!page) { + std::lock_guard lock(fontsMutex_); + fontDataScannedPages_ = i + 1; + continue; + } int objectCount = FPDFPage_CountObjects(page); for (int j = 0; j < objectCount; ++j) { @@ -1534,21 +1796,49 @@ std::expected, EngineError> PdfiumDocument::getFontData(con std::vector 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) { + + std::lock_guard lock(fontsMutex_); + + // Always cache every font we encounter to avoid rescanning + if (fontDataCache_.find(fontName) == fontDataCache_.end()) { size_t buflen = FPDFFont_GetFontData(font, nullptr, 0); if (buflen > 0) { std::vector buffer(buflen); if (FPDFFont_GetFontData(font, buffer.data(), buflen) > 0) { - FPDF_ClosePage(page); - return buffer; + fontDataCache_[fontName] = buffer; + } else { + fontDataCache_[fontName] = std::vector(); } + } else { + fontDataCache_[fontName] = std::vector(); + } + } + + // Use exact match instead of prefix match to avoid false positives + if (fontName == expectedFontName) { + const auto& cachedBuf = fontDataCache_[fontName]; + if (!cachedBuf.empty()) { + FPDF_ClosePage(page); + fontDataScannedPages_ = i; // can resume from the same page later + return cachedBuf; } } } } } FPDF_ClosePage(page); + + std::lock_guard lock(fontsMutex_); + fontDataScannedPages_ = i + 1; + } + + // We finished scanning all pages and still didn't find it (or extraction failed) + { + std::lock_guard lock(fontsMutex_); + // Mark as failed so we don't try to rescan for it + if (fontDataCache_.find(expectedFontName) == fontDataCache_.end()) { + fontDataCache_[expectedFontName] = std::vector(); + } } return std::unexpected(EngineError::FileNotFound); #else @@ -1557,6 +1847,35 @@ std::expected, EngineError> PdfiumDocument::getFontData(con #endif } +std::expected, std::string> PdfiumDocument::getResolvedFont(const FontInfo& fontInfo) { +#ifdef PDFENGINE_WITH_PDFIUM + std::lock_guard lock(resolvedFontsMutex_); + + // Check Cache + if (resolvedFontsCache_.count(fontInfo.internalFontId)) { + return resolvedFontsCache_[fontInfo.internalFontId]; + } + + if (!fontResolver_) { + // We pass a shared_ptr to 'this' + fontResolver_ = std::make_unique<::pdfengine::fonts::loader::FontResolver>(shared_from_this()); + } + + // Resolve font + auto result = fontResolver_->resolveFont(fontInfo); + if (!result) { + return std::unexpected(result.error()); + } + + // Cache and return + std::shared_ptr sharedFont(std::move(result.value())); + resolvedFontsCache_[fontInfo.internalFontId] = sharedFont; + return sharedFont; +#else + return std::unexpected(std::string("EngineError::Unknown")); +#endif +} + void PdfiumDocument::invalidateCaches() { { std::lock_guard lock(fontsMutex_); @@ -1567,6 +1886,10 @@ void PdfiumDocument::invalidateCaches() { std::lock_guard lock(pageCacheMutex_); pageCache_.clear(); } + { + std::lock_guard lock(resolvedFontsMutex_); + resolvedFontsCache_.clear(); + } spdlog::info("Document caches have been invalidated."); } diff --git a/engine/src/parser/pdfium_document.hpp b/engine/src/parser/pdfium_document.hpp index 68b020a..61eb5fd 100644 --- a/engine/src/parser/pdfium_document.hpp +++ b/engine/src/parser/pdfium_document.hpp @@ -12,6 +12,7 @@ #include #include #include +namespace pdfengine::fonts::loader { class FontResolver; } namespace pdfengine::parser { @@ -41,6 +42,7 @@ public: std::expected render(int dpi = 96) const override; std::expected extractText() const override; std::expected, EngineError> extractTextWithBounds() const override; + std::expected extractDocumentModel() const override; std::expected, EngineError> getFonts() const override; std::expected, EngineError> extractAnnotationsText() const override; @@ -61,7 +63,7 @@ private: void ensureTextPageLoaded() const; }; -class PdfiumDocument : public PdfDocument { +class PdfiumDocument : public PdfDocument, public std::enable_shared_from_this { public: explicit PdfiumDocument(NativeDocHandle docHandle); PdfiumDocument(NativeDocHandle docHandle, std::vector memoryBuffer); @@ -78,6 +80,8 @@ public: std::expected, EngineError> getPage(int pageIndex) override; std::expected, EngineError> getFonts(int startPage = 0, int endPage = -1) const override; std::expected, EngineError> getFontData(const std::string& internalFontId) const override; + std::expected, std::string> getResolvedFont(const FontInfo& fontInfo) override; + void invalidateCaches(); std::expected applyEdits(const std::string& editsJson) override; @@ -91,8 +95,17 @@ private: mutable bool hasCachedFonts_ = false; mutable std::mutex fontsMutex_; + // Cache to prevent O(N*M) full-document scans for font data extraction + mutable std::unordered_map> fontDataCache_; + mutable int fontDataScannedPages_ = 0; + mutable std::unordered_map> pageCache_; mutable std::mutex pageCacheMutex_; + + // Font Engine Bridge + std::unique_ptr fontResolver_; + std::unordered_map> resolvedFontsCache_; + std::mutex resolvedFontsMutex_; }; // Exposed for testing