FreeType + HarfBuzz wrappers + LRU glyph cache
This commit is contained in:
@@ -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
@@ -0,0 +1 @@
|
||||
#include "fonts/cache/glyph_bitmap.hpp"
|
||||
+16
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user