fix
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ struct ShapedGlyph {
|
||||
double advanceY;
|
||||
double offsetX;
|
||||
double offsetY;
|
||||
uint32_t clusterIndex;
|
||||
};
|
||||
|
||||
class HbShaper {
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user