#include "fonts/face/font_face.hpp" #include "fonts/shaping/hb_shaper.hpp" #include "fonts/cache/glyph_cache.hpp" #include "fonts/pdf_fonts/font.hpp" #include "fonts/pdf_fonts/types/truetype_font.hpp" #include "fonts/pdf_fonts/types/type1_font.hpp" #include "fonts/pdf_fonts/types/cid_font.hpp" #include "fonts/pdf_fonts/font_loader.hpp" #include "fonts/pdf_fonts/encoding/encoding.hpp" #include "fonts/pdf_fonts/encoding/cjk_collection_db.hpp" #include "fonts/pdf_fonts/font_fallback.hpp" #include "fonts/pdf_fonts/font_subset.hpp" #include #include #include #include #include #include namespace { bool saveGlyphAsPGM(const pdfengine::fonts::GlyphBitmap& bitmap, const std::string& filename) { if (bitmap.width == 0 || bitmap.height == 0 || bitmap.pixels.empty()) { return false; } std::ofstream out(filename, std::ios::binary); if (!out) { return false; } out << "P5\n" << bitmap.width << " " << bitmap.height << "\n255\n"; out.write(reinterpret_cast(bitmap.pixels.data()), bitmap.pixels.size()); return true; } std::string getSystemFontPath() { #if defined(_WIN32) // Common Windows fonts std::vector paths = { "C:\\Windows\\Fonts\\arial.ttf", "C:\\Windows\\Fonts\\consola.ttf", "C:\\Windows\\Fonts\\tahoma.ttf" }; #elif defined(__APPLE__) // Common macOS fonts std::vector 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 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(glyph.width * glyph.height)); EXPECT_GT(glyph.advance, 0.0); } TEST(FontTest, GlyphCacheBasicGetInsert) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run cache test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); GlyphCache cache(10); EXPECT_EQ(cache.capacity(), 10u); EXPECT_EQ(cache.size(), 0u); // Initial check: cache miss auto miss = cache.get(face, 12, 16); EXPECT_FALSE(miss.has_value()); // Create a dummy GlyphBitmap GlyphBitmap bitmap; bitmap.width = 10; bitmap.height = 12; bitmap.pixels = std::vector(120, 255); bitmap.advance = 8.5; // Insert cache.insert(face, 12, 16, bitmap); EXPECT_EQ(cache.size(), 1u); // Cache hit auto hit = cache.get(face, 12, 16); ASSERT_TRUE(hit.has_value()); EXPECT_EQ(hit->width, 10); EXPECT_EQ(hit->height, 12); EXPECT_EQ(hit->advance, 8.5); EXPECT_EQ(hit->pixels.size(), 120u); } TEST(FontTest, GlyphCacheEvictionPolicy) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run cache eviction test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); // Capacity 2 GlyphCache cache(2); GlyphBitmap bmp1{ .width = 1 }; GlyphBitmap bmp2{ .width = 2 }; GlyphBitmap bmp3{ .width = 3 }; cache.insert(face, 1, 16, bmp1); cache.insert(face, 2, 16, bmp2); EXPECT_EQ(cache.size(), 2u); // Insert third one: should evict the oldest (1, 16) cache.insert(face, 3, 16, bmp3); EXPECT_EQ(cache.size(), 2u); EXPECT_FALSE(cache.get(face, 1, 16).has_value()); EXPECT_TRUE(cache.get(face, 2, 16).has_value()); EXPECT_TRUE(cache.get(face, 3, 16).has_value()); } TEST(FontTest, GlyphCacheLRUPolicy) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run cache LRU test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); GlyphCache cache(2); GlyphBitmap bmp1{ .width = 1 }; GlyphBitmap bmp2{ .width = 2 }; GlyphBitmap bmp3{ .width = 3 }; cache.insert(face, 1, 16, bmp1); cache.insert(face, 2, 16, bmp2); // Access 1 to make it most recently used auto hit = cache.get(face, 1, 16); ASSERT_TRUE(hit.has_value()); // Insert 3: since 2 is the oldest (least recently used), 2 should be evicted and 1 should remain cache.insert(face, 3, 16, bmp3); EXPECT_EQ(cache.size(), 2u); EXPECT_TRUE(cache.get(face, 1, 16).has_value()); EXPECT_FALSE(cache.get(face, 2, 16).has_value()); EXPECT_TRUE(cache.get(face, 3, 16).has_value()); } TEST(FontTest, FontPipelineIntegration) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run pipeline integration test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); HbShaper shaper; GlyphCache cache(100); std::vector testTexts = {"Hello World", "office", "سلام"}; unsigned int fontSize = 16; for (const auto& text : testTexts) { auto shapedGlyphs = shaper.shapeRun(text, face, fontSize); EXPECT_FALSE(shapedGlyphs.empty()); for (const auto& sg : shapedGlyphs) { auto cachedBmp = cache.get(face, sg.glyphIndex, fontSize); if (!cachedBmp.has_value()) { auto renderedOpt = face.renderGlyph(sg.glyphIndex, fontSize); ASSERT_TRUE(renderedOpt.has_value()); cache.insert(face, sg.glyphIndex, fontSize, *renderedOpt); EXPECT_EQ(renderedOpt->pixels.size(), static_cast(renderedOpt->width * renderedOpt->height)); } auto hitBmp = cache.get(face, sg.glyphIndex, fontSize); ASSERT_TRUE(hitBmp.has_value()); EXPECT_EQ(hitBmp->pixels.size(), static_cast(hitBmp->width * hitBmp->height)); EXPECT_GE(hitBmp->advance, 0.0); } } } TEST(FontTest, UnicodeAndRtlShaping) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run Unicode and RTL shaping test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); HbShaper shaper; unsigned int fontSize = 16; // Test A: Arabic (RTL) - "سلام" { std::string arabicText = "سلام"; auto glyphs = shaper.shapeRun(arabicText, face, fontSize); EXPECT_FALSE(glyphs.empty()); for (const auto& g : glyphs) { // Validate that shaping executed and returned valid layout metrics EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.offsetX != 0.0 || g.offsetY != 0.0 || g.glyphIndex != 999999u); } } // Test B: Hindi - "नमस्ते" { std::string hindiText = "नमस्ते"; auto glyphs = shaper.shapeRun(hindiText, face, fontSize); EXPECT_FALSE(glyphs.empty()); for (const auto& g : glyphs) { EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u); } } // Test C: Ligatures - "office" { std::string ligatureText = "office"; auto glyphs = shaper.shapeRun(ligatureText, face, fontSize); EXPECT_FALSE(glyphs.empty()); EXPECT_LE(glyphs.size(), ligatureText.length()); for (const auto& g : glyphs) { EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u); } } } TEST(FontTest, CachePerformanceTest) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run cache performance test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); HbShaper shaper; GlyphCache cache(100); std::string text = "Hello Hello Hello Hello"; unsigned int fontSize = 16; { auto glyphs = shaper.shapeRun(text, face, fontSize); for (const auto& g : glyphs) { auto cached = cache.get(face, g.glyphIndex, fontSize); if (!cached.has_value()) { auto rendered = face.renderGlyph(g.glyphIndex, fontSize); if (rendered.has_value()) { cache.insert(face, g.glyphIndex, fontSize, *rendered); } } } } cache.resetStats(); for (int i = 0; i < 1000; ++i) { auto glyphs = shaper.shapeRun(text, face, fontSize); for (const auto& g : glyphs) { auto cached = cache.get(face, g.glyphIndex, fontSize); if (!cached.has_value()) { auto rendered = face.renderGlyph(g.glyphIndex, fontSize); if (rendered.has_value()) { cache.insert(face, g.glyphIndex, fontSize, *rendered); } } } } double hitRateVal = cache.hitRate(); std::cout << "[ INFO ] Cache Hit Rate for repetitive text: " << (hitRateVal * 100.0) << "%" << std::endl; EXPECT_GT(hitRateVal, 0.90); } TEST(FontTest, EngineStressTest) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run engine stress test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); HbShaper shaper; GlyphCache cache(32); std::string base = "The quick brown fox jumps over the lazy dog. 1234567890!@#$%^&*() "; std::string longText; longText.reserve(10000); while (longText.length() < 10000) { longText += base; } unsigned int fontSize = 16; auto glyphs = shaper.shapeRun(longText, face, fontSize); EXPECT_FALSE(glyphs.empty()); for (const auto& g : glyphs) { auto cached = cache.get(face, g.glyphIndex, fontSize); if (!cached.has_value()) { auto rendered = face.renderGlyph(g.glyphIndex, fontSize); if (rendered.has_value()) { cache.insert(face, g.glyphIndex, fontSize, *rendered); } } } EXPECT_LE(cache.size(), cache.capacity()); EXPECT_GT(cache.size(), 0u); } TEST(FontTest, VisualBitmapDebugging) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run visual bitmap debugging test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A'); ASSERT_GT(glyphIndex, 0u); auto renderedOpt = face.renderGlyph(glyphIndex, 48); ASSERT_TRUE(renderedOpt.has_value()); std::string filename = "A.pgm"; std::filesystem::remove(filename); ASSERT_TRUE(saveGlyphAsPGM(*renderedOpt, filename)); EXPECT_TRUE(std::filesystem::exists(filename)); EXPECT_GT(std::filesystem::file_size(filename), 0u); } TEST(FontTest, MetricsValidation) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run metrics validation test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); FT_Face ftFace = face.getFace(); ASSERT_NE(ftFace, nullptr); unsigned int glyphIndex = FT_Get_Char_Index(ftFace, 'B'); ASSERT_GT(glyphIndex, 0u); unsigned int fontSize = 24; auto renderedOpt = face.renderGlyph(glyphIndex, fontSize); ASSERT_TRUE(renderedOpt.has_value()); ASSERT_EQ(FT_Set_Pixel_Sizes(ftFace, 0, fontSize), 0); ASSERT_EQ(FT_Load_Glyph(ftFace, glyphIndex, FT_LOAD_RENDER), 0); FT_GlyphSlot slot = ftFace->glyph; EXPECT_EQ(renderedOpt->width, static_cast(slot->bitmap.width)); EXPECT_EQ(renderedOpt->height, static_cast(slot->bitmap.rows)); EXPECT_EQ(renderedOpt->bearingX, slot->bitmap_left); EXPECT_EQ(renderedOpt->bearingY, slot->bitmap_top); EXPECT_DOUBLE_EQ(renderedOpt->advance, static_cast(slot->advance.x) / 64.0); } TEST(FontTest, CacheRecencyStress) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run cache recency stress test."; } FontFace face; ASSERT_TRUE(face.loadFromFile(fontPath)); GlyphCache cache(5); std::vector bmps; for (int i = 0; i < 10; ++i) { GlyphBitmap b; b.width = i; bmps.push_back(b); } for (unsigned int i = 0; i < 5; ++i) { cache.insert(face, i, 16, bmps[i]); } EXPECT_EQ(cache.size(), 5u); ASSERT_TRUE(cache.get(face, 0, 16).has_value()); ASSERT_TRUE(cache.get(face, 2, 16).has_value()); cache.insert(face, 5, 16, bmps[5]); // 1 should be evicted because it was the oldest EXPECT_FALSE(cache.get(face, 1, 16).has_value()); // 5 was just inserted, should be at the front EXPECT_TRUE(cache.get(face, 5, 16).has_value()); // Access 3 to promote it to the front ASSERT_TRUE(cache.get(face, 3, 16).has_value()); // Insert 6. With cache.get lookups, 4 is now the oldest (since 3, 5, 2, 0 have been looked up recently) cache.insert(face, 6, 16, bmps[6]); // 4 should be evicted EXPECT_FALSE(cache.get(face, 4, 16).has_value()); // The rest should remain EXPECT_TRUE(cache.get(face, 0, 16).has_value()); EXPECT_TRUE(cache.get(face, 2, 16).has_value()); EXPECT_TRUE(cache.get(face, 3, 16).has_value()); EXPECT_TRUE(cache.get(face, 5, 16).has_value()); EXPECT_TRUE(cache.get(face, 6, 16).has_value()); } TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run load from memory success test."; } // Read the entire file into a buffer std::ifstream file(fontPath, std::ios::binary | std::ios::ate); ASSERT_TRUE(file.is_open()); std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); // Load from memory FontFace face; ASSERT_TRUE(face.loadFromMemory(buffer)); ASSERT_NE(face.getFace(), nullptr); // Validate that glyph rendering and metrics are valid unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'M'); ASSERT_GT(glyphIndex, 0u); auto glyph = face.renderGlyph(glyphIndex, 16); ASSERT_TRUE(glyph.has_value()); EXPECT_GT(glyph->width, 0); EXPECT_GT(glyph->height, 0); EXPECT_GT(glyph->advance, 0.0); } TEST(FontLoaderTest, FontFaceLoadFromMemoryInvalid) { FontFace face; // Empty vector std::vector emptyData; EXPECT_FALSE(face.loadFromMemory(emptyData)); EXPECT_EQ(face.getFace(), nullptr); // Corrupt garbage data std::vector corruptData = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; EXPECT_FALSE(face.loadFromMemory(corruptData)); EXPECT_EQ(face.getFace(), nullptr); } TEST(FontLoaderTest, FontLoaderTrueTypeSuccess) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run PDF font loader test."; } std::ifstream file(fontPath, std::ios::binary | std::ios::ate); ASSERT_TRUE(file.is_open()); std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); // Use factory loader auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial", buffer); ASSERT_NE(pdfFont, nullptr); EXPECT_EQ(pdfFont->getBaseFont(), "Arial"); EXPECT_EQ(pdfFont->getType(), pdfengine::fonts::pdf_fonts::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(FontDescriptorTest, DescriptorDefaultValues) { pdfengine::fonts::pdf_fonts::FontDescriptor 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_fonts::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(FontDescriptorTest, 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_fonts::FontDescriptor desc; ASSERT_TRUE(desc.parseFromDictionaryString(dict)); EXPECT_EQ(desc.getFontName(), "ArialMT"); EXPECT_EQ(desc.getFlags(), 32); pdfengine::fonts::pdf_fonts::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(FontDescriptorTest, DescriptorParsingMalformed) { pdfengine::fonts::pdf_fonts::FontDescriptor 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(FontDescriptorTest, FontLoaderWithDescriptor) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run PDF font loader test."; } std::ifstream file(fontPath, std::ios::binary | std::ios::ate); ASSERT_TRUE(file.is_open()); std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); // Create descriptor auto descriptor = std::make_unique(); descriptor->setFontName("Arial-BoldMT"); descriptor->setFlags(96); // Nonsymbolic (32) | Italic (64) descriptor->setAscent(905.0); descriptor->setDescent(-211.0); // Load with descriptor auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::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); } TEST(EncodingTest, PredefinedEncodingTest) { using namespace pdfengine::fonts::pdf_fonts; // WinAnsiEncoding PredefinedEncoding winAnsi(SimpleEncodingType::WinAnsi); EXPECT_EQ(winAnsi.getType(), SimpleEncodingType::WinAnsi); EXPECT_EQ(winAnsi.decode(65), 65); // 'A' EXPECT_EQ(winAnsi.decode(128), 0x20AC); // Euro symbol exception EXPECT_EQ(winAnsi.decode(169), 169); // copyright symbol (standard ISO-8859-1) EXPECT_EQ(winAnsi.decode(300), 0); // Out of bounds // MacRomanEncoding PredefinedEncoding macRoman(SimpleEncodingType::MacRoman); EXPECT_EQ(macRoman.getType(), SimpleEncodingType::MacRoman); EXPECT_EQ(macRoman.decode(65), 65); // 'A' EXPECT_EQ(macRoman.decode(128), 0x00C4); // High page lookup exception (A-dieresis) EXPECT_EQ(macRoman.decode(300), 0); // Out of bounds // Identity encoding (pass-through) PredefinedEncoding identity(SimpleEncodingType::Identity); EXPECT_EQ(identity.getType(), SimpleEncodingType::Identity); EXPECT_EQ(identity.decode(65), 65); EXPECT_EQ(identity.decode(128), 128); EXPECT_EQ(identity.decode(1000), 1000); // Beyond 255 pass-through } TEST(EncodingTest, CustomEncodingWithDifferences) { using namespace pdfengine::fonts::pdf_fonts; auto baseEncoding = std::make_unique(SimpleEncodingType::WinAnsi); CustomEncoding custom(std::move(baseEncoding)); // Fallback to base EXPECT_EQ(custom.decode(65), 65); // Standard glyph name difference mapping custom.addDifference(120, "quotesingle"); EXPECT_EQ(custom.decode(120), 0x0027); // Unicode-like glyph name (uniXXXX) difference mapping custom.addDifference(121, "uni0041"); EXPECT_EQ(custom.decode(121), 0x0041); // 'A' // uXXXX representation custom.addDifference(122, "u0042"); EXPECT_EQ(custom.decode(122), 0x0042); // 'B' // Non-existent or unresolved glyph name custom.addDifference(123, "nonexistentglyphname123"); EXPECT_EQ(custom.decode(123), 123); // Falls back to base (WinAnsi maps 123 to 123 '{') } TEST(EncodingTest, ToUnicodeCMapbfchar) { using namespace pdfengine::fonts::pdf_fonts; ToUnicodeCMap cmap; std::string cmapStream = "/CIDInit /ProcSet findresource begin\n" "12 dict begin\n" "begincmap\n" "/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n" "/CMapName /Custom-ToUnicode def\n" "1 begincodespacerange\n" "<0000> \n" "endcodespacerange\n" "2 beginbfchar\n" "<0001> <0041>\n" "<0002> <0042>\n" "endbfchar\n" "endcmap\n" "CMapName currentdict /CMap defineresource pop\n" "end\n" "end\n"; ASSERT_TRUE(cmap.parseCMapStream(cmapStream)); EXPECT_EQ(cmap.decode(1), 0x0041); // 'A' EXPECT_EQ(cmap.decode(2), 0x0042); // 'B' EXPECT_EQ(cmap.decode(3), 0); // Missing } TEST(EncodingTest, ToUnicodeCMapbfrange) { using namespace pdfengine::fonts::pdf_fonts; ToUnicodeCMap cmap; std::string cmapStream = "begincmap\n" "2 beginbfrange\n" "<0001> <0005> <0041>\n" // Sequential base mapping (<0001> to <0005> starting at <0041>) "<0010> <0012> [<0061> <0062> <0063>]\n" // Array mapping "endbfrange\n" "endcmap\n"; ASSERT_TRUE(cmap.parseCMapStream(cmapStream)); // Assert sequential EXPECT_EQ(cmap.decode(1), 0x0041); // 'A' EXPECT_EQ(cmap.decode(3), 0x0043); // 'C' EXPECT_EQ(cmap.decode(5), 0x0045); // 'E' // Assert array EXPECT_EQ(cmap.decode(0x10), 0x0061); // 'a' EXPECT_EQ(cmap.decode(0x11), 0x0062); // 'b' EXPECT_EQ(cmap.decode(0x12), 0x0063); // 'c' } TEST(EncodingTest, ToUnicodeMalformedCMap) { using namespace pdfengine::fonts::pdf_fonts; ToUnicodeCMap cmap; // Completely invalid/garbage content std::string garbageStream = "This is a garbage string with no valid CMap elements"; EXPECT_FALSE(cmap.parseCMapStream(garbageStream)); // Partially valid CMap - should recover parsed entries std::string partialStream = "begincmap\n" "beginbfchar\n" "<0001> <0041>\n" // Valid "<0002> /invalid\n" // Invalid/missing dest (non-hex) "<0003> <0043>\n" // Valid "endbfchar\n" "endcmap\n"; EXPECT_TRUE(cmap.parseCMapStream(partialStream)); EXPECT_EQ(cmap.decode(1), 0x0041); EXPECT_EQ(cmap.decode(2), 0); EXPECT_EQ(cmap.decode(3), 0x0043); } TEST(EncodingTest, FontLoaderWithEncoding) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run PDF font loader with encoding test."; } std::ifstream file(fontPath, std::ios::binary | std::ios::ate); ASSERT_TRUE(file.is_open()); std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); // Create predefined encoding (WinAnsi) auto encoding = std::make_unique( pdfengine::fonts::pdf_fonts::SimpleEncodingType::WinAnsi ); // Load font with encoding auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory( "Arial-With-Encoding", buffer, nullptr, std::move(encoding) ); ASSERT_NE(pdfFont, nullptr); EXPECT_EQ(pdfFont->getBaseFont(), "Arial-With-Encoding"); const auto* retrievedEncoding = pdfFont->getEncoding(); ASSERT_NE(retrievedEncoding, nullptr); // Verify it translates Euro symbol exception properly EXPECT_EQ(retrievedEncoding->decode(128), 0x20AC); } TEST(Type1FontTest, EmbeddedType1FontLoading) { std::string fontPath = getSystemFontPath(); if (fontPath.empty()) { GTEST_SKIP() << "No system font found to run embedded Type1 test."; } std::ifstream file(fontPath, std::ios::binary | std::ios::ate); ASSERT_TRUE(file.is_open()); std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(size); ASSERT_TRUE(file.read(reinterpret_cast(buffer.data()), size)); auto font = pdfengine::fonts::pdf_fonts::FontLoader::loadType1FromMemory("LegacyType1", buffer); ASSERT_NE(font, nullptr); EXPECT_EQ(font->getBaseFont(), "LegacyType1"); EXPECT_EQ(font->getType(), pdfengine::fonts::pdf_fonts::FontType::Type1); EXPECT_TRUE(font->isEmbedded()); } TEST(Type1FontTest, NonEmbeddedType1SystemFallback) { auto fontHelv = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Helvetica"); ASSERT_NE(fontHelv, nullptr); EXPECT_EQ(fontHelv->getBaseFont(), "Helvetica"); EXPECT_EQ(fontHelv->getType(), pdfengine::fonts::pdf_fonts::FontType::Type1); EXPECT_FALSE(fontHelv->isEmbedded()); auto fontTimes = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Times-BoldItalic"); ASSERT_NE(fontTimes, nullptr); EXPECT_EQ(fontTimes->getBaseFont(), "Times-BoldItalic"); EXPECT_FALSE(fontTimes->isEmbedded()); auto fontCourier = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Courier-Oblique"); ASSERT_NE(fontCourier, nullptr); EXPECT_EQ(fontCourier->getBaseFont(), "Courier-Oblique"); EXPECT_FALSE(fontCourier->isEmbedded()); auto fontUnknown = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("UnknownLegacyFont"); ASSERT_NE(fontUnknown, nullptr); EXPECT_EQ(fontUnknown->getBaseFont(), "UnknownLegacyFont"); EXPECT_FALSE(fontUnknown->isEmbedded()); } TEST(CIDFontTest, CompositeFontInitializationAndTypes) { using namespace pdfengine::fonts::pdf_fonts; auto desc = std::make_unique(); desc->setFontName("SimSun-Descriptor"); // Test CIDFontType0 CIDFont font0("SimSun", FontType::CIDFontType0, false, std::move(desc)); EXPECT_EQ(font0.getBaseFont(), "SimSun"); EXPECT_EQ(font0.getType(), FontType::CIDFontType0); EXPECT_FALSE(font0.isEmbedded()); EXPECT_EQ(font0.getDescriptor()->getFontName(), "SimSun-Descriptor"); // Test CIDFontType2 CIDFont font2("MS-Gothic", FontType::CIDFontType2, true); EXPECT_EQ(font2.getBaseFont(), "MS-Gothic"); EXPECT_EQ(font2.getType(), FontType::CIDFontType2); EXPECT_TRUE(font2.isEmbedded()); } TEST(CIDFontTest, CIDToGIDTranslations) { using namespace pdfengine::fonts::pdf_fonts; CIDFont font("SimSun", FontType::CIDFontType2, false); // By default, it should be an identity mapping EXPECT_TRUE(font.isIdentityMap()); EXPECT_EQ(font.mapCIDToGID(100), 100); EXPECT_EQ(font.mapCIDToGID(5000), 5000); // Set custom mapping std::unordered_map customMap = { {10, 100}, {20, 200}, {30, 300} }; font.setCIDToGIDMap(customMap); EXPECT_FALSE(font.isIdentityMap()); EXPECT_EQ(font.mapCIDToGID(10), 100); EXPECT_EQ(font.mapCIDToGID(20), 200); EXPECT_EQ(font.mapCIDToGID(30), 300); EXPECT_EQ(font.mapCIDToGID(40), 0); // Undefined / missing // Set back to identity font.setIdentityCIDToGIDMap(); EXPECT_TRUE(font.isIdentityMap()); EXPECT_EQ(font.mapCIDToGID(10), 10); } TEST(CIDFontTest, NonEmbeddedCIDFontSystemFallback) { using namespace pdfengine::fonts::pdf_fonts; // Load Chinese Simplified CJK fallback auto fontSimSun = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2); ASSERT_NE(fontSimSun, nullptr); EXPECT_EQ(fontSimSun->getBaseFont(), "SimSun"); EXPECT_EQ(fontSimSun->getType(), FontType::CIDFontType2); EXPECT_FALSE(fontSimSun->isEmbedded()); // Load Japanese CJK fallback auto fontGothic = FontLoader::loadCIDFontSystemFallback("HeiseiMin-W3", FontType::CIDFontType0); ASSERT_NE(fontGothic, nullptr); EXPECT_EQ(fontGothic->getBaseFont(), "HeiseiMin-W3"); EXPECT_EQ(fontGothic->getType(), FontType::CIDFontType0); EXPECT_FALSE(fontGothic->isEmbedded()); // Verify it resolved to a valid system font that can render and shape text // E.g. we can shape a basic CJK run with SimSun or MS Gothic (like Japanese characters) HbShaper shaper; auto glyphs = shaper.shapeRun("日本語漢字", fontGothic->getFontFace(), 16); EXPECT_FALSE(glyphs.empty()); } TEST(FontFallbackTest, SingletonInstanceIsUnique) { using namespace pdfengine::fonts::pdf_fonts; auto& instance1 = FontFallback::getInstance(); auto& instance2 = FontFallback::getInstance(); EXPECT_EQ(&instance1, &instance2); } TEST(FontFallbackTest, StandardFontFallbacksOnWindows) { using namespace pdfengine::fonts::pdf_fonts; auto& fallback = FontFallback::getInstance(); // Test Helvetica to Arial std::string path1 = fallback.getFallbackFontPath("Helvetica"); EXPECT_FALSE(path1.empty()); EXPECT_TRUE(std::filesystem::exists(path1)); EXPECT_TRUE(path1.find("arial") != std::string::npos || path1.find("ARIAL") != std::string::npos); // Test Times to Times New Roman std::string path2 = fallback.getFallbackFontPath("Times-Roman"); EXPECT_FALSE(path2.empty()); EXPECT_TRUE(std::filesystem::exists(path2)); EXPECT_TRUE(path2.find("times") != std::string::npos || path2.find("TIMES") != std::string::npos); } TEST(FontFallbackTest, StyleModifierResolutions) { using namespace pdfengine::fonts::pdf_fonts; auto& fallback = FontFallback::getInstance(); // Bold Helvetica should map to Arial Bold std::string pathBold = fallback.getFallbackFontPath("Helvetica", true, false); EXPECT_TRUE(pathBold.find("arialbd") != std::string::npos); // Bold Italic Times should map to Times New Roman Bold Italic std::string pathBoldItalic = fallback.getFallbackFontPath("Times", true, true); EXPECT_TRUE(pathBoldItalic.find("timesbi") != std::string::npos); } TEST(FontFallbackTest, CustomFallbackRegistration) { using namespace pdfengine::fonts::pdf_fonts; auto& fallback = FontFallback::getInstance(); fallback.resetToDefaults(); // Lookup standard Arial path std::string standardPath = fallback.getFallbackFontPath("Helvetica"); // Register custom override for "helvetica" pointing to times.ttf fallback.registerFallback("helvetica", "C:\\Windows\\Fonts\\times.ttf"); std::string overridenPath = fallback.getFallbackFontPath("Helvetica"); EXPECT_EQ(overridenPath, "C:\\Windows\\Fonts\\times.ttf"); // Reset back to defaults fallback.resetToDefaults(); std::string restoredPath = fallback.getFallbackFontPath("Helvetica"); EXPECT_EQ(restoredPath, standardPath); } TEST(FontSubsetTest, SubsetTagParsingAndStripping) { using namespace pdfengine::fonts::pdf_fonts; std::string subsetName = "KTJHQO+Arial"; EXPECT_TRUE(FontSubset::hasSubsetPrefix(subsetName)); EXPECT_EQ(FontSubset::getSubsetPrefix(subsetName), "KTJHQO"); EXPECT_EQ(FontSubset::stripSubsetPrefix(subsetName), "Arial"); // Standard naming (no prefix) std::string normalName = "Arial"; EXPECT_FALSE(FontSubset::hasSubsetPrefix(normalName)); EXPECT_EQ(FontSubset::getSubsetPrefix(normalName), ""); EXPECT_EQ(FontSubset::stripSubsetPrefix(normalName), "Arial"); } TEST(FontSubsetTest, PrefixFormatValidation) { using namespace pdfengine::fonts::pdf_fonts; // Prefixes must be exactly 6 UPPERCASE letters followed by '+' EXPECT_TRUE(FontSubset::hasSubsetPrefix("ABCDEF+Helvetica")); // Lowercase should fail EXPECT_FALSE(FontSubset::hasSubsetPrefix("abcDEF+Helvetica")); EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCdef+Helvetica")); // Numbers/Special should fail EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABC123+Helvetica")); EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE_+Helvetica")); // Length must be exactly 6 characters EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica")); // 5 chars EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica")); // 7 chars } TEST(FontSubsetTest, GIDRemappingTranslations) { using namespace pdfengine::fonts::pdf_fonts; FontSubset subset("KTJHQO+Arial"); EXPECT_TRUE(subset.isSubset()); EXPECT_EQ(subset.getFullFontName(), "KTJHQO+Arial"); EXPECT_EQ(subset.getBaseFontName(), "Arial"); EXPECT_EQ(subset.getPrefix(), "KTJHQO"); // Default pass-through lookup EXPECT_EQ(subset.mapSubsetToOriginal(5), 5); EXPECT_FALSE(subset.hasGlyphMapping(5)); // Register glyph ID translations subset.addGlyphMapping(1, 41); // 'A' subset.addGlyphMapping(2, 42); // 'B' subset.addGlyphMapping(3, 43); // 'C' EXPECT_EQ(subset.getMappingCount(), 3u); EXPECT_TRUE(subset.hasGlyphMapping(1)); EXPECT_TRUE(subset.hasGlyphMapping(2)); EXPECT_EQ(subset.mapSubsetToOriginal(1), 41); EXPECT_EQ(subset.mapSubsetToOriginal(2), 42); EXPECT_EQ(subset.mapSubsetToOriginal(3), 43); EXPECT_EQ(subset.mapSubsetToOriginal(4), 4); // Falls back to standard GID } TEST(FontSubsetTest, CoreFontSubsettingIntegration) { using namespace pdfengine::fonts::pdf_fonts; // Test TrueType Font integration TrueTypeFont fontTT("KTJHQO+Arial", false); const auto* ttSubset = fontTT.getSubsetInfo(); ASSERT_NE(ttSubset, nullptr); EXPECT_TRUE(ttSubset->isSubset()); EXPECT_EQ(ttSubset->getBaseFontName(), "Arial"); EXPECT_EQ(ttSubset->getPrefix(), "KTJHQO"); // Test Type1 Font integration Type1Font fontT1("SUBSET+Courier", false); const auto* t1Subset = fontT1.getSubsetInfo(); ASSERT_NE(t1Subset, nullptr); EXPECT_TRUE(t1Subset->isSubset()); EXPECT_EQ(t1Subset->getBaseFontName(), "Courier"); EXPECT_EQ(t1Subset->getPrefix(), "SUBSET"); // Test CID Font integration CIDFont fontCID("CJKTAG+SimSun", FontType::CIDFontType2, false); const auto* cidSubset = fontCID.getSubsetInfo(); ASSERT_NE(cidSubset, nullptr); EXPECT_TRUE(cidSubset->isSubset()); EXPECT_EQ(cidSubset->getBaseFontName(), "SimSun"); EXPECT_EQ(cidSubset->getPrefix(), "CJKTAG"); } TEST(TextExtractionLayerTest, SimpleCharacterDecoding) { using namespace pdfengine::fonts::pdf_fonts; auto font = FontLoader::loadType1SystemFallback("Helvetica"); ASSERT_NE(font, nullptr); EXPECT_EQ(font->decodeToUnicode(65), 65u); EXPECT_EQ(font->decodeToUnicode(97), 97u); EXPECT_EQ(font->decodeToUnicode(48), 48u); } TEST(TextExtractionLayerTest, EncodingAndToUnicodeDecoding) { using namespace pdfengine::fonts::pdf_fonts; auto baseEncoding = std::make_unique(SimpleEncodingType::WinAnsi); auto customEnc = std::make_unique(std::move(baseEncoding)); customEnc->addDifference(128, "euro"); auto font = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(customEnc)); ASSERT_NE(font, nullptr); EXPECT_EQ(font->decodeToUnicode(128), 0x20ACu); auto cmap = std::make_unique(); cmap->addMapping(1, 0x0041); cmap->addMapping(2, 0x0042); cmap->addMapping(3, 0x20AC); auto fontCMap = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(cmap)); ASSERT_NE(fontCMap, nullptr); EXPECT_EQ(fontCMap->decodeToUnicode(1), 0x0041u); EXPECT_EQ(fontCMap->decodeToUnicode(2), 0x0042u); EXPECT_EQ(fontCMap->decodeToUnicode(3), 0x20ACu); } TEST(TextExtractionLayerTest, SubsetFontTextExtraction) { using namespace pdfengine::fonts::pdf_fonts; auto font = FontLoader::loadType1SystemFallback("KTJHQO+Helvetica"); ASSERT_NE(font, nullptr); const auto* subsetConst = font->getSubsetInfo(); ASSERT_NE(subsetConst, nullptr); auto* subset = const_cast(subsetConst); FT_Face face = font->getFontFace().getFace(); ASSERT_NE(face, nullptr); FT_UInt originalGid = FT_Get_Char_Index(face, 'A'); ASSERT_GT(originalGid, 0u); subset->addGlyphMapping(5, originalGid); EXPECT_EQ(font->decodeToUnicode(5), 65u); } TEST(TextExtractionLayerTest, CIDFontTextExtraction) { using namespace pdfengine::fonts::pdf_fonts; auto font = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2); ASSERT_NE(font, nullptr); CIDFont* cidFont = dynamic_cast(font.get()); ASSERT_NE(cidFont, nullptr); FT_Face face = cidFont->getFontFace().getFace(); ASSERT_NE(face, nullptr); FT_UInt gid65 = 65; FT_ULong expectedChar65 = 0; FT_UInt gindex; FT_ULong charcode = FT_Get_First_Char(face, &gindex); while (gindex != 0) { if (gindex == gid65) { expectedChar65 = charcode; break; } charcode = FT_Get_Next_Char(face, charcode, &gindex); } if (expectedChar65 != 0) { EXPECT_EQ(cidFont->decodeToUnicode(65), expectedChar65); } else { EXPECT_EQ(cidFont->decodeToUnicode(65), 65u); } FT_UInt gidA = FT_Get_Char_Index(face, 'A'); if (gidA > 0) { std::unordered_map cidToGid = { {500, gidA} }; cidFont->setCIDToGIDMap(cidToGid); EXPECT_EQ(cidFont->decodeToUnicode(500), 65u); } } TEST(TextExtractionLayerTest, StringUtf8Conversion) { using namespace pdfengine::fonts::pdf_fonts; auto font = FontLoader::loadType1SystemFallback("Helvetica"); ASSERT_NE(font, nullptr); std::vector codes = {65, 66, 67}; EXPECT_EQ(font->decodeStringToUnicode(codes), "ABC"); auto cmap = std::make_unique(); cmap->addMapping(10, 0x65E5); cmap->addMapping(11, 0x672C); cmap->addMapping(12, 0x8A9E); auto fontCMap = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(cmap)); ASSERT_NE(fontCMap, nullptr); std::vector cjkCodes = {10, 11, 12}; std::string decodedCjk = fontCMap->decodeStringToUnicode(cjkCodes); EXPECT_EQ(decodedCjk, "日本語"); } TEST(FontSubstitutionAndWidthsTest, WidthMatchingAndSubstitutionVerification) { using namespace pdfengine::fonts::pdf_fonts; // Load non-embedded Helvetica font, which triggers substitution auto font = FontLoader::loadType1SystemFallback("Helvetica"); ASSERT_NE(font, nullptr); // Confirm that the font does not have widths set yet EXPECT_FALSE(font->hasWidths()); // Original widths for character codes 65 to 68 ('A' to 'D') from PDF /Widths array // E.g., 'A'=600, 'B'=500, 'C'=550, 'D'=400 std::vector pdfWidths = { 600.0, 500.0, 550.0, 400.0 }; font->setWidths(65, 68, pdfWidths); EXPECT_TRUE(font->hasWidths()); // Font size context: 12.0 double fontSize = 12.0; // Expected widths = (W / 1000.0) * fontSize double expectedWidthA = (600.0 / 1000.0) * fontSize; // 7.2 double expectedWidthB = (500.0 / 1000.0) * fontSize; // 6.0 double expectedWidthC = (550.0 / 1000.0) * fontSize; // 6.6 double expectedWidthD = (400.0 / 1000.0) * fontSize; // 4.8 // Verify mapped widths match original PDF widths with 0% error (well under 5%) EXPECT_NEAR(font->getCharWidth(65, fontSize), expectedWidthA, 1e-5); EXPECT_NEAR(font->getCharWidth(66, fontSize), expectedWidthB, 1e-5); EXPECT_NEAR(font->getCharWidth(67, fontSize), expectedWidthC, 1e-5); EXPECT_NEAR(font->getCharWidth(68, fontSize), expectedWidthD, 1e-5); // Verify out-of-range character falls back safely (returns 0.0 or descriptor missing width) EXPECT_EQ(font->getCharWidth(999, fontSize), 0.0); } TEST(CIDAdvancedMappingTest, IdentityVSupport) { using namespace pdfengine::fonts::pdf_fonts; PredefinedEncoding identityV(SimpleEncodingType::Identity_V); EXPECT_EQ(identityV.getType(), SimpleEncodingType::Identity_V); EXPECT_EQ(identityV.decode(65), 65); EXPECT_EQ(identityV.decode(1000), 1000); } TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) { using namespace pdfengine::fonts::pdf_fonts; auto font = FontLoader::loadType1SystemFallback("Helvetica"); ASSERT_NE(font, nullptr); // Default vertical advance metrics: 1.0em = font size context double fontSize = 12.0; EXPECT_EQ(font->isVertical(), false); EXPECT_EQ(font->getCharHeight(65, fontSize), fontSize); // Turn vertical metrics ON and verify custom heights font->setVertical(true); EXPECT_EQ(font->isVertical(), true); std::vector verticalAdvances = { 1000.0, 800.0, 900.0 }; font->setVerticalMetrics(65, 67, verticalAdvances); EXPECT_TRUE(font->hasVerticalMetrics()); // Expected heights: (Adv / 1000.0) * fontSize EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5); // (1000/1000) * 12 EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5); // (800/1000) * 12 EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5); // (900/1000) * 12 EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5); // Fallback to 12.0 } TEST(CIDAdvancedMappingTest, CjkCollectionResolutionDB) { using namespace pdfengine::fonts::pdf_fonts; // Standard Adobe-Japan1 Hiragana CIDs EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041); // Hiragana 'ぁ' EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093); // Hiragana 'ん' // Standard Adobe-Japan1 Katakana CIDs EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1); // Katakana 'ァ' EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6); // Katakana 'ヶ' // Core Kanji EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00); // Kanji '一' // GB1 Chinese simplified ideographic marks EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000); // space EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001); // comma } TEST(CIDAdvancedMappingTest, HarfBuzzVerticalShapingSignature) { using namespace pdfengine::fonts; // Validate WritingMode configurations EXPECT_EQ(static_cast(HbShaper::WritingMode::Horizontal), 0); EXPECT_EQ(static_cast(HbShaper::WritingMode::Vertical), 1); } } // namespace pdfengine::fonts