Files
pdf/engine/tests/fonts_test.cpp
T

729 lines
23 KiB
C++
Raw Normal View History

#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
std::vector<std::string> paths = {
"C:\\Windows\\Fonts\\arial.ttf",
"C:\\Windows\\Fonts\\consola.ttf",
"C:\\Windows\\Fonts\\tahoma.ttf"
};
#elif defined(__APPLE__)
// Common macOS fonts
std::vector<std::string> paths = {
"/Library/Fonts/Arial.ttf",
"/System/Library/Fonts/Geneva.ttf",
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Supplemental/Arial.ttf"
};
#else
// Common Linux fonts
std::vector<std::string> paths = {
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf"
};
#endif
for (const auto& path : paths) {
if (std::filesystem::exists(path)) {
return path;
}
}
return "";
}
} // namespace
namespace pdfengine::fonts {
TEST(FontTest, FontFaceInitialization) {
FontFace face;
EXPECT_EQ(face.getFace(), nullptr);
}
TEST(FontTest, FontFaceLoadNonExistentFile) {
FontFace face;
EXPECT_FALSE(face.loadFromFile("this_file_does_not_exist_12345.ttf"));
EXPECT_EQ(face.getFace(), nullptr);
}
TEST(FontTest, FontFaceMoveSemantics) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run move semantics test.";
}
FontFace face1;
ASSERT_TRUE(face1.loadFromFile(fontPath));
FT_Face rawFace = face1.getFace();
ASSERT_NE(rawFace, nullptr);
// Move construction
FontFace face2(std::move(face1));
EXPECT_EQ(face1.getFace(), nullptr);
EXPECT_EQ(face2.getFace(), rawFace);
// Move assignment
FontFace face3;
face3 = std::move(face2);
EXPECT_EQ(face2.getFace(), nullptr);
EXPECT_EQ(face3.getFace(), rawFace);
}
TEST(FontTest, HbShaperEmptyInput) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run empty input shaper test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
HbShaper shaper;
auto glyphs = shaper.shapeRun("", face, 16);
EXPECT_TRUE(glyphs.empty());
}
TEST(FontTest, HbShaperNullFace) {
FontFace face; // Null face
HbShaper shaper;
auto glyphs = shaper.shapeRun("Hello", face, 16);
EXPECT_TRUE(glyphs.empty());
}
TEST(FontTest, HbShaperShapeTextSuccess) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
std::cout << "[ WARNING ] Skipping shape success test: no system font found." << std::endl;
GTEST_SKIP() << "No system font found to run text shaping test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
ASSERT_NE(face.getFace(), nullptr);
HbShaper shaper;
std::string testText = "Hello World!";
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),
// but for simple English it's usually 1:1.
EXPECT_FALSE(glyphs.empty());
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.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