#include "document_test_helpers.hpp" namespace pdfengine { TEST(FontDiagnosticsTest, IntrospectionAccuracy) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto page = *pageRes; auto fontsRes = page->getFonts(); ASSERT_TRUE(fontsRes.has_value()); auto fonts = *fontsRes; if (!fonts.empty()) { auto firstFont = fonts[0]; EXPECT_FALSE(firstFont.fontName.empty()); EXPECT_FALSE(firstFont.type.empty()); EXPECT_FALSE(firstFont.normalizedFamily.empty()); EXPECT_FALSE(firstFont.internalFontId.empty()); } auto docFontsRes = doc->getFonts(); ASSERT_TRUE(docFontsRes.has_value()); auto docFonts = *docFontsRes; EXPECT_EQ(docFonts.size(), fonts.size()); } TEST(FontDiagnosticsTest, SubsetAndVerticalTextIntrospection) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "vertical_text.pdf"); if (!std::filesystem::exists(path)) { path = getCorpusPath("fonts", "utf-8.pdf"); } if (!std::filesystem::exists(path)) { GTEST_SKIP() << "vertical_text.pdf or utf-8.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes = doc->getFonts(); ASSERT_TRUE(fontsRes.has_value()); for (const auto& f : *fontsRes) { if (f.isSubset) { EXPECT_FALSE(f.subsetTag.empty()); EXPECT_EQ(f.subsetTag.size(), 6); } if (f.isVertical) { EXPECT_TRUE(f.isVertical); EXPECT_NE(f.encoding.find("Identity-V"), std::string::npos); } } } TEST(FontDiagnosticsTest, CacheInvalidationAfterEdits) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes1 = doc->getFonts(); ASSERT_TRUE(fontsRes1.has_value()); std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_test_2", "type": "text_overlay", "pageIndex": 0, "data": { "text": "IntrospectionDiagnosticsNewText", "x": 10.0, "y": 20.0, "width": 200.0, "height": 20.0, "fontSize": 12.0, "fontFamily": "Helvetica", "color": "#000000" } } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto fontsRes2 = doc->getFonts(); ASSERT_TRUE(fontsRes2.has_value()); } TEST(FontDiagnosticsTest, ConcurrencyThreadSafety) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; std::vector threads; for (int i = 0; i < 8; ++i) { threads.emplace_back([&doc]() { auto res = doc->getFonts(); ASSERT_TRUE(res.has_value()); }); } for (auto& t : threads) { t.join(); } } TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { SKIP_IF_NO_PDFIUM(); { auto path = getCorpusPath("fonts", "utf-8.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes = doc->getFonts(); ASSERT_TRUE(fontsRes.has_value()); auto fonts = *fontsRes; for (const auto& f : fonts) { EXPECT_FALSE(f.fontName.empty()); EXPECT_FALSE(f.type.empty()); EXPECT_FALSE(f.normalizedFamily.empty()); EXPECT_FALSE(f.internalFontId.empty()); if (f.isSubset) { EXPECT_EQ(f.subsetTag.size(), 6); for (char c : f.subsetTag) { EXPECT_TRUE(std::isupper(static_cast(c))); } EXPECT_EQ(f.sourceType, "Embedded"); EXPECT_TRUE(f.isEmbedded); EXPECT_EQ(f.internalFontId, f.fontName); } else { EXPECT_TRUE(f.subsetTag.empty()); EXPECT_EQ(f.internalFontId, f.fontName + "_" + f.type + "_" + std::to_string(f.flags)); } if (f.sourceType == "SystemFallback") { EXPECT_FALSE(f.isEmbedded); EXPECT_TRUE(f.substitutedFrom.empty()); EXPECT_TRUE(f.substitutedTo.empty()); } else if (f.sourceType == "Substituted") { EXPECT_FALSE(f.isEmbedded); EXPECT_EQ(f.substitutedFrom, f.fontName); #if defined(_WIN32) EXPECT_EQ(f.substitutedTo, "Arial"); #else EXPECT_EQ(f.substitutedTo, "Liberation Sans"); #endif } EXPECT_GT(f.ascent, 0.0); EXPECT_LT(f.descent, 0.0); EXPECT_GT(f.capHeight, 0.0); } } } { auto path = getCorpusPath("fonts", "vertical_text.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes = doc->getFonts(); ASSERT_TRUE(fontsRes.has_value()); bool foundVertical = false; for (const auto& f : *fontsRes) { if (f.isVertical) { foundVertical = true; EXPECT_TRUE(f.encoding.find("-V") != std::string::npos || f.cmapName.find("-V") != std::string::npos); } } EXPECT_TRUE(foundVertical); } } { auto path1 = getCorpusPath("fonts", "vertical_identity_v.pdf"); if (std::filesystem::exists(path1)) { auto docRes = PdfDocument::loadFromFile(path1.string()); ASSERT_TRUE(docRes.has_value()); auto fontsRes = (*docRes)->getFonts(); ASSERT_TRUE(fontsRes.has_value()); bool foundVertical = false; for (const auto& f : *fontsRes) { if (f.isVertical) foundVertical = true; } EXPECT_TRUE(foundVertical) << "Failed to detect vertical font in vertical_identity_v.pdf"; } } { auto path = getCorpusPath("fonts", "utf-8.pdf"); if (!std::filesystem::exists(path)) { path = getCorpusPath("basic", "hello_world.pdf"); } if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto page = *pageRes; auto boundsRes = page->extractTextWithBounds(); ASSERT_TRUE(boundsRes.has_value()); const auto& glyphs = *boundsRes; ASSERT_FALSE(glyphs.empty()); std::vector uniqueSizes; for (const auto& glyph : glyphs) { if (glyph.text != " " && glyph.text != "\r" && glyph.text != "\n" && glyph.text != "\t") { EXPECT_GT(glyph.w, 0.0); EXPECT_GT(glyph.h, 0.0); } EXPECT_GT(glyph.fontSize, 0.0); EXPECT_LT(glyph.fontSize, 100.0); if (std::find(uniqueSizes.begin(), uniqueSizes.end(), glyph.fontSize) == uniqueSizes.end()) { uniqueSizes.push_back(glyph.fontSize); } } if (path.filename().string() == "utf-8.pdf") { EXPECT_GE(uniqueSizes.size(), 2u); } } } } TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "text_font.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "text_font.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()) << "Failed to open text_font.pdf"; auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto fontsRes = (*pageRes)->getFonts(); ASSERT_TRUE(fontsRes.has_value()); const auto& fonts = *fontsRes; ASSERT_FALSE(fonts.empty()) << "text_font.pdf must expose at least one font"; for (const auto& f : fonts) { EXPECT_FALSE(f.fontName.empty()); EXPECT_FALSE(f.type.empty()); static const std::vector kValidTypes = { "Type1", "TrueType", "CIDFontType0", "CIDFontType2" }; bool typeValid = std::find(kValidTypes.begin(), kValidTypes.end(), f.type) != kValidTypes.end(); EXPECT_TRUE(typeValid) << "Unexpected type '" << f.type << "' for font '" << f.fontName << "'"; if (f.isSubset) { EXPECT_TRUE(f.isEmbedded) << "Subset font '" << f.fontName << "' must be embedded (FPDFFont_GetIsEmbedded should return 1)"; EXPECT_EQ(f.sourceType, "Embedded") << "sourceType must be 'Embedded' when isEmbedded=true"; EXPECT_TRUE(f.substitutedFrom.empty()); EXPECT_TRUE(f.substitutedTo.empty()); } if (f.isEmbedded) { EXPECT_EQ(f.sourceType, "Embedded"); } } } TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "text_font.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "text_font.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto fontsRes = (*docRes)->getFonts(); ASSERT_TRUE(fontsRes.has_value()); for (const auto& f : *fontsRes) { EXPECT_GT(f.ascent, 0.0) << "ascent must be positive for font '" << f.fontName << "'"; EXPECT_LT(f.descent, 0.0) << "descent must be negative for font '" << f.fontName << "'"; EXPECT_GT(f.capHeight, 0.0) << "capHeight must be positive for font '" << f.fontName << "'"; EXPECT_LE(f.capHeight, f.ascent + 1.0) << "capHeight should not exceed ascent for font '" << f.fontName << "'"; EXPECT_LT(f.ascent, 1500.0) << "Implausibly large ascent for '" << f.fontName << "'"; EXPECT_GT(f.descent, -1500.0) << "Implausibly deep descent for '" << f.fontName << "'"; } } TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) { SKIP_IF_NO_PDFIUM(); { auto path = getCorpusPath("fonts", "with_tounicode.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); if (docRes.has_value()) { auto pageRes = (*docRes)->getPage(0); if (pageRes.has_value()) { auto fontsRes = (*pageRes)->getFonts(); if (fontsRes.has_value() && !fontsRes->empty()) { bool anyTrue = false; for (const auto& f : *fontsRes) { if (f.hasToUnicode) { anyTrue = true; break; } } EXPECT_TRUE(anyTrue) << "At least one font in with_tounicode.pdf must have hasToUnicode=true"; } } } } } { auto path = getCorpusPath("fonts", "no_tounicode.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); if (docRes.has_value()) { auto pageRes = (*docRes)->getPage(0); if (pageRes.has_value()) { auto fontsRes = (*pageRes)->getFonts(); if (fontsRes.has_value() && !fontsRes->empty()) { for (const auto& f : *fontsRes) { EXPECT_FALSE(f.hasToUnicode) << "Font '" << f.fontName << "' in no_tounicode.pdf must have hasToUnicode=false"; } } } } } } { auto path = getCorpusPath("fonts", "latin_extended.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); if (docRes.has_value()) { auto pageRes = (*docRes)->getPage(0); if (pageRes.has_value()) { auto fontsRes = (*pageRes)->getFonts(); if (fontsRes.has_value() && !fontsRes->empty()) { bool anyTrue = false; for (const auto& f : *fontsRes) { if (f.hasToUnicode) { anyTrue = true; break; } } EXPECT_TRUE(anyTrue) << "At least one font in latin_extended.pdf must decode to Unicode"; } } } } } } TEST(GlyphCacheTest, ConcurrencyBench) { using namespace pdfengine::fonts; FontFace face; bool loaded = face.loadFromFile("C:\\Windows\\Fonts\\arial.ttf"); if (!loaded) { GTEST_SKIP() << "Skipping benchmark: Arial font not found."; } GlyphCache cache(1000); auto run_benchmark = [&](int num_threads, int ops_per_thread) { std::atomic start_flag{0}; std::vector threads; for (int i = 0; i < num_threads; ++i) { threads.emplace_back([&, i]() { while (start_flag.load() == 0) { std::this_thread::yield(); } for (int op = 0; op < ops_per_thread; ++op) { unsigned int glyphIndex = (op + i) % 2000; unsigned int fontSize = 12 + (op % 5); auto hit = cache.get(face, glyphIndex, fontSize); if (!hit) { GlyphBitmap bmp; bmp.width = 10; bmp.height = 10; cache.insert(face, glyphIndex, fontSize, bmp); } } }); } auto start_time = std::chrono::high_resolution_clock::now(); start_flag.store(1); for (auto& t : threads) { t.join(); } auto end_time = std::chrono::high_resolution_clock::now(); std::chrono::duration diff = end_time - start_time; return diff.count(); }; run_benchmark(2, 1000); cache.clear(); double time_10 = run_benchmark(10, 10000); std::cout << "[ BENCHMARK ] 10 Threads Time: " << time_10 << " seconds (" << (100000.0 / time_10) << " ops/sec)\n"; cache.clear(); double time_50 = run_benchmark(50, 10000); std::cout << "[ BENCHMARK ] 50 Threads Time: " << time_50 << " seconds (" << (500000.0 / time_50) << " ops/sec)\n"; EXPECT_LE(cache.size(), 1000 + 16); } TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) { SKIP_IF_NO_PDFIUM(); std::vector testFiles = { "text_font.pdf", "embedded_truetype.pdf", "embedded_cid_font.pdf", "subset_font.pdf", "latin_extended.pdf" }; bool foundAnyEmbedded = false; for (const auto& fileName : testFiles) { auto path = getCorpusPath("fonts", fileName); if (!std::filesystem::exists(path)) { continue; } std::cout << "\n========================================\n"; std::cout << "Testing PDF: " << fileName << "\n"; std::cout << "========================================\n"; auto docRes = PdfDocument::loadFromFile(path.string()); if (!docRes.has_value()) { std::cout << "Failed to load document: " << fileName << std::endl; continue; } auto doc = *docRes; auto fontsRes = doc->getFonts(); if (!fontsRes.has_value()) { std::cout << "Failed to get fonts for: " << fileName << std::endl; continue; } const auto& fonts = *fontsRes; for (const auto& fontInfo : fonts) { std::cout << "Font: " << fontInfo.fontName << ", type: " << fontInfo.type << ", isEmbedded: " << (fontInfo.isEmbedded ? "yes" : "no") << ", flags: " << fontInfo.flags << std::endl; if (fontInfo.isEmbedded) { foundAnyEmbedded = true; auto resolvedFontRes = doc->getResolvedFont(fontInfo); if (!resolvedFontRes.has_value()) { std::cout << " Failed to resolve font: " << resolvedFontRes.error() << std::endl; continue; } auto resolvedFont = *resolvedFontRes; std::cout << " Resolved font successfully." << std::endl; auto face = static_cast(resolvedFont->getFontFace().getFace()); if (face) { std::cout << " FreeType Face Num Glyphs: " << face->num_glyphs << std::endl; std::cout << " FreeType Charmaps Count: " << face->num_charmaps << std::endl; for (int i = 0; i < face->num_charmaps; ++i) { FT_CharMap cm = face->charmaps[i]; std::cout << " Charmap " << i << ": platform_id=" << cm->platform_id << ", encoding_id=" << cm->encoding_id << std::endl; FT_Error err = FT_Set_Charmap(face, cm); if (err) { std::cout << " FT_Set_Charmap failed: " << err << std::endl; continue; } FT_UInt gindex; FT_ULong charcode = FT_Get_First_Char(face, &gindex); std::cout << " Mapped characters under charmap " << i << ": "; int count = 0; while (gindex != 0 && count < 10) { std::cout << charcode << "->" << gindex << " "; charcode = FT_Get_Next_Char(face, charcode, &gindex); count++; } std::cout << std::endl; } if (face->num_charmaps > 0) { FT_Set_Charmap(face, face->charmaps[0]); } std::cout << " Glyph names: "; for (int i = 0; i < face->num_glyphs; ++i) { char nameBuf[64] = {0}; if (FT_Get_Glyph_Name(face, i, nameBuf, sizeof(nameBuf)) == 0) { std::cout << i << ":" << nameBuf << " "; } else { std::cout << i << ":[unknown] "; } } std::cout << std::endl; } else { std::cout << " No FreeType Face available." << std::endl; } EXPECT_TRUE(resolvedFont->isEmbedded()); std::vector testChars = {32, 48, 65, 97}; for (uint32_t cp : testChars) { bool hasG = resolvedFont->hasGlyph(cp); double w = resolvedFont->getAdvanceWidth(cp, 12.0); std::cout << " char(" << cp << "): hasGlyph=" << (hasG ? "yes" : "no") << ", advanceWidth=" << w << std::endl; } auto metrics = resolvedFont->getMetrics(12.0); std::cout << " Metrics: ascent=" << metrics.ascent << ", descent=" << metrics.descent << ", capHeight=" << metrics.capHeight << std::endl; EXPECT_NE(metrics.ascent, 0.0); EXPECT_NE(metrics.descent, 0.0); EXPECT_NE(metrics.capHeight, 0.0); if (fileName == "text_font.pdf") { EXPECT_TRUE(resolvedFont->hasGlyph(1)); double w = resolvedFont->getAdvanceWidth(1, 12.0); EXPECT_GT(w, 0.0); std::cout << " [VERIFIED] text_font.pdf char(1): hasGlyph=yes, advanceWidth=" << w << std::endl; } if (face->num_glyphs > 1) { bool foundNonZeroWidth = false; for (int gid = 1; gid < face->num_glyphs; ++gid) { FT_Error err = FT_Load_Glyph(face, gid, FT_LOAD_DEFAULT); if (err == 0) { double directWidth = static_cast(face->glyph->advance.x) / 64.0; if (directWidth > 0.0) { foundNonZeroWidth = true; std::cout << " [VERIFIED] Direct glyph " << gid << " load: advanceWidth=" << directWidth << std::endl; break; } } } EXPECT_TRUE(foundNonZeroWidth) << "Expected to find at least one glyph with a non-zero advance width"; } } } } EXPECT_TRUE(foundAnyEmbedded) << "Expected to find at least one embedded font in test files"; } }