FreeType + HarfBuzz wrappers + LRU glyph cache

This commit is contained in:
saqib mir
2026-05-22 15:48:57 +05:30
parent d4fe33b007
commit 77b59d4432
22 changed files with 1421 additions and 103 deletions
+7 -2
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
#include "fonts/cache/glyph_bitmap.hpp"
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <vector>
namespace pdfengine::fonts {
struct GlyphBitmap {
std::vector<unsigned char> 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
+86
View File
@@ -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<GlyphBitmap> 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<double>(hits_) / total;
}
void GlyphCache::resetStats() {
hits_ = 0;
misses_ = 0;
}
} // namespace pdfengine::fonts
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include "fonts/face/font_face.hpp"
#include "fonts/cache/glyph_bitmap.hpp"
#include <optional>
#include <unordered_map>
#include <list>
#include <cstddef>
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<void*>{}(static_cast<void*>(key.face));
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
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<GlyphBitmap> 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<GlyphCacheKey> lru_list_;
using CacheIterator = std::list<GlyphCacheKey>::iterator;
std::unordered_map<
GlyphCacheKey,
std::pair<GlyphBitmap, CacheIterator>,
GlyphCacheKeyHash
> cache_map_;
};
} // namespace pdfengine::fonts
+158
View File
@@ -0,0 +1,158 @@
#include "fonts/face/font_face.hpp"
#include <iostream>
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<uint8_t>& 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<FT_Long>(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<GlyphBitmap> 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<int>(bitmap.width);
glyph_bitmap.height = static_cast<int>(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<double>(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
@@ -1,6 +1,10 @@
#pragma once
#include "fonts/cache/glyph_bitmap.hpp"
#include <optional>
#include <string>
#include <vector>
#include <cstdint>
#include <ft2build.h>
#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<uint8_t>& data);
FT_Face getFace() 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_;
FT_Face face_;
std::vector<uint8_t> font_data_; // Keeps the loaded memory buffer alive for FT_Face
};
} // namespace pdfengine::fonts
} // namespace pdfengine::fonts
-75
View File
@@ -1,75 +0,0 @@
#include "font_face.hpp"
#include <iostream>
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
View File
View File
View File
View File
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "fonts/face/font_face.hpp"
#include <string>
#include <memory>
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
@@ -0,0 +1,175 @@
#include "fonts/pdf/pdf_font_descriptor.hpp"
#include <sstream>
#include <stdexcept>
#include <algorithm>
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<double> 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<int>(nums[0]);
font_bbox_.lly = static_cast<int>(nums[1]);
font_bbox_.urx = static_cast<int>(nums[2]);
font_bbox_.ury = static_cast<int>(nums[3]);
}
} catch (const std::exception&) {
return false; // parsing or range exception
}
}
return true;
}
} // namespace pdfengine::fonts::pdf
@@ -0,0 +1,100 @@
#pragma once
#include <string>
#include <vector>
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
+18
View File
@@ -0,0 +1,18 @@
#include "fonts/pdf/pdf_font_loader.hpp"
#include "fonts/pdf/truetype_font.hpp"
namespace pdfengine::fonts::pdf {
std::unique_ptr<PdfFont> PdfFontLoader::loadTrueTypeFromMemory(
const std::string& baseFont,
const std::vector<uint8_t>& streamData,
std::unique_ptr<PdfFontDescriptor> descriptor
) {
auto font = std::make_unique<TrueTypeFont>(baseFont, true, std::move(descriptor));
if (!font->loadFromStream(streamData)) {
return nullptr;
}
return font;
}
} // namespace pdfengine::fonts::pdf
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "fonts/pdf/pdf_font.hpp"
#include "fonts/pdf/pdf_font_descriptor.hpp"
#include <memory>
#include <vector>
#include <cstdint>
namespace pdfengine::fonts::pdf {
class PdfFontLoader {
public:
// Factory method to load an embedded TrueType font from its raw stream bytes
static std::unique_ptr<PdfFont> loadTrueTypeFromMemory(
const std::string& baseFont,
const std::vector<uint8_t>& streamData,
std::unique_ptr<PdfFontDescriptor> descriptor = nullptr
);
};
} // namespace pdfengine::fonts::pdf
+41
View File
@@ -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<PdfFontDescriptor> descriptor
) : base_font_(baseFont),
is_embedded_(isEmbedded),
descriptor_(std::move(descriptor)) {}
bool TrueTypeFont::loadFromStream(const std::vector<uint8_t>& 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
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "fonts/pdf/pdf_font.hpp"
#include "fonts/pdf/pdf_font_descriptor.hpp"
#include <vector>
#include <cstdint>
#include <memory>
namespace pdfengine::fonts::pdf {
class TrueTypeFont : public PdfFont {
public:
TrueTypeFont(
const std::string& baseFont,
bool isEmbedded,
std::unique_ptr<PdfFontDescriptor> descriptor = nullptr
);
~TrueTypeFont() override = default;
// Load the font from raw embedded stream bytes
bool loadFromStream(const std::vector<uint8_t>& 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<PdfFontDescriptor> descriptor_;
};
} // namespace pdfengine::fonts::pdf
@@ -1,4 +1,4 @@
#include "hb_shaper.hpp"
#include "fonts/shaping/hb_shaper.hpp"
#include <hb.h>
#include <hb-ft.h>
@@ -8,24 +8,31 @@ namespace pdfengine::fonts {
HbShaper::HbShaper() = default;
HbShaper::~HbShaper() = default;
std::vector<ShapedGlyph> HbShaper::shapeText(const FontFace& fontFace, const std::string& text) {
std::vector<ShapedGlyph> HbShaper::shapeRun(
const std::string& text,
FontFace& font,
unsigned int fontSize
) {
std::vector<ShapedGlyph> 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<ShapedGlyph> 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<double>(glyphPositions[i].x_advance) / 64.0;
sg.yAdvance = static_cast<double>(glyphPositions[i].y_advance) / 64.0;
sg.xOffset = static_cast<double>(glyphPositions[i].x_offset) / 64.0;
sg.yOffset = static_cast<double>(glyphPositions[i].y_offset) / 64.0;
sg.advanceX = static_cast<double>(glyphPositions[i].x_advance) / 64.0;
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;
result.push_back(sg);
}
}
@@ -1,17 +1,18 @@
#pragma once
#include "font_face.hpp"
#include "fonts/face/font_face.hpp"
#include <string>
#include <vector>
#include <cstdint>
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<ShapedGlyph> shapeText(const FontFace& fontFace, const std::string& text);
std::vector<ShapedGlyph> shapeRun(
const std::string& text,
FontFace& font,
unsigned int fontSize
);
};
} // namespace pdfengine::fonts
+603 -6
View File
@@ -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 <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
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<const char*>(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<size_t>(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<unsigned char>(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<std::string> 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<size_t>(renderedOpt->width * renderedOpt->height));
}
auto hitBmp = cache.get(face, sg.glyphIndex, fontSize);
ASSERT_TRUE(hitBmp.has_value());
EXPECT_EQ(hitBmp->pixels.size(), static_cast<size_t>(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<int>(slot->bitmap.width));
EXPECT_EQ(renderedOpt->height, static_cast<int>(slot->bitmap.rows));
EXPECT_EQ(renderedOpt->bearingX, slot->bitmap_left);
EXPECT_EQ(renderedOpt->bearingY, slot->bitmap_top);
EXPECT_DOUBLE_EQ(renderedOpt->advance, static_cast<double>(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<GlyphBitmap> 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<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(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<uint8_t> emptyData;
EXPECT_FALSE(face.loadFromMemory(emptyData));
EXPECT_EQ(face.getFace(), nullptr);
// Corrupt garbage data
std::vector<uint8_t> 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<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(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<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
// Create descriptor
auto descriptor = std::make_unique<pdfengine::fonts::pdf::PdfFontDescriptor>();
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