This commit is contained in:
saqib mir
2026-06-08 11:23:27 +05:30
parent bfa8d0eddf
commit 34906a6110
16 changed files with 658 additions and 60 deletions
+49
View File
@@ -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<Glyph> glyphs;
double x = 0.0, y = 0.0, w = 0.0, h = 0.0;
};
struct TextLine {
std::vector<TextRun> runs;
double baselineY = 0.0;
double x = 0.0, y = 0.0, w = 0.0, h = 0.0;
};
struct Paragraph {
std::vector<TextLine> lines;
double x = 0.0, y = 0.0, w = 0.0, h = 0.0;
};
struct PageModel {
std::vector<Paragraph> 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<std::string, EngineError> extractText() const = 0;
[[nodiscard]] virtual std::expected<std::vector<GlyphBounds>, EngineError> extractTextWithBounds() const = 0;
[[nodiscard]] virtual std::expected<PageModel, EngineError> extractDocumentModel() const = 0;
[[nodiscard]] virtual std::expected<std::vector<FontInfo>, EngineError> getFonts() const = 0;
[[nodiscard]] virtual std::expected<std::vector<std::string>, 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<std::vector<uint8_t>, EngineError>
getFontData(const std::string& internalFontId) const = 0;
[[nodiscard]] virtual std::expected<std::shared_ptr<fonts::pdf_fonts::Font>, std::string>
getResolvedFont(const FontInfo& fontInfo) = 0;
virtual std::expected<void, EngineError> applyEdits(const std::string& editsJson) = 0;
[[nodiscard]] virtual std::expected<std::vector<uint8_t>, EngineError>
+2
View File
@@ -122,6 +122,8 @@ std::optional<GlyphBitmap> FontFace::renderGlyph(unsigned int glyphIndex, unsign
return std::nullopt;
}
std::lock_guard<std::mutex> lock(*mutex_);
// Set font size in pixels.
if (FT_Set_Pixel_Sizes(face_, 0, fontSize)) {
return std::nullopt;
+26 -14
View File
@@ -21,24 +21,28 @@ std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> FontResolver::resol
const auto& bytes = dataRes.value();
// Build FontDescriptor from FontInfo
auto descriptor = std::make_unique<pdf_fonts::FontDescriptor>();
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::unique_ptr<pdf_fonts::Font>, 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<pdf_fonts::FontDescriptor>();
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;
}
+72 -1
View File
@@ -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<std::mutex> lock(getFontFace().getMutex());
FT_Face rawFace = getFontFace().getFace();
FT_Set_Pixel_Sizes(rawFace, 0, static_cast<FT_UInt>(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<double>(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<std::mutex> lock(getFontFace().getMutex());
FT_Face rawFace = getFontFace().getFace();
FT_Set_Pixel_Sizes(rawFace, 0, static_cast<FT_UInt>(fontSize));
// Convert from 26.6 to double
metrics_cache_.ascent = static_cast<double>(rawFace->size->metrics.ascender) / 64.0;
metrics_cache_.descent = static_cast<double>(rawFace->size->metrics.descender) / 64.0;
metrics_cache_.lineGap = static_cast<double>(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<double>(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<double> 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;
+23 -1
View File
@@ -121,6 +121,8 @@ std::string FontFallback::getFallbackFontPath(const std::string& fontName, bool
stylePattern += "-italic";
}
std::lock_guard<std::mutex> 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<std::string> 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<std::string> 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<std::mutex> lock(rules_mutex_);
custom_rules_.insert(custom_rules_.begin(), {lowerPattern, {systemFontPath}});
}
void FontFallback::resetToDefaults() {
std::lock_guard<std::mutex> lock(rules_mutex_);
custom_rules_.clear();
}
@@ -2,6 +2,7 @@
#include <string>
#include <vector>
#include <mutex>
namespace pdfengine::fonts::pdf_fonts {
@@ -32,6 +33,7 @@ private:
};
std::vector<FallbackRule> default_rules_;
std::vector<FallbackRule> custom_rules_;
mutable std::mutex rules_mutex_;
};
} // namespace pdfengine::fonts::pdf_fonts
@@ -1,9 +1,85 @@
#include "fonts/pdf_fonts/font_subset.hpp"
#include <algorithm>
#include <cctype>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <hb.h>
#include <hb-subset.h>
namespace pdfengine::fonts::pdf_fonts {
void FontSubset::populateFromFace(FontSubset& subset, void* ftFace) {
if (!ftFace) return;
FT_Face face = static_cast<FT_Face>(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<uint32_t>(charcode));
charcode = FT_Get_Next_Char(face, charcode, &gindex);
}
}
std::vector<uint8_t> FontSubset::buildSubset(const std::vector<uint8_t>& originalStream, const std::vector<uint32_t>& glyphIdsToKeep) {
std::vector<uint8_t> result;
if (originalStream.empty() || glyphIdsToKeep.empty()) {
return result;
}
hb_blob_t* blob = hb_blob_create(
reinterpret_cast<const char*>(originalStream.data()),
static_cast<unsigned int>(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;
@@ -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<uint8_t> buildSubset(const std::vector<uint8_t>& originalStream, const std::vector<uint32_t>& glyphIdsToKeep);
explicit FontSubset(const std::string& fontName);
~FontSubset() = default;
+33 -11
View File
@@ -54,7 +54,13 @@ CIDFont::CIDFont(
CIDFont::~CIDFont() = default;
bool CIDFont::loadFromStream(const std::vector<uint8_t>& 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<uint32_t>(charcode);
}
charcode = FT_Get_Next_Char(face, charcode, &gindex);
}
if (!is_gid_to_unicode_map_built_) {
buildGidToUnicodeMap();
}
std::lock_guard<std::mutex> 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<uint32_t>& charCode
return result;
}
void CIDFont::buildGidToUnicodeMap() const {
std::lock_guard<std::mutex> 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<uint32_t>(charcode);
charcode = FT_Get_Next_Char(face, charcode, &gindex);
}
}
is_gid_to_unicode_map_built_ = true;
}
} // namespace pdfengine::fonts::pdf_fonts
@@ -8,6 +8,7 @@
#include <memory>
#include <unordered_map>
#include <string>
#include <mutex>
namespace pdfengine::fonts::pdf_fonts {
class FontSubset;
@@ -60,6 +61,11 @@ private:
bool is_identity_map_ = true;
std::unordered_map<uint32_t, uint32_t> cid_to_gid_map_;
std::unique_ptr<FontSubset> subset_info_;
mutable bool is_gid_to_unicode_map_built_ = false;
mutable std::mutex gid_to_unicode_mutex_;
mutable std::unordered_map<uint32_t, uint32_t> gid_to_unicode_map_;
void buildGidToUnicodeMap() const;
};
} // namespace pdfengine::fonts::pdf_fonts
@@ -50,7 +50,13 @@ TrueTypeFont::TrueTypeFont(
TrueTypeFont::~TrueTypeFont() = default;
bool TrueTypeFont::loadFromStream(const std::vector<uint8_t>& 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<uint32_t>(charcode);
}
charcode = FT_Get_Next_Char(face, charcode, &gindex);
}
if (subset_info_->hasGlyphMapping(charCode)) {
return subset_info_->mapSubsetToOriginal(charCode);
}
}
@@ -50,7 +50,13 @@ Type1Font::Type1Font(
Type1Font::~Type1Font() = default;
bool Type1Font::loadFromStream(const std::vector<uint8_t>& 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<uint32_t>(charcode);
}
charcode = FT_Get_Next_Char(face, charcode, &gindex);
}
if (subset_info_->hasGlyphMapping(charCode)) {
return subset_info_->mapSubsetToOriginal(charCode);
}
}
+1
View File
@@ -91,6 +91,7 @@ std::vector<ShapedGlyph> HbShaper::shapeRun(
sg.advanceY = static_cast<double>(glyphPositions[i].y_advance) / 64.0;
sg.offsetX = static_cast<double>(glyphPositions[i].x_offset) / 64.0;
sg.offsetY = static_cast<double>(glyphPositions[i].y_offset) / 64.0;
sg.clusterIndex = glyphInfos[i].cluster;
result.push_back(sg);
}
}
+1
View File
@@ -13,6 +13,7 @@ struct ShapedGlyph {
double advanceY;
double offsetX;
double offsetY;
uint32_t clusterIndex;
};
class HbShaper {
+329 -6
View File
@@ -10,6 +10,9 @@
#include "parser/pdfium_loader.hpp"
#endif
#include "fonts/loader/font_resolver.hpp"
#include "fonts/pdf_fonts/font.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <csetjmp>
@@ -806,6 +809,229 @@ std::expected<std::vector<GlyphBounds>, EngineError> PdfiumPage::extractTextWith
#endif
}
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::vector<Glyph> 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<char> 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<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) {
return a.originX < b.originX;
});
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 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<Paragraph> 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<std::vector<std::string>, EngineError> PdfiumPage::extractAnnotationsText() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
@@ -1516,10 +1742,46 @@ std::expected<std::vector<uint8_t>, 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<std::mutex> 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<std::mutex> lock(fontsMutex_);
fontDataScannedPages_ = i + 1;
continue;
}
int objectCount = FPDFPage_CountObjects(page);
for (int j = 0; j < objectCount; ++j) {
@@ -1534,21 +1796,49 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
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) {
std::lock_guard<std::mutex> 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<uint8_t> buffer(buflen);
if (FPDFFont_GetFontData(font, buffer.data(), buflen) > 0) {
FPDF_ClosePage(page);
return buffer;
fontDataCache_[fontName] = buffer;
} else {
fontDataCache_[fontName] = std::vector<uint8_t>();
}
} else {
fontDataCache_[fontName] = std::vector<uint8_t>();
}
}
// 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<std::mutex> lock(fontsMutex_);
fontDataScannedPages_ = i + 1;
}
// We finished scanning all pages and still didn't find it (or extraction failed)
{
std::lock_guard<std::mutex> lock(fontsMutex_);
// Mark as failed so we don't try to rescan for it
if (fontDataCache_.find(expectedFontName) == fontDataCache_.end()) {
fontDataCache_[expectedFontName] = std::vector<uint8_t>();
}
}
return std::unexpected(EngineError::FileNotFound);
#else
@@ -1557,6 +1847,35 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
#endif
}
std::expected<std::shared_ptr<fonts::pdf_fonts::Font>, std::string> PdfiumDocument::getResolvedFont(const FontInfo& fontInfo) {
#ifdef PDFENGINE_WITH_PDFIUM
std::lock_guard<std::mutex> 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<fonts::pdf_fonts::Font> 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<std::mutex> lock(fontsMutex_);
@@ -1567,6 +1886,10 @@ void PdfiumDocument::invalidateCaches() {
std::lock_guard<std::mutex> lock(pageCacheMutex_);
pageCache_.clear();
}
{
std::lock_guard<std::mutex> lock(resolvedFontsMutex_);
resolvedFontsCache_.clear();
}
spdlog::info("Document caches have been invalidated.");
}
+14 -1
View File
@@ -12,6 +12,7 @@
#include <vector>
#include <mutex>
#include <unordered_map>
namespace pdfengine::fonts::loader { class FontResolver; }
namespace pdfengine::parser {
@@ -41,6 +42,7 @@ public:
std::expected<PageImage, EngineError> render(int dpi = 96) const override;
std::expected<std::string, EngineError> extractText() const override;
std::expected<std::vector<GlyphBounds>, EngineError> extractTextWithBounds() const override;
std::expected<PageModel, EngineError> extractDocumentModel() const override;
std::expected<std::vector<FontInfo>, EngineError> getFonts() const override;
std::expected<std::vector<std::string>, 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<PdfiumDocument> {
public:
explicit PdfiumDocument(NativeDocHandle docHandle);
PdfiumDocument(NativeDocHandle docHandle, std::vector<uint8_t> memoryBuffer);
@@ -78,6 +80,8 @@ public:
std::expected<std::shared_ptr<PdfPage>, EngineError> getPage(int pageIndex) override;
std::expected<std::vector<FontInfo>, EngineError> getFonts(int startPage = 0, int endPage = -1) const override;
std::expected<std::vector<uint8_t>, EngineError> getFontData(const std::string& internalFontId) const override;
std::expected<std::shared_ptr<fonts::pdf_fonts::Font>, std::string> getResolvedFont(const FontInfo& fontInfo) override;
void invalidateCaches();
std::expected<void, EngineError> 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<std::string, std::vector<uint8_t>> fontDataCache_;
mutable int fontDataScannedPages_ = 0;
mutable std::unordered_map<int, std::shared_ptr<PdfPage>> pageCache_;
mutable std::mutex pageCacheMutex_;
// Font Engine Bridge
std::unique_ptr<pdfengine::fonts::loader::FontResolver> fontResolver_;
std::unordered_map<std::string, std::shared_ptr<fonts::pdf_fonts::Font>> resolvedFontsCache_;
std::mutex resolvedFontsMutex_;
};
// Exposed for testing