From 77b59d44324748c3436bf6d951957a1a1e41faef Mon Sep 17 00:00:00 2001 From: saqib mir Date: Fri, 22 May 2026 15:48:57 +0530 Subject: [PATCH] FreeType + HarfBuzz wrappers + LRU glyph cache --- engine/CMakeLists.txt | 9 +- engine/src/fonts/cache/glyph_bitmap.cpp | 1 + engine/src/fonts/cache/glyph_bitmap.hpp | 16 + engine/src/fonts/cache/glyph_cache.cpp | 86 +++ engine/src/fonts/cache/glyph_cache.hpp | 80 +++ engine/src/fonts/face/font_face.cpp | 158 +++++ engine/src/fonts/{ => face}/font_face.hpp | 11 +- engine/src/fonts/font_face.cpp | 75 --- engine/src/fonts/glyph_bitmap.cpp | 0 engine/src/fonts/glyph_bitmap.hpp | 0 engine/src/fonts/glyph_cache.cpp | 0 engine/src/fonts/glyph_cache.hpp | 0 engine/src/fonts/pdf/pdf_font.hpp | 35 ++ engine/src/fonts/pdf/pdf_font_descriptor.cpp | 175 ++++++ engine/src/fonts/pdf/pdf_font_descriptor.hpp | 100 +++ engine/src/fonts/pdf/pdf_font_loader.cpp | 18 + engine/src/fonts/pdf/pdf_font_loader.hpp | 21 + engine/src/fonts/pdf/truetype_font.cpp | 41 ++ engine/src/fonts/pdf/truetype_font.hpp | 39 ++ engine/src/fonts/{ => shaping}/hb_shaper.cpp | 29 +- engine/src/fonts/{ => shaping}/hb_shaper.hpp | 21 +- engine/tests/fonts_test.cpp | 609 ++++++++++++++++++- 22 files changed, 1421 insertions(+), 103 deletions(-) create mode 100644 engine/src/fonts/cache/glyph_bitmap.cpp create mode 100644 engine/src/fonts/cache/glyph_bitmap.hpp create mode 100644 engine/src/fonts/cache/glyph_cache.cpp create mode 100644 engine/src/fonts/cache/glyph_cache.hpp create mode 100644 engine/src/fonts/face/font_face.cpp rename engine/src/fonts/{ => face}/font_face.hpp (55%) delete mode 100644 engine/src/fonts/font_face.cpp delete mode 100644 engine/src/fonts/glyph_bitmap.cpp delete mode 100644 engine/src/fonts/glyph_bitmap.hpp delete mode 100644 engine/src/fonts/glyph_cache.cpp delete mode 100644 engine/src/fonts/glyph_cache.hpp create mode 100644 engine/src/fonts/pdf/pdf_font.hpp create mode 100644 engine/src/fonts/pdf/pdf_font_descriptor.cpp create mode 100644 engine/src/fonts/pdf/pdf_font_descriptor.hpp create mode 100644 engine/src/fonts/pdf/pdf_font_loader.cpp create mode 100644 engine/src/fonts/pdf/pdf_font_loader.hpp create mode 100644 engine/src/fonts/pdf/truetype_font.cpp create mode 100644 engine/src/fonts/pdf/truetype_font.hpp rename engine/src/fonts/{ => shaping}/hb_shaper.cpp (71%) rename engine/src/fonts/{ => shaping}/hb_shaper.hpp (53%) diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 2016748..a2be80b 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -10,8 +10,13 @@ configure_file( add_library(pdfengine STATIC src/core/engine_info.cpp src/parser/pdfium_loader.cpp - src/fonts/font_face.cpp - src/fonts/hb_shaper.cpp + src/fonts/face/font_face.cpp + src/fonts/shaping/hb_shaper.cpp + src/fonts/cache/glyph_bitmap.cpp + src/fonts/cache/glyph_cache.cpp + src/fonts/pdf/truetype_font.cpp + src/fonts/pdf/pdf_font_loader.cpp + src/fonts/pdf/pdf_font_descriptor.cpp ) add_library(pdfengine::pdfengine ALIAS pdfengine) diff --git a/engine/src/fonts/cache/glyph_bitmap.cpp b/engine/src/fonts/cache/glyph_bitmap.cpp new file mode 100644 index 0000000..09bfb9f --- /dev/null +++ b/engine/src/fonts/cache/glyph_bitmap.cpp @@ -0,0 +1 @@ +#include "fonts/cache/glyph_bitmap.hpp" diff --git a/engine/src/fonts/cache/glyph_bitmap.hpp b/engine/src/fonts/cache/glyph_bitmap.hpp new file mode 100644 index 0000000..fd252c9 --- /dev/null +++ b/engine/src/fonts/cache/glyph_bitmap.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace pdfengine::fonts { + +struct GlyphBitmap { + std::vector pixels; // 8-bit grayscale pixels (0 = transparent, 255 = fully opaque) + int width = 0; // Width of the glyph bitmap in pixels + int height = 0; // Height of the glyph bitmap in pixels + int bearingX = 0; // Horizontal bearing X (bitmap_left) in pixels + int bearingY = 0; // Horizontal bearing Y (bitmap_top) in pixels + double advance = 0.0; // Horizontal advance in pixels +}; + +} // namespace pdfengine::fonts diff --git a/engine/src/fonts/cache/glyph_cache.cpp b/engine/src/fonts/cache/glyph_cache.cpp new file mode 100644 index 0000000..8825842 --- /dev/null +++ b/engine/src/fonts/cache/glyph_cache.cpp @@ -0,0 +1,86 @@ +#include "fonts/cache/glyph_cache.hpp" + +namespace pdfengine::fonts { + +GlyphCache::GlyphCache(std::size_t capacity) + : capacity_(capacity) {} + +GlyphCache::~GlyphCache() = default; + +std::optional GlyphCache::get(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize) { + FT_Face face = fontFace.getFace(); + if (!face) { + return std::nullopt; + } + + GlyphCacheKey key{face, glyphIndex, fontSize}; + auto it = cache_map_.find(key); + if (it == cache_map_.end()) { + misses_++; + return std::nullopt; // Cache miss + } + + hits_++; + // Cache hit: move the referenced key to the front of the LRU list + lru_list_.splice(lru_list_.begin(), lru_list_, it->second.second); + + return it->second.first; +} + +void GlyphCache::insert(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize, const GlyphBitmap& bitmap) { + FT_Face face = fontFace.getFace(); + if (!face) { + return; + } + + GlyphCacheKey key{face, glyphIndex, fontSize}; + auto it = cache_map_.find(key); + if (it != cache_map_.end()) { + // Element already exists: update bitmap and move it to the front + it->second.first = bitmap; + lru_list_.splice(lru_list_.begin(), lru_list_, it->second.second); + return; + } + + // Evict oldest element if at capacity + if (cache_map_.size() >= capacity_ && capacity_ > 0) { + GlyphCacheKey oldest = lru_list_.back(); + cache_map_.erase(oldest); + lru_list_.pop_back(); + } + + // Insert new element + if (capacity_ > 0) { + lru_list_.push_front(key); + cache_map_[key] = std::make_pair(bitmap, lru_list_.begin()); + } +} + +std::size_t GlyphCache::size() const { + return cache_map_.size(); +} + +std::size_t GlyphCache::capacity() const { + return capacity_; +} + +void GlyphCache::clear() { + cache_map_.clear(); + lru_list_.clear(); + resetStats(); +} + +double GlyphCache::hitRate() const { + std::size_t total = hits_ + misses_; + if (total == 0) { + return 0.0; + } + return static_cast(hits_) / total; +} + +void GlyphCache::resetStats() { + hits_ = 0; + misses_ = 0; +} + +} // namespace pdfengine::fonts diff --git a/engine/src/fonts/cache/glyph_cache.hpp b/engine/src/fonts/cache/glyph_cache.hpp new file mode 100644 index 0000000..560488b --- /dev/null +++ b/engine/src/fonts/cache/glyph_cache.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include "fonts/face/font_face.hpp" +#include "fonts/cache/glyph_bitmap.hpp" +#include +#include +#include +#include + +namespace pdfengine::fonts { + +struct GlyphCacheKey { + FT_Face face; + unsigned int glyphIndex; + unsigned int fontSize; + + bool operator==(const GlyphCacheKey& other) const { + return face == other.face && + glyphIndex == other.glyphIndex && + fontSize == other.fontSize; + } +}; + +struct GlyphCacheKeyHash { + std::size_t operator()(const GlyphCacheKey& key) const { + std::size_t h1 = std::hash{}(static_cast(key.face)); + std::size_t h2 = std::hash{}(key.glyphIndex); + std::size_t h3 = std::hash{}(key.fontSize); + // Combine hashes using standard boost hash_combine algorithm + return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)) ^ (h3 + 0x9e3779b9 + (h2 << 6) + (h2 >> 2)); + } +}; + +class GlyphCache { +public: + explicit GlyphCache(std::size_t capacity); + ~GlyphCache(); + + // Cache is move-only to prevent copying internal list iterators + GlyphCache(const GlyphCache&) = delete; + GlyphCache& operator=(const GlyphCache&) = delete; + GlyphCache(GlyphCache&&) noexcept = default; + GlyphCache& operator=(GlyphCache&&) noexcept = default; + + // Retrieves a glyph from the cache (and marks it as most recently used on hit) + std::optional get(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize); + + // Inserts a glyph into the cache. Evicts the least recently used glyph if full. + void insert(const FontFace& fontFace, unsigned int glyphIndex, unsigned int fontSize, const GlyphBitmap& bitmap); + + // Returns the current number of cached glyphs + std::size_t size() const; + + // Returns the maximum capacity of the cache + std::size_t capacity() const; + + // Clears all elements from the cache + void clear(); + + // Returns the cache hit rate (hits / (hits + misses)). Returns 0.0 if no lookups have occurred. + double hitRate() const; + + // Resets hit and miss counters + void resetStats(); + +private: + std::size_t capacity_; + std::size_t hits_ = 0; + std::size_t misses_ = 0; + std::list lru_list_; + + using CacheIterator = std::list::iterator; + std::unordered_map< + GlyphCacheKey, + std::pair, + GlyphCacheKeyHash + > cache_map_; +}; + +} // namespace pdfengine::fonts diff --git a/engine/src/fonts/face/font_face.cpp b/engine/src/fonts/face/font_face.cpp new file mode 100644 index 0000000..296159f --- /dev/null +++ b/engine/src/fonts/face/font_face.cpp @@ -0,0 +1,158 @@ +#include "fonts/face/font_face.hpp" + +#include + +namespace pdfengine::fonts { + +FontFace::FontFace() + : ft_library_(nullptr), + face_(nullptr) { + + if (FT_Init_FreeType(&ft_library_)) { + std::cerr << "Failed to initialize FreeType\n"; + } +} + +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_), + face_(other.face_), + font_data_(std::move(other.font_data_)) { + other.ft_library_ = nullptr; + other.face_ = nullptr; +} + +FontFace& FontFace::operator=(FontFace&& other) noexcept { + if (this != &other) { + if (face_) { + FT_Done_Face(face_); + } + if (ft_library_) { + FT_Done_FreeType(ft_library_); + } + ft_library_ = other.ft_library_; + face_ = other.face_; + font_data_ = std::move(other.font_data_); + other.ft_library_ = nullptr; + other.face_ = nullptr; + } + return *this; +} + +bool FontFace::loadFromFile(const std::string& path) { + + if (face_) { + FT_Done_Face(face_); + face_ = nullptr; + } + font_data_.clear(); + + if (FT_New_Face( + ft_library_, + path.c_str(), + 0, + &face_)) { + + std::cerr << "Failed to load font: " + << path << '\n'; + + return false; + } + + // Set a default pixel size of 16px so that font coordinates and shaping + // advances are non-zero by default. + FT_Set_Pixel_Sizes(face_, 0, 16); + + return true; +} + +bool FontFace::loadFromMemory(const std::vector& data) { + if (data.empty()) { + std::cerr << "Cannot load font from empty memory buffer\n"; + return false; + } + + if (face_) { + FT_Done_Face(face_); + face_ = nullptr; + } + + // Copy to internal buffer to guarantee its lifetime aligns with face_ + font_data_ = data; + + if (FT_New_Memory_Face( + ft_library_, + font_data_.data(), + static_cast(font_data_.size()), + 0, + &face_)) { + + std::cerr << "Failed to load font from memory buffer\n"; + font_data_.clear(); + return false; + } + + // Set a default pixel size of 16px so that font coordinates and shaping + // advances are non-zero by default. + FT_Set_Pixel_Sizes(face_, 0, 16); + + return true; +} + +FT_Face FontFace::getFace() const { + return face_; +} + +std::optional FontFace::renderGlyph(unsigned int glyphIndex, unsigned int fontSize) { + if (!face_) { + return std::nullopt; + } + + // Set font size in pixels. + if (FT_Set_Pixel_Sizes(face_, 0, fontSize)) { + return std::nullopt; + } + + // Load and render the glyph bitmap into the face->glyph slot. + if (FT_Load_Glyph(face_, glyphIndex, FT_LOAD_RENDER)) { + return std::nullopt; + } + + FT_GlyphSlot slot = face_->glyph; + FT_Bitmap& bitmap = slot->bitmap; + + GlyphBitmap glyph_bitmap; + glyph_bitmap.width = static_cast(bitmap.width); + glyph_bitmap.height = static_cast(bitmap.rows); + glyph_bitmap.bearingX = slot->bitmap_left; + glyph_bitmap.bearingY = slot->bitmap_top; + + // Advance is in 26.6 fractional pixels. Convert to double. + glyph_bitmap.advance = static_cast(slot->advance.x) / 64.0; + + // Extract the pixels. Pitch specifies bytes per row. + if (glyph_bitmap.width > 0 && glyph_bitmap.height > 0) { + glyph_bitmap.pixels.resize(glyph_bitmap.width * glyph_bitmap.height); + for (int r = 0; r < glyph_bitmap.height; ++r) { + std::copy( + bitmap.buffer + r * bitmap.pitch, + bitmap.buffer + r * bitmap.pitch + glyph_bitmap.width, + glyph_bitmap.pixels.begin() + r * glyph_bitmap.width + ); + } + } + + return glyph_bitmap; +} + +} // namespace pdfengine::fonts diff --git a/engine/src/fonts/font_face.hpp b/engine/src/fonts/face/font_face.hpp similarity index 55% rename from engine/src/fonts/font_face.hpp rename to engine/src/fonts/face/font_face.hpp index a158153..025b611 100644 --- a/engine/src/fonts/font_face.hpp +++ b/engine/src/fonts/face/font_face.hpp @@ -1,6 +1,10 @@ #pragma once +#include "fonts/cache/glyph_bitmap.hpp" +#include #include +#include +#include #include #include FT_FREETYPE_H @@ -19,12 +23,17 @@ public: FontFace& operator=(FontFace&& other) noexcept; bool loadFromFile(const std::string& path); + bool loadFromMemory(const std::vector& data); FT_Face getFace() const; + // Renders a glyph by index and size, returning a GlyphBitmap on success. + std::optional renderGlyph(unsigned int glyphIndex, unsigned int fontSize); + private: FT_Library ft_library_; FT_Face face_; + std::vector font_data_; // Keeps the loaded memory buffer alive for FT_Face }; -} // namespace pdfengine::fonts \ No newline at end of file +} // namespace pdfengine::fonts diff --git a/engine/src/fonts/font_face.cpp b/engine/src/fonts/font_face.cpp deleted file mode 100644 index 509838a..0000000 --- a/engine/src/fonts/font_face.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "font_face.hpp" - -#include - -namespace pdfengine::fonts { - -FontFace::FontFace() - : ft_library_(nullptr), - face_(nullptr) { - - if (FT_Init_FreeType(&ft_library_)) { - std::cerr << "Failed to initialize FreeType\n"; - } -} - -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_), - face_(other.face_) { - other.ft_library_ = nullptr; - other.face_ = nullptr; -} - -FontFace& FontFace::operator=(FontFace&& other) noexcept { - if (this != &other) { - if (face_) { - FT_Done_Face(face_); - } - if (ft_library_) { - FT_Done_FreeType(ft_library_); - } - ft_library_ = other.ft_library_; - face_ = other.face_; - other.ft_library_ = nullptr; - other.face_ = nullptr; - } - return *this; -} - -bool FontFace::loadFromFile(const std::string& path) { - - if (FT_New_Face( - ft_library_, - path.c_str(), - 0, - &face_)) { - - std::cerr << "Failed to load font: " - << path << '\n'; - - return false; - } - - // Set a default pixel size of 16px so that font coordinates and shaping - // advances are non-zero by default. - FT_Set_Pixel_Sizes(face_, 0, 16); - - return true; -} - -FT_Face FontFace::getFace() const { - return face_; -} - -} // namespace pdfengine::fonts \ No newline at end of file diff --git a/engine/src/fonts/glyph_bitmap.cpp b/engine/src/fonts/glyph_bitmap.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/engine/src/fonts/glyph_bitmap.hpp b/engine/src/fonts/glyph_bitmap.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/engine/src/fonts/glyph_cache.cpp b/engine/src/fonts/glyph_cache.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/engine/src/fonts/glyph_cache.hpp b/engine/src/fonts/glyph_cache.hpp deleted file mode 100644 index e69de29..0000000 diff --git a/engine/src/fonts/pdf/pdf_font.hpp b/engine/src/fonts/pdf/pdf_font.hpp new file mode 100644 index 0000000..9cefa85 --- /dev/null +++ b/engine/src/fonts/pdf/pdf_font.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "fonts/face/font_face.hpp" +#include +#include + +namespace pdfengine::fonts::pdf { + +class PdfFontDescriptor; + +enum class FontType { + TrueType, + Type1, + CIDFontType0, + CIDFontType2, + Type3 +}; + +class PdfFont { +public: + virtual ~PdfFont() = default; + + virtual std::string getBaseFont() const = 0; + virtual FontType getType() const = 0; + virtual bool isEmbedded() const = 0; + + // Gets reference to underlying FontFace rendering object + virtual pdfengine::fonts::FontFace& getFontFace() = 0; + virtual const pdfengine::fonts::FontFace& getFontFace() const = 0; + + // Gets the font descriptor (returns nullptr if none exists) + virtual const PdfFontDescriptor* getDescriptor() const = 0; +}; + +} // namespace pdfengine::fonts::pdf diff --git a/engine/src/fonts/pdf/pdf_font_descriptor.cpp b/engine/src/fonts/pdf/pdf_font_descriptor.cpp new file mode 100644 index 0000000..80f75b9 --- /dev/null +++ b/engine/src/fonts/pdf/pdf_font_descriptor.cpp @@ -0,0 +1,175 @@ +#include "fonts/pdf/pdf_font_descriptor.hpp" +#include +#include +#include + +namespace pdfengine::fonts::pdf { + +namespace { + +void skipWhitespace(const std::string& str, size_t& pos) { + while (pos < str.size()) { + char c = str[pos]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\0') { + pos++; + } else if (c == '%') { + // comment, skip to end of line + while (pos < str.size() && str[pos] != '\n' && str[pos] != '\r') { + pos++; + } + } else { + break; + } + } +} + +} // namespace + +bool PdfFontDescriptor::parseFromDictionaryString(const std::string& dictStr) { + size_t pos = 0; + skipWhitespace(dictStr, pos); + + if (pos + 2 > dictStr.size() || dictStr[pos] != '<' || dictStr[pos+1] != '<') { + return false; // must start with << + } + pos += 2; + + while (true) { + skipWhitespace(dictStr, pos); + if (pos >= dictStr.size()) { + return false; // missing closing >> + } + + if (pos + 2 <= dictStr.size() && dictStr[pos] == '>' && dictStr[pos+1] == '>') { + pos += 2; + break; // successfully reached closing >> + } + + if (dictStr[pos] != '/') { + return false; // key must start with / + } + pos++; // skip '/' + + size_t keyStart = pos; + while (pos < dictStr.size()) { + char c = dictStr[pos]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\0' || + c == '(' || c == ')' || c == '<' || c == '>' || c == '[' || c == ']' || + c == '{' || c == '}' || c == '/' || c == '%') { + break; + } + pos++; + } + if (pos == keyStart) { + return false; // empty key name + } + std::string key = dictStr.substr(keyStart, pos - keyStart); + + skipWhitespace(dictStr, pos); + if (pos >= dictStr.size()) { + return false; // key with no value + } + + std::string valueStr; + if (dictStr[pos] == '[') { + size_t arrStart = pos; + pos++; // skip '[' + int depth = 1; + while (pos < dictStr.size() && depth > 0) { + if (dictStr[pos] == '[') depth++; + else if (dictStr[pos] == ']') depth--; + pos++; + } + if (depth > 0) { + return false; // unmatched brackets + } + valueStr = dictStr.substr(arrStart, pos - arrStart); + } else if (dictStr[pos] == '/') { + pos++; // skip '/' + size_t valStart = pos; + while (pos < dictStr.size()) { + char c = dictStr[pos]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\0' || + c == '(' || c == ')' || c == '<' || c == '>' || c == '[' || c == ']' || + c == '{' || c == '}' || c == '/' || c == '%') { + break; + } + pos++; + } + valueStr = "/" + dictStr.substr(valStart, pos - valStart); + } else { + size_t valStart = pos; + while (pos < dictStr.size()) { + char c = dictStr[pos]; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\0' || + c == '(' || c == ')' || c == '<' || c == '>' || c == '[' || c == ']' || + c == '{' || c == '}' || c == '/' || c == '%') { + break; + } + pos++; + } + valueStr = dictStr.substr(valStart, pos - valStart); + } + + try { + if (key == "FontName") { + if (!valueStr.empty() && valueStr[0] == '/') { + font_name_ = valueStr.substr(1); + } else { + font_name_ = valueStr; + } + } else if (key == "Flags") { + flags_ = std::stoi(valueStr); + } else if (key == "ItalicAngle") { + italic_angle_ = std::stod(valueStr); + } else if (key == "Ascent") { + ascent_ = std::stod(valueStr); + } else if (key == "Descent") { + descent_ = std::stod(valueStr); + } else if (key == "Leading") { + leading_ = std::stod(valueStr); + } else if (key == "CapHeight") { + cap_height_ = std::stod(valueStr); + } else if (key == "XHeight") { + x_height_ = std::stod(valueStr); + } else if (key == "StemV") { + stem_v_ = std::stod(valueStr); + } else if (key == "StemH") { + stem_h_ = std::stod(valueStr); + } else if (key == "AvgWidth") { + avg_width_ = std::stod(valueStr); + } else if (key == "MaxWidth") { + max_width_ = std::stod(valueStr); + } else if (key == "MissingWidth") { + missing_width_ = std::stod(valueStr); + } else if (key == "FontBBox") { + std::vector nums; + size_t idx = 0; + if (!valueStr.empty() && valueStr[0] == '[') idx++; + while (idx < valueStr.size()) { + while (idx < valueStr.size() && (valueStr[idx] == ' ' || valueStr[idx] == ',' || valueStr[idx] == '\t' || valueStr[idx] == ']' || valueStr[idx] == '\r' || valueStr[idx] == '\n')) { + idx++; + } + if (idx >= valueStr.size() || valueStr[idx] == ']') break; + size_t processed; + double val = std::stod(valueStr.substr(idx), &processed); + nums.push_back(val); + idx += processed; + } + if (nums.size() != 4) { + return false; // FontBBox must have 4 coordinates + } + font_bbox_.llx = static_cast(nums[0]); + font_bbox_.lly = static_cast(nums[1]); + font_bbox_.urx = static_cast(nums[2]); + font_bbox_.ury = static_cast(nums[3]); + } + } catch (const std::exception&) { + return false; // parsing or range exception + } + } + + return true; +} + +} // namespace pdfengine::fonts::pdf diff --git a/engine/src/fonts/pdf/pdf_font_descriptor.hpp b/engine/src/fonts/pdf/pdf_font_descriptor.hpp new file mode 100644 index 0000000..d8a1e3a --- /dev/null +++ b/engine/src/fonts/pdf/pdf_font_descriptor.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include + +namespace pdfengine::fonts::pdf { + +struct FontBBox { + int llx = 0; + int lly = 0; + int urx = 0; + int ury = 0; + + bool operator==(const FontBBox& o) const { + return llx == o.llx && lly == o.lly && urx == o.urx && ury == o.ury; + } +}; + +class PdfFontDescriptor { +public: + PdfFontDescriptor() = default; + ~PdfFontDescriptor() = default; + + // Getters and Setters + std::string getFontName() const { return font_name_; } + void setFontName(const std::string& name) { font_name_ = name; } + + int getFlags() const { return flags_; } + void setFlags(int flags) { flags_ = flags; } + + FontBBox getFontBBox() const { return font_bbox_; } + void setFontBBox(const FontBBox& bbox) { font_bbox_ = bbox; } + + double getItalicAngle() const { return italic_angle_; } + void setItalicAngle(double angle) { italic_angle_ = angle; } + + double getAscent() const { return ascent_; } + void setAscent(double ascent) { ascent_ = ascent; } + + double getDescent() const { return descent_; } + void setDescent(double descent) { descent_ = descent; } + + double getLeading() const { return leading_; } + void setLeading(double leading) { leading_ = leading; } + + double getCapHeight() const { return cap_height_; } + void setCapHeight(double cap_height) { cap_height_ = cap_height; } + + double getXHeight() const { return x_height_; } + void setXHeight(double x_height) { x_height_ = x_height; } + + double getStemV() const { return stem_v_; } + void setStemV(double stem_v) { stem_v_ = stem_v; } + + double getStemH() const { return stem_h_; } + void setStemH(double stem_h) { stem_h_ = stem_h; } + + double getAvgWidth() const { return avg_width_; } + void setAvgWidth(double avg_width) { avg_width_ = avg_width; } + + double getMaxWidth() const { return max_width_; } + void setMaxWidth(double max_width) { max_width_ = max_width; } + + double getMissingWidth() const { return missing_width_; } + void setMissingWidth(double missing_width) { missing_width_ = missing_width; } + + // Flags helper functions (PDF Spec Section 5.7.1) + bool isFixedPitch() const { return (flags_ & 1) != 0; } + bool isSerif() const { return (flags_ & 2) != 0; } + bool isSymbolic() const { return (flags_ & 4) != 0; } + bool isScript() const { return (flags_ & 8) != 0; } + bool isNonsymbolic() const { return (flags_ & 32) != 0; } + bool isItalic() const { return (flags_ & 64) != 0; } + bool isAllCap() const { return (flags_ & 65536) != 0; } + bool isSmallCap() const { return (flags_ & 131072) != 0; } + bool isForceBold() const { return (flags_ & 262144) != 0; } + + // Parses a PDF dictionary string representing a FontDescriptor. + // e.g., "<< /Type /FontDescriptor /FontName /ArialMT /Flags 32 /FontBBox [-166 -225 1000 931] /Ascent 905 /Descent -211 /CapHeight 728 /ItalicAngle 0 /StemV 94 >>" + // Returns true if parsing was successful, false otherwise. + bool parseFromDictionaryString(const std::string& dictStr); + +private: + std::string font_name_; + int flags_ = 0; + FontBBox font_bbox_; + double italic_angle_ = 0.0; + double ascent_ = 0.0; + double descent_ = 0.0; + double leading_ = 0.0; + double cap_height_ = 0.0; + double x_height_ = 0.0; + double stem_v_ = 0.0; + double stem_h_ = 0.0; + double avg_width_ = 0.0; + double max_width_ = 0.0; + double missing_width_ = 0.0; +}; + +} // namespace pdfengine::fonts::pdf diff --git a/engine/src/fonts/pdf/pdf_font_loader.cpp b/engine/src/fonts/pdf/pdf_font_loader.cpp new file mode 100644 index 0000000..34a48bd --- /dev/null +++ b/engine/src/fonts/pdf/pdf_font_loader.cpp @@ -0,0 +1,18 @@ +#include "fonts/pdf/pdf_font_loader.hpp" +#include "fonts/pdf/truetype_font.hpp" + +namespace pdfengine::fonts::pdf { + +std::unique_ptr PdfFontLoader::loadTrueTypeFromMemory( + const std::string& baseFont, + const std::vector& streamData, + std::unique_ptr descriptor +) { + auto font = std::make_unique(baseFont, true, std::move(descriptor)); + if (!font->loadFromStream(streamData)) { + return nullptr; + } + return font; +} + +} // namespace pdfengine::fonts::pdf diff --git a/engine/src/fonts/pdf/pdf_font_loader.hpp b/engine/src/fonts/pdf/pdf_font_loader.hpp new file mode 100644 index 0000000..e4e15a8 --- /dev/null +++ b/engine/src/fonts/pdf/pdf_font_loader.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "fonts/pdf/pdf_font.hpp" +#include "fonts/pdf/pdf_font_descriptor.hpp" +#include +#include +#include + +namespace pdfengine::fonts::pdf { + +class PdfFontLoader { +public: + // Factory method to load an embedded TrueType font from its raw stream bytes + static std::unique_ptr loadTrueTypeFromMemory( + const std::string& baseFont, + const std::vector& streamData, + std::unique_ptr descriptor = nullptr + ); +}; + +} // namespace pdfengine::fonts::pdf diff --git a/engine/src/fonts/pdf/truetype_font.cpp b/engine/src/fonts/pdf/truetype_font.cpp new file mode 100644 index 0000000..97f291d --- /dev/null +++ b/engine/src/fonts/pdf/truetype_font.cpp @@ -0,0 +1,41 @@ +#include "fonts/pdf/truetype_font.hpp" + +namespace pdfengine::fonts::pdf { + +TrueTypeFont::TrueTypeFont( + const std::string& baseFont, + bool isEmbedded, + std::unique_ptr descriptor +) : base_font_(baseFont), + is_embedded_(isEmbedded), + descriptor_(std::move(descriptor)) {} + +bool TrueTypeFont::loadFromStream(const std::vector& streamData) { + return font_face_.loadFromMemory(streamData); +} + +std::string TrueTypeFont::getBaseFont() const { + return base_font_; +} + +FontType TrueTypeFont::getType() const { + return FontType::TrueType; +} + +bool TrueTypeFont::isEmbedded() const { + return is_embedded_; +} + +pdfengine::fonts::FontFace& TrueTypeFont::getFontFace() { + return font_face_; +} + +const pdfengine::fonts::FontFace& TrueTypeFont::getFontFace() const { + return font_face_; +} + +const PdfFontDescriptor* TrueTypeFont::getDescriptor() const { + return descriptor_.get(); +} + +} // namespace pdfengine::fonts::pdf diff --git a/engine/src/fonts/pdf/truetype_font.hpp b/engine/src/fonts/pdf/truetype_font.hpp new file mode 100644 index 0000000..4b6bd44 --- /dev/null +++ b/engine/src/fonts/pdf/truetype_font.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "fonts/pdf/pdf_font.hpp" +#include "fonts/pdf/pdf_font_descriptor.hpp" +#include +#include +#include + +namespace pdfengine::fonts::pdf { + +class TrueTypeFont : public PdfFont { +public: + TrueTypeFont( + const std::string& baseFont, + bool isEmbedded, + std::unique_ptr descriptor = nullptr + ); + ~TrueTypeFont() override = default; + + // Load the font from raw embedded stream bytes + bool loadFromStream(const std::vector& streamData); + + std::string getBaseFont() const override; + FontType getType() const override; + bool isEmbedded() const override; + + pdfengine::fonts::FontFace& getFontFace() override; + const pdfengine::fonts::FontFace& getFontFace() const override; + + const PdfFontDescriptor* getDescriptor() const override; + +private: + std::string base_font_; + bool is_embedded_ = false; + pdfengine::fonts::FontFace font_face_; + std::unique_ptr descriptor_; +}; + +} // namespace pdfengine::fonts::pdf diff --git a/engine/src/fonts/hb_shaper.cpp b/engine/src/fonts/shaping/hb_shaper.cpp similarity index 71% rename from engine/src/fonts/hb_shaper.cpp rename to engine/src/fonts/shaping/hb_shaper.cpp index 32e52e2..dcb0818 100644 --- a/engine/src/fonts/hb_shaper.cpp +++ b/engine/src/fonts/shaping/hb_shaper.cpp @@ -1,4 +1,4 @@ -#include "hb_shaper.hpp" +#include "fonts/shaping/hb_shaper.hpp" #include #include @@ -8,24 +8,31 @@ namespace pdfengine::fonts { HbShaper::HbShaper() = default; HbShaper::~HbShaper() = default; -std::vector HbShaper::shapeText(const FontFace& fontFace, const std::string& text) { +std::vector HbShaper::shapeRun( + const std::string& text, + FontFace& font, + unsigned int fontSize +) { std::vector result; - FT_Face ftFace = fontFace.getFace(); + FT_Face ftFace = font.getFace(); if (!ftFace) { return result; } + // 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)) { + return result; + } + // Create a HarfBuzz font wrapper around the FreeType face. - // hb_ft_font_create_referenced increments the reference count of the FT_Face, - // making it safe even if the FontFace object changes or moves. hb_font_t* hbFont = hb_ft_font_create_referenced(ftFace); if (!hbFont) { return result; } - // Set the scale of HarfBuzz font to match the FreeType face size. - // If not set, HarfBuzz will default to using the font's design units (upem). + // Sync HarfBuzz font with FreeType face changes. hb_ft_font_changed(hbFont); // Create a text buffer. @@ -56,10 +63,10 @@ std::vector HbShaper::shapeText(const FontFace& fontFace, const std sg.glyphIndex = glyphInfos[i].codepoint; // HarfBuzz coordinates are fractional 26.6 pixels (1/64 of a pixel). // Convert to standard double-precision float values. - sg.xAdvance = static_cast(glyphPositions[i].x_advance) / 64.0; - sg.yAdvance = static_cast(glyphPositions[i].y_advance) / 64.0; - sg.xOffset = static_cast(glyphPositions[i].x_offset) / 64.0; - sg.yOffset = static_cast(glyphPositions[i].y_offset) / 64.0; + sg.advanceX = static_cast(glyphPositions[i].x_advance) / 64.0; + sg.advanceY = static_cast(glyphPositions[i].y_advance) / 64.0; + sg.offsetX = static_cast(glyphPositions[i].x_offset) / 64.0; + sg.offsetY = static_cast(glyphPositions[i].y_offset) / 64.0; result.push_back(sg); } } diff --git a/engine/src/fonts/hb_shaper.hpp b/engine/src/fonts/shaping/hb_shaper.hpp similarity index 53% rename from engine/src/fonts/hb_shaper.hpp rename to engine/src/fonts/shaping/hb_shaper.hpp index 0c37ee9..6d4dc75 100644 --- a/engine/src/fonts/hb_shaper.hpp +++ b/engine/src/fonts/shaping/hb_shaper.hpp @@ -1,17 +1,18 @@ #pragma once -#include "font_face.hpp" +#include "fonts/face/font_face.hpp" #include #include +#include namespace pdfengine::fonts { struct ShapedGlyph { - unsigned int glyphIndex; - double xAdvance; - double yAdvance; - double xOffset; - double yOffset; + uint32_t glyphIndex; + double advanceX; + double advanceY; + double offsetX; + double offsetY; }; class HbShaper { @@ -24,9 +25,13 @@ public: HbShaper(HbShaper&&) noexcept = default; HbShaper& operator=(HbShaper&&) noexcept = default; - // Shapes the input UTF-8 text using the given FontFace. + // Shapes the input UTF-8 text run using the given FontFace and fontSize. // Returns a vector of shaped glyphs. - std::vector shapeText(const FontFace& fontFace, const std::string& text); + std::vector shapeRun( + const std::string& text, + FontFace& font, + unsigned int fontSize + ); }; } // namespace pdfengine::fonts diff --git a/engine/tests/fonts_test.cpp b/engine/tests/fonts_test.cpp index 6d6dbe0..22ff2e7 100644 --- a/engine/tests/fonts_test.cpp +++ b/engine/tests/fonts_test.cpp @@ -1,14 +1,32 @@ -#include "fonts/font_face.hpp" -#include "fonts/hb_shaper.hpp" +#include "fonts/face/font_face.hpp" +#include "fonts/shaping/hb_shaper.hpp" +#include "fonts/cache/glyph_cache.hpp" +#include "fonts/pdf/pdf_font.hpp" +#include "fonts/pdf/truetype_font.hpp" +#include "fonts/pdf/pdf_font_loader.hpp" #include #include +#include #include #include #include namespace { +bool saveGlyphAsPGM(const pdfengine::fonts::GlyphBitmap& bitmap, const std::string& filename) { + if (bitmap.width == 0 || bitmap.height == 0 || bitmap.pixels.empty()) { + return false; + } + std::ofstream out(filename, std::ios::binary); + if (!out) { + return false; + } + out << "P5\n" << bitmap.width << " " << bitmap.height << "\n255\n"; + out.write(reinterpret_cast(bitmap.pixels.data()), bitmap.pixels.size()); + return true; +} + std::string getSystemFontPath() { #if defined(_WIN32) // Common Windows fonts @@ -90,14 +108,14 @@ TEST(FontTest, HbShaperEmptyInput) { ASSERT_TRUE(face.loadFromFile(fontPath)); HbShaper shaper; - auto glyphs = shaper.shapeText(face, ""); + auto glyphs = shaper.shapeRun("", face, 16); EXPECT_TRUE(glyphs.empty()); } TEST(FontTest, HbShaperNullFace) { FontFace face; // Null face HbShaper shaper; - auto glyphs = shaper.shapeText(face, "Hello"); + auto glyphs = shaper.shapeRun("Hello", face, 16); EXPECT_TRUE(glyphs.empty()); } @@ -114,7 +132,7 @@ TEST(FontTest, HbShaperShapeTextSuccess) { HbShaper shaper; std::string testText = "Hello World!"; - auto glyphs = shaper.shapeText(face, testText); + auto glyphs = shaper.shapeRun(testText, face, 16); // Validate that some glyphs were shaped. // Note that the number of glyphs doesn't strictly have to match testText.length() (e.g. ligatures), @@ -124,8 +142,587 @@ TEST(FontTest, HbShaperShapeTextSuccess) { for (const auto& g : glyphs) { // Glyph index should be non-zero for valid glyphs (0 is usually .notdef) // Note: some fonts might not map all characters, but Arial/DejaVu/Consolas should map ASCII. - EXPECT_GT(g.xAdvance, 0.0); + EXPECT_GT(g.advanceX, 0.0); } } +TEST(FontTest, FontFaceRenderGlyphNullFace) { + FontFace face; // Null face + auto glyph = face.renderGlyph(0, 16); + EXPECT_FALSE(glyph.has_value()); +} + +TEST(FontTest, FontFaceRenderGlyphSuccess) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run render glyph success test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + ASSERT_NE(face.getFace(), nullptr); + + // Get the glyph index for character 'A'. + unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A'); + ASSERT_GT(glyphIndex, 0u); // Ensure it's not the undefined glyph + + // Render it at 24px. + auto glyphOpt = face.renderGlyph(glyphIndex, 24); + ASSERT_TRUE(glyphOpt.has_value()); + + const auto& glyph = *glyphOpt; + EXPECT_GT(glyph.width, 0); + EXPECT_GT(glyph.height, 0); + EXPECT_EQ(glyph.pixels.size(), static_cast(glyph.width * glyph.height)); + EXPECT_GT(glyph.advance, 0.0); +} + +TEST(FontTest, GlyphCacheBasicGetInsert) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run cache test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + GlyphCache cache(10); + EXPECT_EQ(cache.capacity(), 10u); + EXPECT_EQ(cache.size(), 0u); + + // Initial check: cache miss + auto miss = cache.get(face, 12, 16); + EXPECT_FALSE(miss.has_value()); + + // Create a dummy GlyphBitmap + GlyphBitmap bitmap; + bitmap.width = 10; + bitmap.height = 12; + bitmap.pixels = std::vector(120, 255); + bitmap.advance = 8.5; + + // Insert + cache.insert(face, 12, 16, bitmap); + EXPECT_EQ(cache.size(), 1u); + + // Cache hit + auto hit = cache.get(face, 12, 16); + ASSERT_TRUE(hit.has_value()); + EXPECT_EQ(hit->width, 10); + EXPECT_EQ(hit->height, 12); + EXPECT_EQ(hit->advance, 8.5); + EXPECT_EQ(hit->pixels.size(), 120u); +} + +TEST(FontTest, GlyphCacheEvictionPolicy) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run cache eviction test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + // Capacity 2 + GlyphCache cache(2); + + GlyphBitmap bmp1{ .width = 1 }; + GlyphBitmap bmp2{ .width = 2 }; + GlyphBitmap bmp3{ .width = 3 }; + + cache.insert(face, 1, 16, bmp1); + cache.insert(face, 2, 16, bmp2); + EXPECT_EQ(cache.size(), 2u); + + // Insert third one: should evict the oldest (1, 16) + cache.insert(face, 3, 16, bmp3); + EXPECT_EQ(cache.size(), 2u); + + EXPECT_FALSE(cache.get(face, 1, 16).has_value()); + EXPECT_TRUE(cache.get(face, 2, 16).has_value()); + EXPECT_TRUE(cache.get(face, 3, 16).has_value()); +} + +TEST(FontTest, GlyphCacheLRUPolicy) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run cache LRU test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + GlyphCache cache(2); + + GlyphBitmap bmp1{ .width = 1 }; + GlyphBitmap bmp2{ .width = 2 }; + GlyphBitmap bmp3{ .width = 3 }; + + cache.insert(face, 1, 16, bmp1); + cache.insert(face, 2, 16, bmp2); + + // Access 1 to make it most recently used + auto hit = cache.get(face, 1, 16); + ASSERT_TRUE(hit.has_value()); + + // Insert 3: since 2 is the oldest (least recently used), 2 should be evicted and 1 should remain + cache.insert(face, 3, 16, bmp3); + EXPECT_EQ(cache.size(), 2u); + + EXPECT_TRUE(cache.get(face, 1, 16).has_value()); + EXPECT_FALSE(cache.get(face, 2, 16).has_value()); + EXPECT_TRUE(cache.get(face, 3, 16).has_value()); +} + +TEST(FontTest, FontPipelineIntegration) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run pipeline integration test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + HbShaper shaper; + GlyphCache cache(100); + + std::vector testTexts = {"Hello World", "office", "سلام"}; + unsigned int fontSize = 16; + + for (const auto& text : testTexts) { + auto shapedGlyphs = shaper.shapeRun(text, face, fontSize); + EXPECT_FALSE(shapedGlyphs.empty()); + + for (const auto& sg : shapedGlyphs) { + auto cachedBmp = cache.get(face, sg.glyphIndex, fontSize); + if (!cachedBmp.has_value()) { + auto renderedOpt = face.renderGlyph(sg.glyphIndex, fontSize); + ASSERT_TRUE(renderedOpt.has_value()); + cache.insert(face, sg.glyphIndex, fontSize, *renderedOpt); + EXPECT_EQ(renderedOpt->pixels.size(), static_cast(renderedOpt->width * renderedOpt->height)); + } + + auto hitBmp = cache.get(face, sg.glyphIndex, fontSize); + ASSERT_TRUE(hitBmp.has_value()); + EXPECT_EQ(hitBmp->pixels.size(), static_cast(hitBmp->width * hitBmp->height)); + EXPECT_GE(hitBmp->advance, 0.0); + } + } +} + +TEST(FontTest, UnicodeAndRtlShaping) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run Unicode and RTL shaping test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + HbShaper shaper; + unsigned int fontSize = 16; + + // Test A: Arabic (RTL) - "سلام" + { + std::string arabicText = "سلام"; + auto glyphs = shaper.shapeRun(arabicText, face, fontSize); + EXPECT_FALSE(glyphs.empty()); + for (const auto& g : glyphs) { + // Validate that shaping executed and returned valid layout metrics + EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.offsetX != 0.0 || g.offsetY != 0.0 || g.glyphIndex != 999999u); + } + } + + // Test B: Hindi - "नमस्ते" + { + std::string hindiText = "नमस्ते"; + auto glyphs = shaper.shapeRun(hindiText, face, fontSize); + EXPECT_FALSE(glyphs.empty()); + for (const auto& g : glyphs) { + EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u); + } + } + + // Test C: Ligatures - "office" + { + std::string ligatureText = "office"; + auto glyphs = shaper.shapeRun(ligatureText, face, fontSize); + EXPECT_FALSE(glyphs.empty()); + EXPECT_LE(glyphs.size(), ligatureText.length()); + for (const auto& g : glyphs) { + EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u); + } + } +} + +TEST(FontTest, CachePerformanceTest) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run cache performance test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + HbShaper shaper; + GlyphCache cache(100); + + std::string text = "Hello Hello Hello Hello"; + unsigned int fontSize = 16; + + { + auto glyphs = shaper.shapeRun(text, face, fontSize); + for (const auto& g : glyphs) { + auto cached = cache.get(face, g.glyphIndex, fontSize); + if (!cached.has_value()) { + auto rendered = face.renderGlyph(g.glyphIndex, fontSize); + if (rendered.has_value()) { + cache.insert(face, g.glyphIndex, fontSize, *rendered); + } + } + } + } + + cache.resetStats(); + + for (int i = 0; i < 1000; ++i) { + auto glyphs = shaper.shapeRun(text, face, fontSize); + for (const auto& g : glyphs) { + auto cached = cache.get(face, g.glyphIndex, fontSize); + if (!cached.has_value()) { + auto rendered = face.renderGlyph(g.glyphIndex, fontSize); + if (rendered.has_value()) { + cache.insert(face, g.glyphIndex, fontSize, *rendered); + } + } + } + } + + double hitRateVal = cache.hitRate(); + std::cout << "[ INFO ] Cache Hit Rate for repetitive text: " << (hitRateVal * 100.0) << "%" << std::endl; + EXPECT_GT(hitRateVal, 0.90); +} + +TEST(FontTest, EngineStressTest) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run engine stress test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + HbShaper shaper; + GlyphCache cache(32); + + std::string base = "The quick brown fox jumps over the lazy dog. 1234567890!@#$%^&*() "; + std::string longText; + longText.reserve(10000); + while (longText.length() < 10000) { + longText += base; + } + + unsigned int fontSize = 16; + + auto glyphs = shaper.shapeRun(longText, face, fontSize); + EXPECT_FALSE(glyphs.empty()); + + for (const auto& g : glyphs) { + auto cached = cache.get(face, g.glyphIndex, fontSize); + if (!cached.has_value()) { + auto rendered = face.renderGlyph(g.glyphIndex, fontSize); + if (rendered.has_value()) { + cache.insert(face, g.glyphIndex, fontSize, *rendered); + } + } + } + + EXPECT_LE(cache.size(), cache.capacity()); + EXPECT_GT(cache.size(), 0u); +} + +TEST(FontTest, VisualBitmapDebugging) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run visual bitmap debugging test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A'); + ASSERT_GT(glyphIndex, 0u); + + auto renderedOpt = face.renderGlyph(glyphIndex, 48); + ASSERT_TRUE(renderedOpt.has_value()); + + std::string filename = "A.pgm"; + std::filesystem::remove(filename); + + ASSERT_TRUE(saveGlyphAsPGM(*renderedOpt, filename)); + EXPECT_TRUE(std::filesystem::exists(filename)); + EXPECT_GT(std::filesystem::file_size(filename), 0u); +} + +TEST(FontTest, MetricsValidation) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run metrics validation test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + FT_Face ftFace = face.getFace(); + ASSERT_NE(ftFace, nullptr); + + unsigned int glyphIndex = FT_Get_Char_Index(ftFace, 'B'); + ASSERT_GT(glyphIndex, 0u); + + unsigned int fontSize = 24; + auto renderedOpt = face.renderGlyph(glyphIndex, fontSize); + ASSERT_TRUE(renderedOpt.has_value()); + + ASSERT_EQ(FT_Set_Pixel_Sizes(ftFace, 0, fontSize), 0); + ASSERT_EQ(FT_Load_Glyph(ftFace, glyphIndex, FT_LOAD_RENDER), 0); + + FT_GlyphSlot slot = ftFace->glyph; + EXPECT_EQ(renderedOpt->width, static_cast(slot->bitmap.width)); + EXPECT_EQ(renderedOpt->height, static_cast(slot->bitmap.rows)); + EXPECT_EQ(renderedOpt->bearingX, slot->bitmap_left); + EXPECT_EQ(renderedOpt->bearingY, slot->bitmap_top); + EXPECT_DOUBLE_EQ(renderedOpt->advance, static_cast(slot->advance.x) / 64.0); +} + +TEST(FontTest, CacheRecencyStress) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run cache recency stress test."; + } + + FontFace face; + ASSERT_TRUE(face.loadFromFile(fontPath)); + + GlyphCache cache(5); + + std::vector bmps; + for (int i = 0; i < 10; ++i) { + GlyphBitmap b; + b.width = i; + bmps.push_back(b); + } + + for (unsigned int i = 0; i < 5; ++i) { + cache.insert(face, i, 16, bmps[i]); + } + EXPECT_EQ(cache.size(), 5u); + + ASSERT_TRUE(cache.get(face, 0, 16).has_value()); + ASSERT_TRUE(cache.get(face, 2, 16).has_value()); + + cache.insert(face, 5, 16, bmps[5]); + // 1 should be evicted because it was the oldest + EXPECT_FALSE(cache.get(face, 1, 16).has_value()); + // 5 was just inserted, should be at the front + EXPECT_TRUE(cache.get(face, 5, 16).has_value()); + + // Access 3 to promote it to the front + ASSERT_TRUE(cache.get(face, 3, 16).has_value()); + + // Insert 6. With cache.get lookups, 4 is now the oldest (since 3, 5, 2, 0 have been looked up recently) + cache.insert(face, 6, 16, bmps[6]); + // 4 should be evicted + EXPECT_FALSE(cache.get(face, 4, 16).has_value()); + + // The rest should remain + EXPECT_TRUE(cache.get(face, 0, 16).has_value()); + EXPECT_TRUE(cache.get(face, 2, 16).has_value()); + EXPECT_TRUE(cache.get(face, 3, 16).has_value()); + EXPECT_TRUE(cache.get(face, 5, 16).has_value()); + EXPECT_TRUE(cache.get(face, 6, 16).has_value()); +} + +TEST(PdfFontLoaderTest, FontFaceLoadFromMemorySuccess) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run load from memory success test."; + } + + // Read the entire file into a buffer + std::ifstream file(fontPath, std::ios::binary | std::ios::ate); + ASSERT_TRUE(file.is_open()); + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector buffer(size); + ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); + + // Load from memory + FontFace face; + ASSERT_TRUE(face.loadFromMemory(buffer)); + ASSERT_NE(face.getFace(), nullptr); + + // Validate that glyph rendering and metrics are valid + unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'M'); + ASSERT_GT(glyphIndex, 0u); + + auto glyph = face.renderGlyph(glyphIndex, 16); + ASSERT_TRUE(glyph.has_value()); + EXPECT_GT(glyph->width, 0); + EXPECT_GT(glyph->height, 0); + EXPECT_GT(glyph->advance, 0.0); +} + +TEST(PdfFontLoaderTest, FontFaceLoadFromMemoryInvalid) { + FontFace face; + // Empty vector + std::vector emptyData; + EXPECT_FALSE(face.loadFromMemory(emptyData)); + EXPECT_EQ(face.getFace(), nullptr); + + // Corrupt garbage data + std::vector corruptData = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; + EXPECT_FALSE(face.loadFromMemory(corruptData)); + EXPECT_EQ(face.getFace(), nullptr); +} + +TEST(PdfFontLoaderTest, PdfFontLoaderTrueTypeSuccess) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run PDF font loader test."; + } + + std::ifstream file(fontPath, std::ios::binary | std::ios::ate); + ASSERT_TRUE(file.is_open()); + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buffer(size); + ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); + + // Use factory loader + auto pdfFont = pdfengine::fonts::pdf::PdfFontLoader::loadTrueTypeFromMemory("Arial", buffer); + ASSERT_NE(pdfFont, nullptr); + EXPECT_EQ(pdfFont->getBaseFont(), "Arial"); + EXPECT_EQ(pdfFont->getType(), pdfengine::fonts::pdf::FontType::TrueType); + EXPECT_TRUE(pdfFont->isEmbedded()); + + // Shaping via the loaded FontFace + HbShaper shaper; + auto glyphs = shaper.shapeRun("Test Memory Load", pdfFont->getFontFace(), 16); + EXPECT_FALSE(glyphs.empty()); +} + +TEST(PdfFontDescriptorTest, DescriptorDefaultValues) { + pdfengine::fonts::pdf::PdfFontDescriptor desc; + EXPECT_EQ(desc.getFontName(), ""); + EXPECT_EQ(desc.getFlags(), 0); + EXPECT_EQ(desc.getItalicAngle(), 0.0); + EXPECT_EQ(desc.getAscent(), 0.0); + EXPECT_EQ(desc.getDescent(), 0.0); + EXPECT_EQ(desc.getCapHeight(), 0.0); + EXPECT_EQ(desc.getStemV(), 0.0); + + pdfengine::fonts::pdf::FontBBox bbox = desc.getFontBBox(); + EXPECT_EQ(bbox.llx, 0); + EXPECT_EQ(bbox.lly, 0); + EXPECT_EQ(bbox.urx, 0); + EXPECT_EQ(bbox.ury, 0); + + EXPECT_FALSE(desc.isFixedPitch()); + EXPECT_FALSE(desc.isSerif()); + EXPECT_FALSE(desc.isSymbolic()); + EXPECT_FALSE(desc.isItalic()); +} + +TEST(PdfFontDescriptorTest, DescriptorParsingSuccess) { + std::string dict = + "<< /Type /FontDescriptor\n" + " /FontName /ArialMT\n" + " /Flags 32\n" + " /FontBBox [-166 -225 1000 931]\n" + " /ItalicAngle 0\n" + " /Ascent 905\n" + " /Descent -211\n" + " /CapHeight 728\n" + " /StemV 94\n" + ">>"; + + pdfengine::fonts::pdf::PdfFontDescriptor desc; + ASSERT_TRUE(desc.parseFromDictionaryString(dict)); + + EXPECT_EQ(desc.getFontName(), "ArialMT"); + EXPECT_EQ(desc.getFlags(), 32); + + pdfengine::fonts::pdf::FontBBox bbox = desc.getFontBBox(); + EXPECT_EQ(bbox.llx, -166); + EXPECT_EQ(bbox.lly, -225); + EXPECT_EQ(bbox.urx, 1000); + EXPECT_EQ(bbox.ury, 931); + + EXPECT_DOUBLE_EQ(desc.getItalicAngle(), 0.0); + EXPECT_DOUBLE_EQ(desc.getAscent(), 905.0); + EXPECT_DOUBLE_EQ(desc.getDescent(), -211.0); + EXPECT_DOUBLE_EQ(desc.getCapHeight(), 728.0); + EXPECT_DOUBLE_EQ(desc.getStemV(), 94.0); + + // Check flags + EXPECT_FALSE(desc.isFixedPitch()); + EXPECT_TRUE(desc.isNonsymbolic()); // 32 + EXPECT_FALSE(desc.isItalic()); +} + +TEST(PdfFontDescriptorTest, DescriptorParsingMalformed) { + pdfengine::fonts::pdf::PdfFontDescriptor desc; + + // Missing << + EXPECT_FALSE(desc.parseFromDictionaryString("/Flags 32 >>")); + + // Unmatched >> + EXPECT_FALSE(desc.parseFromDictionaryString("<< /Flags 32")); + + // Malformed BBox array (missing urx, ury) + EXPECT_FALSE(desc.parseFromDictionaryString("<< /FontBBox [-166 -225] >>")); + + // Malformed double conversion + EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent abc >>")); + + // Key without value + EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent >>")); +} + +TEST(PdfFontDescriptorTest, PdfFontLoaderWithDescriptor) { + std::string fontPath = getSystemFontPath(); + if (fontPath.empty()) { + GTEST_SKIP() << "No system font found to run PDF font loader test."; + } + + std::ifstream file(fontPath, std::ios::binary | std::ios::ate); + ASSERT_TRUE(file.is_open()); + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector buffer(size); + ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); + + // Create descriptor + auto descriptor = std::make_unique(); + descriptor->setFontName("Arial-BoldMT"); + descriptor->setFlags(96); // Nonsymbolic (32) | Italic (64) + descriptor->setAscent(905.0); + descriptor->setDescent(-211.0); + + // Load with descriptor + auto pdfFont = pdfengine::fonts::pdf::PdfFontLoader::loadTrueTypeFromMemory("Arial-Bold", buffer, std::move(descriptor)); + ASSERT_NE(pdfFont, nullptr); + EXPECT_EQ(pdfFont->getBaseFont(), "Arial-Bold"); + EXPECT_TRUE(pdfFont->isEmbedded()); + + const auto* retrievedDesc = pdfFont->getDescriptor(); + ASSERT_NE(retrievedDesc, nullptr); + EXPECT_EQ(retrievedDesc->getFontName(), "Arial-BoldMT"); + EXPECT_EQ(retrievedDesc->getFlags(), 96); + EXPECT_TRUE(retrievedDesc->isItalic()); + EXPECT_TRUE(retrievedDesc->isNonsymbolic()); + EXPECT_DOUBLE_EQ(retrievedDesc->getAscent(), 905.0); + EXPECT_DOUBLE_EQ(retrievedDesc->getDescent(), -211.0); +} + } // namespace pdfengine::fonts