Merge branch 'dev' of https://gitea.maskantech.in/gitea_admin/pdf into furqan
This commit is contained in:
@@ -17,6 +17,9 @@ add_library(pdfengine STATIC
|
||||
src/parser/pdfium_loader.cpp
|
||||
src/parser/pdfium_document.cpp
|
||||
src/fonts/face/font_face.cpp
|
||||
src/fonts/face/free_type_manager.cpp
|
||||
src/fonts/loader/font_resolver.cpp
|
||||
src/fonts/pdf_fonts/font_loader.cpp
|
||||
src/fonts/shaping/hb_shaper.cpp
|
||||
src/fonts/cache/glyph_bitmap.cpp
|
||||
src/fonts/cache/glyph_cache.cpp
|
||||
@@ -47,6 +50,7 @@ target_link_libraries(pdfengine
|
||||
PRIVATE
|
||||
freetype
|
||||
harfbuzz::harfbuzz
|
||||
harfbuzz::harfbuzz-subset
|
||||
PNG::PNG
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
@@ -77,6 +77,52 @@ 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;
|
||||
bool isEmbedded = false;
|
||||
std::string type;
|
||||
std::vector<Glyph> glyphs;
|
||||
double x = 0.0, y = 0.0, w = 0.0, h = 0.0;
|
||||
};
|
||||
|
||||
struct TextLine {
|
||||
std::vector<TextRun> runs;
|
||||
std::vector<Glyph> glyphs;
|
||||
double angle = 0.0;
|
||||
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,14 +135,20 @@ 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;
|
||||
|
||||
[[nodiscard]] virtual std::expected<double, EngineError> getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const = 0;
|
||||
|
||||
[[nodiscard]] virtual DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0;
|
||||
[[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;
|
||||
@@ -116,6 +168,12 @@ public:
|
||||
[[nodiscard]] virtual std::expected<std::vector<FontInfo>, EngineError>
|
||||
getFonts(int startPage = 0, int endPage = -1) const = 0;
|
||||
|
||||
[[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>
|
||||
|
||||
+6
-6
@@ -13,12 +13,12 @@ GlyphCache::GlyphCache(std::size_t capacity)
|
||||
GlyphCache::~GlyphCache() = default;
|
||||
|
||||
std::optional<GlyphBitmap> GlyphCache::get(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize) {
|
||||
FT_Face face = fontFace.getFace();
|
||||
if (!face) {
|
||||
uint64_t fontId = fontFace.getId();
|
||||
if (fontId == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
GlyphCacheKey key{face, glyphIndex, fontSize};
|
||||
GlyphCacheKey key{fontId, glyphIndex, fontSize};
|
||||
std::size_t shard_idx = getShardIndex(key);
|
||||
auto& shard = *shards_[shard_idx];
|
||||
|
||||
@@ -37,12 +37,12 @@ std::optional<GlyphBitmap> GlyphCache::get(const FontFace& fontFace, unsigned in
|
||||
}
|
||||
|
||||
void GlyphCache::insert(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize, const GlyphBitmap& bitmap) {
|
||||
FT_Face face = fontFace.getFace();
|
||||
if (!face) {
|
||||
uint64_t fontId = fontFace.getId();
|
||||
if (fontId == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
GlyphCacheKey key{face, glyphIndex, fontSize};
|
||||
GlyphCacheKey key{fontId, glyphIndex, fontSize};
|
||||
std::size_t shard_idx = getShardIndex(key);
|
||||
auto& shard = *shards_[shard_idx];
|
||||
|
||||
|
||||
+3
-3
@@ -12,12 +12,12 @@
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
struct GlyphCacheKey {
|
||||
FT_Face face;
|
||||
uint64_t fontId;
|
||||
unsigned int glyphIndex;
|
||||
unsigned int fontSize;
|
||||
|
||||
bool operator==(const GlyphCacheKey& other) const {
|
||||
return face == other.face &&
|
||||
return fontId == other.fontId &&
|
||||
glyphIndex == other.glyphIndex &&
|
||||
fontSize == other.fontSize;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ struct GlyphCacheKey {
|
||||
|
||||
struct GlyphCacheKeyHash {
|
||||
std::size_t operator()(const GlyphCacheKey& key) const {
|
||||
std::size_t h1 = std::hash<void*>{}(static_cast<void*>(key.face));
|
||||
std::size_t h1 = std::hash<uint64_t>{}(key.fontId);
|
||||
std::size_t h2 = std::hash<unsigned int>{}(key.glyphIndex);
|
||||
std::size_t h3 = std::hash<unsigned int>{}(key.fontSize);
|
||||
// Combine hashes using standard boost hash_combine algorithm
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#include "fonts/face/font_face.hpp"
|
||||
#include "fonts/face/free_type_manager.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <atomic>
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
FontFace::FontFace()
|
||||
: ft_library_(nullptr),
|
||||
face_(nullptr) {
|
||||
static std::atomic<uint64_t> g_font_id_counter{1};
|
||||
|
||||
if (FT_Init_FreeType(&ft_library_)) {
|
||||
std::cerr << "Failed to initialize FreeType\n";
|
||||
}
|
||||
FontFace::FontFace()
|
||||
: font_id_(g_font_id_counter.fetch_add(1, std::memory_order_relaxed)),
|
||||
face_(nullptr),
|
||||
mutex_(std::make_unique<std::mutex>()) {
|
||||
}
|
||||
|
||||
FontFace::~FontFace() {
|
||||
@@ -18,17 +19,14 @@ FontFace::~FontFace() {
|
||||
if (face_) {
|
||||
FT_Done_Face(face_);
|
||||
}
|
||||
|
||||
if (ft_library_) {
|
||||
FT_Done_FreeType(ft_library_);
|
||||
}
|
||||
}
|
||||
|
||||
FontFace::FontFace(FontFace&& other) noexcept
|
||||
: ft_library_(other.ft_library_),
|
||||
: font_id_(other.font_id_),
|
||||
face_(other.face_),
|
||||
mutex_(std::move(other.mutex_)),
|
||||
font_data_(std::move(other.font_data_)) {
|
||||
other.ft_library_ = nullptr;
|
||||
other.font_id_ = 0;
|
||||
other.face_ = nullptr;
|
||||
}
|
||||
|
||||
@@ -37,13 +35,11 @@ FontFace& FontFace::operator=(FontFace&& other) noexcept {
|
||||
if (face_) {
|
||||
FT_Done_Face(face_);
|
||||
}
|
||||
if (ft_library_) {
|
||||
FT_Done_FreeType(ft_library_);
|
||||
}
|
||||
ft_library_ = other.ft_library_;
|
||||
font_id_ = other.font_id_;
|
||||
face_ = other.face_;
|
||||
mutex_ = std::move(other.mutex_);
|
||||
font_data_ = std::move(other.font_data_);
|
||||
other.ft_library_ = nullptr;
|
||||
other.font_id_ = 0;
|
||||
other.face_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
@@ -58,7 +54,7 @@ bool FontFace::loadFromFile(const std::string& path) {
|
||||
font_data_.clear();
|
||||
|
||||
if (FT_New_Face(
|
||||
ft_library_,
|
||||
FreeTypeManager::instance().getLibrary(),
|
||||
path.c_str(),
|
||||
0,
|
||||
&face_)) {
|
||||
@@ -91,7 +87,7 @@ bool FontFace::loadFromMemory(const std::vector<uint8_t>& data) {
|
||||
font_data_ = data;
|
||||
|
||||
if (FT_New_Memory_Face(
|
||||
ft_library_,
|
||||
FreeTypeManager::instance().getLibrary(),
|
||||
font_data_.data(),
|
||||
static_cast<FT_Long>(font_data_.size()),
|
||||
0,
|
||||
@@ -113,11 +109,21 @@ FT_Face FontFace::getFace() const {
|
||||
return face_;
|
||||
}
|
||||
|
||||
uint64_t FontFace::getId() const {
|
||||
return font_id_;
|
||||
}
|
||||
|
||||
std::mutex& FontFace::getMutex() const {
|
||||
return *mutex_;
|
||||
}
|
||||
|
||||
std::optional<GlyphBitmap> FontFace::renderGlyph(unsigned int glyphIndex, unsigned int fontSize) {
|
||||
if (!face_) {
|
||||
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;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
@@ -26,13 +28,16 @@ public:
|
||||
bool loadFromMemory(const std::vector<uint8_t>& data);
|
||||
|
||||
FT_Face getFace() const;
|
||||
uint64_t getId() const;
|
||||
std::mutex& getMutex() const;
|
||||
|
||||
// Renders a glyph by index and size, returning a GlyphBitmap on success.
|
||||
std::optional<GlyphBitmap> renderGlyph(unsigned int glyphIndex, unsigned int fontSize);
|
||||
|
||||
private:
|
||||
FT_Library ft_library_;
|
||||
uint64_t font_id_;
|
||||
FT_Face face_;
|
||||
std::unique_ptr<std::mutex> mutex_;
|
||||
std::vector<uint8_t> font_data_; // Keeps the loaded memory buffer alive for FT_Face
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "fonts/face/free_type_manager.hpp"
|
||||
#include <iostream>
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
FreeTypeManager& FreeTypeManager::instance() {
|
||||
static FreeTypeManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
FreeTypeManager::FreeTypeManager() : library_(nullptr) {
|
||||
if (FT_Init_FreeType(&library_)) {
|
||||
std::cerr << "Failed to initialize FreeType\n";
|
||||
}
|
||||
}
|
||||
|
||||
FreeTypeManager::~FreeTypeManager() {
|
||||
if (library_) {
|
||||
FT_Done_FreeType(library_);
|
||||
library_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
FT_Library FreeTypeManager::getLibrary() const {
|
||||
return library_;
|
||||
}
|
||||
|
||||
} // namespace pdfengine::fonts
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <ft2build.h>
|
||||
#include FT_FREETYPE_H
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
class FreeTypeManager {
|
||||
public:
|
||||
static FreeTypeManager& instance();
|
||||
|
||||
FT_Library getLibrary() const;
|
||||
|
||||
// Delete copy/move constructors and assignment operators for singleton
|
||||
FreeTypeManager(const FreeTypeManager&) = delete;
|
||||
FreeTypeManager& operator=(const FreeTypeManager&) = delete;
|
||||
FreeTypeManager(FreeTypeManager&&) = delete;
|
||||
FreeTypeManager& operator=(FreeTypeManager&&) = delete;
|
||||
|
||||
private:
|
||||
FreeTypeManager();
|
||||
~FreeTypeManager();
|
||||
|
||||
FT_Library library_;
|
||||
};
|
||||
|
||||
} // namespace pdfengine::fonts
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "fonts/loader/font_resolver.hpp"
|
||||
#include "fonts/pdf_fonts/font_loader.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace pdfengine::fonts::loader {
|
||||
|
||||
FontResolver::FontResolver(std::shared_ptr<PdfDocument> document)
|
||||
: document_(std::move(document)) {}
|
||||
|
||||
std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> FontResolver::resolveFont(const FontInfo& fontInfo) {
|
||||
if (!document_) {
|
||||
return std::unexpected("No document attached to FontResolver");
|
||||
}
|
||||
|
||||
if (fontInfo.isEmbedded) {
|
||||
auto dataRes = document_->getFontData(fontInfo.internalFontId);
|
||||
if (!dataRes) {
|
||||
spdlog::error("Failed to extract embedded font data for ID: {}", fontInfo.internalFontId);
|
||||
return std::unexpected("Failed to extract embedded font data");
|
||||
}
|
||||
|
||||
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") {
|
||||
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, std::move(descriptor));
|
||||
if (font) return font;
|
||||
}
|
||||
|
||||
return std::unexpected("Failed to parse extracted font data");
|
||||
} else {
|
||||
// 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, std::move(descriptor));
|
||||
if (font) return font;
|
||||
} else {
|
||||
auto font = pdf_fonts::FontLoader::loadType1SystemFallback(fontInfo.normalizedFamily, std::move(descriptor));
|
||||
if (font) return font;
|
||||
}
|
||||
|
||||
return std::unexpected("Failed to load system fallback font");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pdfengine::fonts::loader
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "fonts/pdf_fonts/font.hpp"
|
||||
#include <pdfengine/pdf_document.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <expected>
|
||||
|
||||
namespace pdfengine::fonts::loader {
|
||||
|
||||
class FontResolver {
|
||||
public:
|
||||
explicit FontResolver(std::shared_ptr<PdfDocument> document);
|
||||
|
||||
// Resolves a FontInfo object into a fully loaded Font ready for shaping.
|
||||
// If the font is embedded, it extracts the raw bytes from the PdfDocument.
|
||||
// If it's a system fallback, it uses FontLoader to load it from the OS.
|
||||
std::expected<std::unique_ptr<pdf_fonts::Font>, std::string> resolveFont(const FontInfo& fontInfo);
|
||||
|
||||
private:
|
||||
std::shared_ptr<PdfDocument> document_;
|
||||
};
|
||||
|
||||
} // namespace pdfengine::fonts::loader
|
||||
@@ -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) {
|
||||
@@ -126,7 +132,8 @@ uint32_t CIDFont::decodeToUnicode(uint32_t charCode) const {
|
||||
if (descriptor_) {
|
||||
std::string fontName = descriptor_->getFontName();
|
||||
std::string lowerName = fontName;
|
||||
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
|
||||
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
std::string registry = "None";
|
||||
if (lowerName.find("simsun") != std::string::npos ||
|
||||
@@ -177,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;
|
||||
@@ -203,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <hb.h>
|
||||
#include <hb-ft.h>
|
||||
#include <mutex>
|
||||
|
||||
namespace pdfengine::fonts {
|
||||
|
||||
@@ -21,6 +22,9 @@ std::vector<ShapedGlyph> HbShaper::shapeRun(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Acquire lock to prevent concurrent mutation of FT_Face's active pixel size
|
||||
std::lock_guard<std::mutex> lock(font.getMutex());
|
||||
|
||||
// Set the pixel size on the FreeType face before shaping.
|
||||
// This ensures HarfBuzz measures everything using the requested font size context.
|
||||
if (FT_Set_Pixel_Sizes(ftFace, 0, fontSize)) {
|
||||
@@ -53,8 +57,23 @@ std::vector<ShapedGlyph> HbShaper::shapeRun(
|
||||
hb_buffer_set_direction(hbBuffer, HB_DIRECTION_TTB);
|
||||
}
|
||||
|
||||
// Enable professional typography features (Kerning and Ligatures)
|
||||
hb_feature_t features[2];
|
||||
|
||||
// Enable kerning
|
||||
features[0].tag = HB_TAG('k', 'e', 'r', 'n');
|
||||
features[0].value = 1;
|
||||
features[0].start = 0;
|
||||
features[0].end = static_cast<unsigned int>(-1);
|
||||
|
||||
// Enable standard ligatures
|
||||
features[1].tag = HB_TAG('l', 'i', 'g', 'a');
|
||||
features[1].value = 1;
|
||||
features[1].start = 0;
|
||||
features[1].end = static_cast<unsigned int>(-1);
|
||||
|
||||
// Shape the text inside the buffer using the font.
|
||||
hb_shape(hbFont, hbBuffer, nullptr, 0);
|
||||
hb_shape(hbFont, hbBuffer, features, 2);
|
||||
|
||||
// Retrieve the results.
|
||||
unsigned int glyphCount = 0;
|
||||
@@ -72,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 {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "parser/pdfium_document.hpp"
|
||||
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
#include <fpdfview.h>
|
||||
#include <fpdf_text.h>
|
||||
@@ -11,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 <fstream>
|
||||
@@ -401,8 +403,13 @@ void deduceFontMetadata(pdfengine::FontInfo& f) {
|
||||
}
|
||||
|
||||
// 7. Stable Internal Font Identifier
|
||||
// For subset fonts, fontName already contains the subset prefix (e.g. "ABCDEF+Arial").
|
||||
// Use the full fontName directly — it already encodes both the subset tag and
|
||||
// the base font name, separated by '+'. Concatenating subsetTag + "_" + fontName
|
||||
// would duplicate the prefix ("ABCDEF_ABCDEF+Arial").
|
||||
if (f.isSubset && !f.subsetTag.empty()) {
|
||||
f.internalFontId = f.subsetTag + "_" + f.fontName;
|
||||
// fontName is "ABCDEF+Arial"; use it as-is for the stable ID.
|
||||
f.internalFontId = f.fontName;
|
||||
} else {
|
||||
f.internalFontId = f.fontName + "_" + f.type + "_" + std::to_string(f.flags);
|
||||
}
|
||||
@@ -802,7 +809,7 @@ std::expected<std::vector<GlyphBounds>, EngineError> PdfiumPage::extractTextWith
|
||||
}
|
||||
|
||||
std::string utf8_char = code_point_to_utf8(cp);
|
||||
if (utf8_char.empty()) {
|
||||
if (utf8_char.empty() || cp == '\r' || cp == '\n') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -827,6 +834,254 @@ 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::unordered_map<std::string, FontInfo> fontMap;
|
||||
if (auto fontsRes = getFonts()) {
|
||||
for (const auto& f : *fontsRes) {
|
||||
fontMap[f.fontName] = f;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Glyph> documentGlyphs;
|
||||
documentGlyphs.reserve(charCount);
|
||||
|
||||
// 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;
|
||||
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
|
||||
currentRun.internalFontId = it->second.internalFontId;
|
||||
currentRun.isEmbedded = it->second.isEmbedded;
|
||||
currentRun.type = it->second.type;
|
||||
}
|
||||
currentRun.glyphs.push_back(*firstG);
|
||||
currentRun.text += firstG->text;
|
||||
|
||||
for (size_t i = 1; i < line.glyphs.size(); ++i) {
|
||||
const auto& prevG = line.glyphs[i-1];
|
||||
const auto& currG = line.glyphs[i];
|
||||
|
||||
double gap = currG.bboxX - (prevG.bboxX + prevG.bboxW);
|
||||
double 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;
|
||||
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
|
||||
currentRun.internalFontId = it->second.internalFontId;
|
||||
currentRun.isEmbedded = it->second.isEmbedded;
|
||||
currentRun.type = it->second.type;
|
||||
}
|
||||
}
|
||||
currentRun.glyphs.push_back(spaceGlyph);
|
||||
currentRun.text += spaceGlyph.text;
|
||||
} else if (breakRun) {
|
||||
line.runs.push_back(std::move(currentRun));
|
||||
currentRun = TextRun();
|
||||
currentRun.fontName = currG.fontName;
|
||||
currentRun.fontSize = currG.fontSize;
|
||||
currentRun.flags = currG.flags;
|
||||
if (auto it = fontMap.find(currentRun.fontName); it != fontMap.end()) {
|
||||
currentRun.internalFontId = it->second.internalFontId;
|
||||
currentRun.isEmbedded = it->second.isEmbedded;
|
||||
currentRun.type = it->second.type;
|
||||
}
|
||||
}
|
||||
|
||||
currentRun.glyphs.push_back(currG);
|
||||
currentRun.text += currG.text;
|
||||
}
|
||||
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_) {
|
||||
@@ -884,6 +1139,60 @@ Point2D PdfiumPage::deviceToPage(const DevicePoint& devicePoint, int deviceWidth
|
||||
#endif
|
||||
}
|
||||
|
||||
std::expected<double, EngineError> PdfiumPage::getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
if (!page_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
FPDF_FONT font = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(textMutex_);
|
||||
|
||||
auto cached = fontHandleCache_.find(fontName);
|
||||
if (cached != fontHandleCache_.end()) {
|
||||
font = cached->second;
|
||||
} else {
|
||||
int objectCount = FPDFPage_CountObjects(page_);
|
||||
for (int i = 0; i < objectCount; ++i) {
|
||||
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page_, i);
|
||||
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
|
||||
|
||||
FPDF_FONT pageFont = FPDFTextObj_GetFont(obj);
|
||||
if (!pageFont) continue;
|
||||
|
||||
size_t nameLen = FPDFFont_GetBaseFontName(pageFont, nullptr, 0);
|
||||
if (nameLen > 0) {
|
||||
std::vector<char> nameBuf(nameLen);
|
||||
if (FPDFFont_GetBaseFontName(pageFont, nameBuf.data(), nameLen) > 0) {
|
||||
std::string currentName(nameBuf.data());
|
||||
if (currentName == fontName) {
|
||||
font = pageFont;
|
||||
fontHandleCache_[fontName] = font;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!font) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
float width = 0.0f;
|
||||
if (!FPDFFont_GetGlyphWidth(font, charcode, static_cast<float>(fontSize), &width)) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
return static_cast<double>(width);
|
||||
#else
|
||||
(void)fontName; (void)charcode; (void)fontSize;
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PdfiumPage::ensureTextPageLoaded() const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
std::lock_guard<std::mutex> lock(textMutex_);
|
||||
@@ -962,11 +1271,26 @@ std::expected<std::shared_ptr<PdfPage>, EngineError> PdfiumDocument::getPage(int
|
||||
if (pageIndex < 0 || pageIndex >= pageCount()) {
|
||||
return std::unexpected(EngineError::PageOutOfBounds);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pageCacheMutex_);
|
||||
auto it = pageCache_.find(pageIndex);
|
||||
if (it != pageCache_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
|
||||
if (!page) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
return std::make_shared<PdfiumPage>(page, pageIndex);
|
||||
|
||||
auto pageObj = std::make_shared<PdfiumPage>(page, pageIndex);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pageCacheMutex_);
|
||||
pageCache_[pageIndex] = pageObj;
|
||||
}
|
||||
return pageObj;
|
||||
#else
|
||||
(void)pageIndex;
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
@@ -1332,7 +1656,7 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
invalidateFontCache();
|
||||
invalidateCaches();
|
||||
return {};
|
||||
#else
|
||||
(void)editsJson;
|
||||
@@ -1553,8 +1877,10 @@ std::expected<std::vector<FontInfo>, EngineError> PdfiumPage::getFonts() const {
|
||||
// deduceFontMetadata() (SystemFallback or Substituted).
|
||||
|
||||
// --- Recalculate stable identifier with corrected data ---
|
||||
// fontName already includes the subset prefix (e.g. "ABCDEF+Arial");
|
||||
// using it directly avoids the duplicate-prefix bug.
|
||||
if (f.isSubset && !f.subsetTag.empty()) {
|
||||
f.internalFontId = f.subsetTag + "_" + f.fontName;
|
||||
f.internalFontId = f.fontName;
|
||||
} else {
|
||||
f.internalFontId = f.fontName + "_" + f.type + "_" + std::to_string(f.flags);
|
||||
}
|
||||
@@ -1618,12 +1944,6 @@ std::expected<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int s
|
||||
return std::vector<FontInfo>();
|
||||
}
|
||||
|
||||
// Security Safeguard: cap maximum scan range to 1000 pages to prevent memory/CPU exhaustion
|
||||
int scanCount = endPage - startPage + 1;
|
||||
if (scanCount > 1000) {
|
||||
spdlog::warn("Requested scan range ({} pages) exceeds limit. Capping scan to 1000 pages.", scanCount);
|
||||
endPage = startPage + 999;
|
||||
}
|
||||
|
||||
// Return full document-level cache if available and full range is requested
|
||||
if (startPage == 0 && endPage == total - 1 && hasCachedFonts_) {
|
||||
@@ -1632,15 +1952,13 @@ std::expected<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int s
|
||||
|
||||
std::vector<FontInfo> aggregated;
|
||||
for (int i = startPage; i <= endPage; ++i) {
|
||||
FPDF_PAGE page = FPDF_LoadPage(doc_, i);
|
||||
if (!page) {
|
||||
auto pageRes = const_cast<PdfiumDocument*>(this)->getPage(i);
|
||||
if (!pageRes) {
|
||||
spdlog::error("Failed to load page index {} for font diagnostics", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stack-allocated wrapper ensures FPDF handles are closed properly upon destruction
|
||||
PdfiumPage tempPage(page, i);
|
||||
auto pageFontsRes = tempPage.getFonts();
|
||||
auto pageFontsRes = pageRes.value()->getFonts();
|
||||
if (pageFontsRes) {
|
||||
for (const auto& f : *pageFontsRes) {
|
||||
auto it = std::find_if(aggregated.begin(), aggregated.end(), [&](const FontInfo& existing) {
|
||||
@@ -1681,11 +1999,164 @@ std::expected<std::vector<FontInfo>, EngineError> PdfiumDocument::getFonts(int s
|
||||
#endif
|
||||
}
|
||||
|
||||
void PdfiumDocument::invalidateFontCache() {
|
||||
std::lock_guard<std::mutex> lock(fontsMutex_);
|
||||
cachedFonts_.clear();
|
||||
hasCachedFonts_ = false;
|
||||
spdlog::info("Document font cache has been invalidated.");
|
||||
std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(const std::string& internalFontId) const {
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
ensure_pdfium_initialized();
|
||||
if (!doc_) {
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
}
|
||||
|
||||
// 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_);
|
||||
|
||||
// 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) {
|
||||
std::lock_guard<std::mutex> lock(fontsMutex_);
|
||||
fontDataScannedPages_ = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int objectCount = FPDFPage_CountObjects(page);
|
||||
for (int j = 0; j < objectCount; ++j) {
|
||||
FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, j);
|
||||
if (!obj || FPDFPageObj_GetType(obj) != FPDF_PAGEOBJ_TEXT) continue;
|
||||
|
||||
FPDF_FONT font = FPDFTextObj_GetFont(obj);
|
||||
if (!font) continue;
|
||||
|
||||
unsigned long nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
|
||||
if (nameLen > 0) {
|
||||
std::vector<char> nameBuf(nameLen);
|
||||
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) {
|
||||
std::string fontName(nameBuf.data());
|
||||
|
||||
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 = 0;
|
||||
FPDFFont_GetFontData(font, nullptr, 0, &buflen);
|
||||
if (buflen > 0) {
|
||||
std::vector<uint8_t> buffer(buflen);
|
||||
size_t actual_len = 0;
|
||||
if (FPDFFont_GetFontData(font, buffer.data(), buflen, &actual_len)) {
|
||||
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
|
||||
(void)internalFontId;
|
||||
return std::unexpected(EngineError::Unknown);
|
||||
#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_);
|
||||
cachedFonts_.clear();
|
||||
hasCachedFonts_ = false;
|
||||
}
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
namespace pdfengine::fonts::loader { class FontResolver; }
|
||||
|
||||
namespace pdfengine::parser {
|
||||
|
||||
@@ -40,9 +42,12 @@ 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;
|
||||
|
||||
std::expected<double, EngineError> getGlyphWidth(const std::string& fontName, uint32_t charcode, double fontSize) const override;
|
||||
|
||||
DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
|
||||
|
||||
@@ -51,11 +56,14 @@ private:
|
||||
mutable NativeTextHandle textPage_ = nullptr;
|
||||
int pageIndex_ = 0;
|
||||
mutable std::mutex textMutex_;
|
||||
#ifdef PDFENGINE_WITH_PDFIUM
|
||||
mutable std::unordered_map<std::string, FPDF_FONT> fontHandleCache_;
|
||||
#endif
|
||||
|
||||
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);
|
||||
@@ -71,7 +79,10 @@ 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;
|
||||
void invalidateFontCache();
|
||||
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;
|
||||
std::expected<std::vector<uint8_t>, EngineError> saveIncremental() const override;
|
||||
@@ -84,6 +95,18 @@ private:
|
||||
mutable std::vector<FontInfo> cachedFonts_;
|
||||
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
|
||||
|
||||
@@ -887,7 +887,7 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
|
||||
}
|
||||
EXPECT_EQ(f.sourceType, "Embedded");
|
||||
EXPECT_TRUE(f.isEmbedded);
|
||||
EXPECT_EQ(f.internalFontId, f.subsetTag + "_" + f.fontName);
|
||||
EXPECT_EQ(f.internalFontId, f.fontName);
|
||||
} else {
|
||||
EXPECT_TRUE(f.subsetTag.empty());
|
||||
EXPECT_EQ(f.internalFontId, f.fontName + "_" + f.type + "_" + std::to_string(f.flags));
|
||||
|
||||
Reference in New Issue
Block a user