Files
pdf/engine/tests/font_diagnostics_test.cpp
2026-06-29 10:51:04 +05:30

602 lines
22 KiB
C++

#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<std::thread> 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<unsigned char>(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<double> 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<std::string> 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<int> start_flag{0};
std::vector<std::thread> 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<double> 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<std::string> 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<FT_Face>(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<uint32_t> 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<double>(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";
}
}