This commit is contained in:
saqib mir
2026-06-22 15:24:05 +05:30
141 changed files with 554 additions and 1734 deletions
+1 -3
View File
@@ -32,7 +32,7 @@ const PathObject* requirePathObject(const std::unique_ptr<ContentObject>& object
return static_cast<const PathObject*>(object.get());
}
} // namespace
}
TEST(ContentBuilderTest, SimpleTextState) {
Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj");
@@ -67,7 +67,6 @@ TEST(ContentBuilderTest, KerningArrayTJ) {
EXPECT_EQ(objects[0]->getType(), ContentObjectType::Text);
auto* textObj = static_cast<TextObject*>(objects[0].get());
// -600 is less than -500, so it inserts a space
EXPECT_EQ(textObj->text, "Hello World");
}
@@ -202,7 +201,6 @@ TEST(ContentBuilderTest, IntegrationHelloWorld) {
auto ops = parser.parse();
auto objects = builder.build(ops);
// hello_world.pdf has two text lines: "Hello, world!" and "Goodbye, world!"
int textObjectCount = 0;
bool foundHello = false;
bool foundGoodbye = false;
-3
View File
@@ -5,7 +5,6 @@
using namespace pdfengine;
// A simple visitor for testing that just records the sequence of visited commands.
class MockVisitor : public CommandVisitor {
public:
std::vector<std::string> calls;
@@ -26,7 +25,6 @@ TEST(DisplayListTest, RecordAndReplay) {
EXPECT_EQ(list.size(), 0);
// Record some commands
list.saveState();
list.setTransform(Matrix(2.0f, 0.0f, 0.0f, 2.0f, 0.0f, 0.0f));
list.fillRect(10.0f, 10.0f, 100.0f, 50.0f);
@@ -35,7 +33,6 @@ TEST(DisplayListTest, RecordAndReplay) {
EXPECT_EQ(list.size(), 5);
// Replay to the mock visitor
MockVisitor visitor;
list.replay(visitor);
+20 -125
View File
@@ -264,7 +264,7 @@ TEST(PageRenderTest, InvalidPageIndexReturnsPageOutOfBounds) {
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(1); // Page index 1 is out of bounds for 1-page doc
auto pageRes = (*docRes)->getPage(1);
ASSERT_FALSE(pageRes.has_value());
EXPECT_EQ(pageRes.error(), EngineError::PageOutOfBounds);
@@ -447,7 +447,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) {
ASSERT_TRUE(docRes.has_value());
auto doc = *docRes;
// Verify text exists initially
{
auto pageRes = doc->getPage(0);
ASSERT_TRUE(pageRes.has_value());
@@ -456,7 +455,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) {
EXPECT_NE(textRes->find("Hello"), std::string::npos);
}
// Redact the entire page bounds to remove all objects
std::string editsJson = R"({
"version": "1.0",
"operations": [
@@ -494,7 +492,6 @@ TEST(DocumentEditTest, ApplyRedactionAndFullSave) {
auto textRes = newPage->extractText();
ASSERT_TRUE(textRes.has_value());
// The text should be completely gone
EXPECT_EQ(textRes->find("Hello"), std::string::npos);
EXPECT_EQ(textRes->find("world"), std::string::npos);
}
@@ -562,7 +559,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
EXPECT_GT(origW, 0.0);
EXPECT_GT(origH, origW);
// 1. Rotate by 90 degrees (90 total)
std::string editsJson1 = R"({
"version": "1.0",
"operations": [
@@ -580,7 +576,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
auto editRes1 = doc->applyEdits(editsJson1);
ASSERT_TRUE(editRes1.has_value());
// 2. Rotate by another 90 degrees (180 total)
std::string editsJson2 = R"({
"version": "1.0",
"operations": [
@@ -598,7 +593,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
auto editRes2 = doc->applyEdits(editsJson2);
ASSERT_TRUE(editRes2.has_value());
// Save and load back to verify 180 degree rotation (dimensions should be original again)
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
@@ -617,7 +611,6 @@ TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) {
EXPECT_NEAR(rotatedW, origW, 0.01);
EXPECT_NEAR(rotatedH, origH, 0.01);
// 3. Now rotate by -90 degrees (back to 90 total)
std::string editsJson3 = R"({
"version": "1.0",
"operations": [
@@ -700,7 +693,6 @@ TEST(DocumentEditTest, ApplyPageReorderAndIncrementalSave) {
auto doc = *docRes;
EXPECT_EQ(doc->pageCount(), 2);
// Swap the pages: move page 1 (index 1) to page 0 (index 0)
std::string editsJson = R"({
"version": "1.0",
"operations": [
@@ -861,7 +853,6 @@ TEST(FontDiagnosticsTest, ConcurrencyThreadSafety) {
TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
SKIP_IF_NO_PDFIUM();
// Test Part 1: Introspection & Metadata verification using utf-8.pdf
{
auto path = getCorpusPath("fonts", "utf-8.pdf");
if (std::filesystem::exists(path)) {
@@ -874,13 +865,11 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
auto fonts = *fontsRes;
for (const auto& f : fonts) {
// Core fields
EXPECT_FALSE(f.fontName.empty());
EXPECT_FALSE(f.type.empty());
EXPECT_FALSE(f.normalizedFamily.empty());
EXPECT_FALSE(f.internalFontId.empty());
// Subset tagging consistency
if (f.isSubset) {
EXPECT_EQ(f.subsetTag.size(), 6);
for (char c : f.subsetTag) {
@@ -894,7 +883,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
EXPECT_EQ(f.internalFontId, f.fontName + "_" + f.type + "_" + std::to_string(f.flags));
}
// Source Type / Fallbacks & Substitutions consistency
if (f.sourceType == "SystemFallback") {
EXPECT_FALSE(f.isEmbedded);
EXPECT_TRUE(f.substitutedFrom.empty());
@@ -909,7 +897,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
#endif
}
// Check descriptor metrics are non-zero / reasonably set
EXPECT_GT(f.ascent, 0.0);
EXPECT_LT(f.descent, 0.0);
EXPECT_GT(f.capHeight, 0.0);
@@ -917,7 +904,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
}
}
// Test Part 2: Vertical Writing Mode Detection
{
auto path = getCorpusPath("fonts", "vertical_text.pdf");
if (std::filesystem::exists(path)) {
@@ -935,12 +921,10 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
EXPECT_TRUE(f.encoding.find("-V") != std::string::npos || f.cmapName.find("-V") != std::string::npos);
}
}
// Ensure at least one vertical font is found in vertical_text.pdf
EXPECT_TRUE(foundVertical);
}
}
// Test Part 2b: Vertical Font Detection Heuristic explicit validation
{
auto path1 = getCorpusPath("fonts", "vertical_identity_v.pdf");
if (std::filesystem::exists(path1)) {
@@ -957,7 +941,6 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
}
}
// Test Part 3: Font Size and Glyph Bounds Handling
{
auto path = getCorpusPath("fonts", "utf-8.pdf");
if (!std::filesystem::exists(path)) {
@@ -980,20 +963,18 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
std::vector<double> uniqueSizes;
for (const auto& glyph : glyphs) {
// Ensure glyph bounding box and font sizes are valid positive numbers
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); // No absurdly large font sizes
EXPECT_LT(glyph.fontSize, 100.0);
if (std::find(uniqueSizes.begin(), uniqueSizes.end(), glyph.fontSize) == uniqueSizes.end()) {
uniqueSizes.push_back(glyph.fontSize);
}
}
// If it's utf-8.pdf, it should have multiple distinct font sizes
if (path.filename().string() == "utf-8.pdf") {
EXPECT_GE(uniqueSizes.size(), 2u);
}
@@ -1001,23 +982,10 @@ TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) {
}
}
// =========================================================================
// Tests validating real PDFium font dictionary introspection.
// These tests verify that isEmbedded, type, ascent, descent, capHeight, and
// hasToUnicode are now derived from actual PDF font objects rather than from
// font-name heuristics (the behaviour that predated this change).
// =========================================================================
// Verify that embedded fonts report isEmbedded=true and that sourceType is
// set to "Embedded" from the real FPDFFont_GetIsEmbedded() result.
// A subset-embedded font (ABCDEF+FontName prefix) is the clearest case
// because the old heuristic relied solely on the prefix tag for embedding
// detection, while real PDFium checks for the /FontFile stream.
TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
SKIP_IF_NO_PDFIUM();
// text_font.pdf has an embedded subset TrueType font \u2014 best candidate for
// verifying that isEmbedded comes from the PDF font stream, not the name tag.
auto path = getCorpusPath("fonts", "text_font.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "text_font.pdf not found in corpus.";
@@ -1035,11 +1003,9 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
ASSERT_FALSE(fonts.empty()) << "text_font.pdf must expose at least one font";
for (const auto& f : fonts) {
// Core invariant: every font must have a non-empty name and type.
EXPECT_FALSE(f.fontName.empty());
EXPECT_FALSE(f.type.empty());
// type must be one of the four valid PDF font subtypes.
static const std::vector<std::string> kValidTypes = {
"Type1", "TrueType", "CIDFontType0", "CIDFontType2"
};
@@ -1047,8 +1013,6 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
!= kValidTypes.end();
EXPECT_TRUE(typeValid) << "Unexpected type '" << f.type << "' for font '" << f.fontName << "'";
// Subset-prefixed fonts MUST be reported as embedded by PDFium
// (the /FontFile stream is required by the PDF spec when a subset tag is present).
if (f.isSubset) {
EXPECT_TRUE(f.isEmbedded)
<< "Subset font '" << f.fontName
@@ -1059,21 +1023,15 @@ TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) {
EXPECT_TRUE(f.substitutedTo.empty());
}
// isEmbedded=true and sourceType="Embedded" must be consistent.
if (f.isEmbedded) {
EXPECT_EQ(f.sourceType, "Embedded");
}
}
}
// Verify that font descriptor metrics (ascent, descent, capHeight) come from
// the real PDF FontDescriptor via FPDFFont_GetAscent/Descent(), not from the
// hardcoded fallback table. The critical invariant is sign correctness:
// ascent must be positive, descent must be negative.
TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) {
SKIP_IF_NO_PDFIUM();
// Use the largest font corpus file; it contains the most diverse fonts.
auto path = getCorpusPath("fonts", "text_font.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "text_font.pdf not found in corpus.";
@@ -1085,9 +1043,6 @@ TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) {
ASSERT_TRUE(fontsRes.has_value());
for (const auto& f : *fontsRes) {
// ascent and descent from FPDFFont_GetAscent/Descent(font, 1000.0f, …)
// are in PDF 1000-unit space. ascent is above the baseline (positive),
// descent is below (negative).
EXPECT_GT(f.ascent, 0.0)
<< "ascent must be positive for font '" << f.fontName << "'";
EXPECT_LT(f.descent, 0.0)
@@ -1095,30 +1050,17 @@ TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) {
EXPECT_GT(f.capHeight, 0.0)
<< "capHeight must be positive for font '" << f.fontName << "'";
// capHeight must not exceed ascent (sanity: caps never taller than ascender).
EXPECT_LE(f.capHeight, f.ascent + 1.0) // +1 for float rounding
EXPECT_LE(f.capHeight, f.ascent + 1.0)
<< "capHeight should not exceed ascent for font '" << f.fontName << "'";
// Values must be in a plausible PDF 1000-unit-space range.
// Standard fonts typically have ascent in [400, 1200].
EXPECT_LT(f.ascent, 1500.0) << "Implausibly large ascent for '" << f.fontName << "'";
EXPECT_GT(f.descent, -1500.0) << "Implausibly deep descent for '" << f.fontName << "'";
}
}
// Verify that hasToUnicode reflects actual Unicode decode capability rather
// than the old always-true heuristic.
//
// with_tounicode.pdf \u2014 PDF containing a font that has a /ToUnicode stream;
// PDFium should decode characters successfully.
// no_tounicode.pdf \u2014 PDF containing a font with no /ToUnicode stream and no
// standard encoding; PDFium cannot map char codes to Unicode.
// latin_extended.pdf \u2014 Standard Latin font; must decode to Unicode via built-in
// encoding (WinAnsiEncoding or similar).
TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
SKIP_IF_NO_PDFIUM();
// Case 1: font WITH ToUnicode \u2014 hasToUnicode must be true
{
auto path = getCorpusPath("fonts", "with_tounicode.pdf");
if (std::filesystem::exists(path)) {
@@ -1140,7 +1082,6 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
}
}
// Case 2: font WITHOUT ToUnicode or decodable encoding \u2014 hasToUnicode must be false
{
auto path = getCorpusPath("fonts", "no_tounicode.pdf");
if (std::filesystem::exists(path)) {
@@ -1150,7 +1091,6 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
if (pageRes.has_value()) {
auto fontsRes = (*pageRes)->getFonts();
if (fontsRes.has_value() && !fontsRes->empty()) {
// All fonts in a no-tounicode document should fail Unicode decode.
for (const auto& f : *fontsRes) {
EXPECT_FALSE(f.hasToUnicode)
<< "Font '" << f.fontName
@@ -1162,7 +1102,6 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
}
}
// Case 3: standard Latin font \u2014 must decode to Unicode via built-in encoding
{
auto path = getCorpusPath("fonts", "latin_extended.pdf");
if (std::filesystem::exists(path)) {
@@ -1184,25 +1123,18 @@ TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) {
}
}
}
// =========================================================================
// Tests for UTF-16 Surrogate Pairs (Emoji and CJK Ext-B)
// =========================================================================
TEST(UtfConversionTest, EmojiSurrogatePairs) {
// 😀 U+1F600 -> UTF-8: F0 9F 98 80
std::string utf8_grinning = "\xF0\x9F\x98\x80";
auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_grinning);
// Should be D83D DE00 + null terminator
ASSERT_EQ(utf16.size(), 3);
EXPECT_EQ(utf16[0], 0xD83D);
EXPECT_EQ(utf16[1], 0xDE00);
EXPECT_EQ(utf16[2], 0x0000);
// Convert back to UTF-8
std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(utf8_out, utf8_grinning);
// 🚀 U+1F680 -> UTF-8: F0 9F 9A 80
std::string utf8_rocket = "\xF0\x9F\x9A\x80";
utf16 = pdfengine::parser::utf8_to_utf16le(utf8_rocket);
ASSERT_EQ(utf16.size(), 3);
@@ -1215,76 +1147,58 @@ TEST(UtfConversionTest, EmojiSurrogatePairs) {
}
TEST(UtfConversionTest, CJKExtensionB) {
// 𠀀 U+20000 -> UTF-8: F0 A0 80 80
std::string utf8_cjk = "\xF0\xA0\x80\x80";
auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_cjk);
// Should be D840 DC00 + null terminator
ASSERT_EQ(utf16.size(), 3);
EXPECT_EQ(utf16[0], 0xD840);
EXPECT_EQ(utf16[1], 0xDC00);
EXPECT_EQ(utf16[2], 0x0000);
// Convert back to UTF-8
std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(utf8_out, utf8_cjk);
}
TEST(UtfConversionTest, RoundtripMixed) {
// "A😀B𠀀C" -> 41 F0 9F 98 80 42 F0 A0 80 80 43
std::string mixed = "A\xF0\x9F\x98\x80""B\xF0\xA0\x80\x80""C";
auto utf16 = pdfengine::parser::utf8_to_utf16le(mixed);
std::string mixed_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast<const char16_t*>(utf16.data()), utf16.size() - 1);
EXPECT_EQ(mixed_out, mixed);
}
// =========================================================================
// Tests for CJK CID Resolution
// =========================================================================
TEST(CjkResolutionTest, AdobeCNS1) {
// Basic mapping checks for the core Adobe-CNS1 block (Traditional Chinese)
using pdfengine::fonts::pdf_fonts::CjkCollectionDB;
// Test existing block (100-130)
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 100), 0x4E00); // 一
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D); // 中
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 130), 0x4ED7); // 仗
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 100), 0x4E00);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 130), 0x4ED7);
// Test the newly expanded block (131-140)
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 131), 0x4ED8); // 付
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF); // 仟
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01); // 企
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 131), 0x4ED8);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01);
// Test alternative naming
EXPECT_EQ(CjkCollectionDB::resolveCID("Identity-H-CNS1", 137), 0x4EE3); // 代
EXPECT_EQ(CjkCollectionDB::resolveCID("Identity-H-CNS1", 137), 0x4EE3);
}
TEST(CjkResolutionTest, AdobeKorea1) {
using pdfengine::fonts::pdf_fonts::CjkCollectionDB;
// Test existing Korean block (101-150)
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 101), 0xAC00); // 가
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C); // 개
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 150), 0xAC90); // 겔
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 101), 0xAC00);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 150), 0xAC90);
// Test newly added Hangul block (151-160)
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 151), 0xAC94); // 겝
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9); // 결
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD); // 겼
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 151), 0xAC94);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD);
// Test alternative naming
EXPECT_EQ(CjkCollectionDB::resolveCID("UniKS-UTF16-H-Korea1", 153), 0xACA0); // 겠
EXPECT_EQ(CjkCollectionDB::resolveCID("UniKS-UTF16-H-Korea1", 153), 0xACA0);
}
// =========================================================================
// GlyphCache Concurrency & Benchmark Test
// =========================================================================
TEST(GlyphCacheTest, ConcurrencyBench) {
using namespace pdfengine::fonts;
FontFace face;
// Load a common system font for testing cache keys
bool loaded = face.loadFromFile("C:\\Windows\\Fonts\\arial.ttf");
if (!loaded) {
GTEST_SKIP() << "Skipping benchmark: Arial font not found.";
@@ -1323,7 +1237,7 @@ TEST(GlyphCacheTest, ConcurrencyBench) {
return diff.count();
};
run_benchmark(2, 1000); // warmup
run_benchmark(2, 1000);
cache.clear();
double time_10 = run_benchmark(10, 10000);
@@ -1334,7 +1248,7 @@ TEST(GlyphCacheTest, ConcurrencyBench) {
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); // Accommodate shard capacity rounding
EXPECT_LE(cache.size(), 1000 + 16);
}
TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
@@ -1415,12 +1329,10 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
std::cout << std::endl;
}
// Restore first charmap
if (face->num_charmaps > 0) {
FT_Set_Charmap(face, face->charmaps[0]);
}
// Print all glyph names in the face
std::cout << " Glyph names: ";
for (int i = 0; i < face->num_glyphs; ++i) {
char nameBuf[64] = {0};
@@ -1437,7 +1349,6 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
EXPECT_TRUE(resolvedFont->isEmbedded());
// Let's test a few common characters: 'A' (65), 'a' (97), '0' (48), ' ' (32)
std::vector<uint32_t> testChars = {32, 48, 65, 97};
for (uint32_t cp : testChars) {
bool hasG = resolvedFont->hasGlyph(cp);
@@ -1446,23 +1357,19 @@ TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
<< ", advanceWidth=" << w << std::endl;
}
// Verify metrics returned are non-zero/valid
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);
// Specific verification for text_font.pdf where we mapped charcode 1 -> GID 1
if (fileName == "text_font.pdf") {
// hasGlyph(1) should return true because the charmap maps 1 -> 1
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;
}
// Verify we can load glyphs directly by glyph index (0 to num_glyphs - 1)
if (face->num_glyphs > 1) {
bool foundNonZeroWidth = false;
for (int gid = 1; gid < face->num_glyphs; ++gid) {
@@ -1525,7 +1432,6 @@ TEST(DocumentEditTest, ReplaceTextMVPStandardFont) {
if (i + 1 < objectIndices.size()) indicesStr += ",";
}
// Flat format payload
std::string editsJson = R"({
"version": "1.0",
"operations": [
@@ -1604,7 +1510,6 @@ TEST(DocumentEditTest, ReplaceTextRuntimeFontEngine) {
if (i + 1 < objectIndices.size()) indicesStr += ",";
}
// JSON payload including internalFontId to resolve
std::string editsJson = R"({
"version": "1.0",
"operations": [
@@ -1659,7 +1564,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes;
// Find the first text run
std::vector<int> objectIndices;
std::string originalFontId = "";
for (const auto& p : model.paragraphs) {
@@ -1684,7 +1588,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
if (i + 1 < objectIndices.size()) indicesStr += ",";
}
// JSON payload containing replacement using system font embedding
std::string editsJson = R"({
"version": "1.0",
"operations": [
@@ -1702,7 +1605,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
// Save and reload
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
@@ -1712,7 +1614,6 @@ TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
ASSERT_TRUE(newDocRes.has_value());
auto newDoc = *newDocRes;
// Get page fonts to verify that Arial was successfully embedded in the new document
auto fontsRes = newDoc->getFonts(0, 0);
ASSERT_TRUE(fontsRes.has_value());
@@ -1745,7 +1646,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
ASSERT_TRUE(modelRes.has_value());
const auto& model = *modelRes;
// Find a line that has at least 2 runs, where the first run uses Roboto-Regular
std::vector<int> targetIndices;
std::string originalFontId = "";
std::string runBText = "";
@@ -1782,7 +1682,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
if (i + 1 < targetIndices.size()) indicesStr += ",";
}
// JSON payload: replacing runA with a very long text to trigger significant shift
std::string editsJson = R"({
"version": "1.0",
"operations": [
@@ -1800,7 +1699,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
auto editRes = doc->applyEdits(editsJson);
ASSERT_TRUE(editRes.has_value());
// Save and reload
auto saveRes = doc->saveIncremental();
ASSERT_TRUE(saveRes.has_value());
const auto& savedBytes = *saveRes;
@@ -1818,7 +1716,6 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
ASSERT_TRUE(newModelRes.has_value());
const auto& newModel = *newModelRes;
// Find runB in the new document model and verify its X coordinate has shifted to the right
bool foundRunB = false;
double runBNewX = 0.0;
for (const auto& p : newModel.paragraphs) {
@@ -1841,6 +1738,4 @@ TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
std::cout << "Reflow Engine verified: '" << runBText << "' shifted from X=" << runBOrigX << " to X=" << runBNewX << std::endl;
}
}
}
+62 -167
View File
@@ -22,9 +22,6 @@
namespace {
// Case-insensitive substring check. System font filenames differ in case
// across platforms (e.g. macOS ships "Times.ttc", Windows "times.ttf"), so the
// font-fallback assertions match without regard to case.
bool containsCI(const std::string& haystack, const std::string& needle) {
auto it = std::search(
haystack.begin(), haystack.end(), needle.begin(), needle.end(),
@@ -50,14 +47,12 @@ bool saveGlyphAsPGM(const pdfengine::fonts::GlyphBitmap& bitmap, const std::stri
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",
@@ -65,7 +60,6 @@ std::string getSystemFontPath() {
"/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",
@@ -81,7 +75,7 @@ std::string getSystemFontPath() {
return "";
}
} // namespace
}
namespace pdfengine::fonts {
@@ -107,12 +101,10 @@ TEST(FontTest, FontFaceMoveSemantics) {
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);
@@ -134,7 +126,7 @@ TEST(FontTest, HbShaperEmptyInput) {
}
TEST(FontTest, HbShaperNullFace) {
FontFace face; // Null face
FontFace face;
HbShaper shaper;
auto glyphs = shaper.shapeRun("Hello", face, 16);
EXPECT_TRUE(glyphs.empty());
@@ -155,20 +147,15 @@ TEST(FontTest, HbShaperShapeTextSuccess) {
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
FontFace face;
auto glyph = face.renderGlyph(0, 16);
EXPECT_FALSE(glyph.has_value());
}
@@ -183,11 +170,9 @@ TEST(FontTest, FontFaceRenderGlyphSuccess) {
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
ASSERT_GT(glyphIndex, 0u);
// Render it at 24px.
auto glyphOpt = face.renderGlyph(glyphIndex, 24);
ASSERT_TRUE(glyphOpt.has_value());
@@ -211,22 +196,18 @@ TEST(FontTest, GlyphCacheBasicGetInsert) {
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);
@@ -244,7 +225,6 @@ TEST(FontTest, GlyphCacheEvictionPolicy) {
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
// Capacity 2
GlyphCache cache(2);
GlyphBitmap bmp1{ .width = 1 };
@@ -255,7 +235,6 @@ TEST(FontTest, GlyphCacheEvictionPolicy) {
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);
@@ -282,11 +261,9 @@ TEST(FontTest, GlyphCacheLRUPolicy) {
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);
@@ -343,18 +320,15 @@ TEST(FontTest, UnicodeAndRtlShaping) {
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);
@@ -364,7 +338,6 @@ TEST(FontTest, UnicodeAndRtlShaping) {
}
}
// Test C: Ligatures - "office"
{
std::string ligatureText = "office";
auto glyphs = shaper.shapeRun(ligatureText, face, fontSize);
@@ -541,20 +514,14 @@ TEST(FontTest, CacheRecencyStress) {
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());
@@ -568,7 +535,6 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) {
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();
@@ -577,12 +543,10 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) {
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);
@@ -595,12 +559,10 @@ TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) {
TEST(FontLoaderTest, 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);
@@ -619,14 +581,12 @@ TEST(FontLoaderTest, FontLoaderTrueTypeSuccess) {
std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(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());
@@ -685,28 +645,22 @@ TEST(FontDescriptorTest, DescriptorParsingSuccess) {
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_TRUE(desc.isNonsymbolic());
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 >>"));
}
@@ -723,14 +677,12 @@ TEST(FontDescriptorTest, FontLoaderWithDescriptor) {
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_fonts::FontDescriptor>();
descriptor->setFontName("Arial-BoldMT");
descriptor->setFlags(96); // Nonsymbolic (32) | Italic (64)
descriptor->setFlags(96);
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");
@@ -749,27 +701,24 @@ TEST(FontDescriptorTest, FontLoaderWithDescriptor) {
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
EXPECT_EQ(winAnsi.decode(65), 65);
EXPECT_EQ(winAnsi.decode(128), 0x20AC);
EXPECT_EQ(winAnsi.decode(169), 169);
EXPECT_EQ(winAnsi.decode(300), 0);
// 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
EXPECT_EQ(macRoman.decode(65), 65);
EXPECT_EQ(macRoman.decode(128), 0x00C4);
EXPECT_EQ(macRoman.decode(300), 0);
// 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
EXPECT_EQ(identity.decode(1000), 1000);
}
TEST(EncodingTest, CustomEncodingWithDifferences) {
@@ -778,24 +727,19 @@ TEST(EncodingTest, CustomEncodingWithDifferences) {
auto baseEncoding = std::make_unique<PredefinedEncoding>(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'
EXPECT_EQ(custom.decode(121), 0x0041);
// uXXXX representation
custom.addDifference(122, "u0042");
EXPECT_EQ(custom.decode(122), 0x0042); // 'B'
EXPECT_EQ(custom.decode(122), 0x0042);
// 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 '{')
EXPECT_EQ(custom.decode(123), 123);
}
TEST(EncodingTest, ToUnicodeCMapbfchar) {
@@ -822,9 +766,9 @@ TEST(EncodingTest, ToUnicodeCMapbfchar) {
"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
EXPECT_EQ(cmap.decode(1), 0x0041);
EXPECT_EQ(cmap.decode(2), 0x0042);
EXPECT_EQ(cmap.decode(3), 0);
}
TEST(EncodingTest, ToUnicodeCMapbfrange) {
@@ -835,22 +779,20 @@ TEST(EncodingTest, ToUnicodeCMapbfrange) {
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
"<0001> <0005> <0041>\n"
"<0010> <0012> [<0061> <0062> <0063>]\n"
"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'
EXPECT_EQ(cmap.decode(1), 0x0041);
EXPECT_EQ(cmap.decode(3), 0x0043);
EXPECT_EQ(cmap.decode(5), 0x0045);
// Assert array
EXPECT_EQ(cmap.decode(0x10), 0x0061); // 'a'
EXPECT_EQ(cmap.decode(0x11), 0x0062); // 'b'
EXPECT_EQ(cmap.decode(0x12), 0x0063); // 'c'
EXPECT_EQ(cmap.decode(0x10), 0x0061);
EXPECT_EQ(cmap.decode(0x11), 0x0062);
EXPECT_EQ(cmap.decode(0x12), 0x0063);
}
TEST(EncodingTest, ToUnicodeMalformedCMap) {
@@ -858,17 +800,15 @@ TEST(EncodingTest, ToUnicodeMalformedCMap) {
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
"<0001> <0041>\n"
"<0002> /invalid\n"
"<0003> <0043>\n"
"endbfchar\n"
"endcmap\n";
@@ -891,12 +831,10 @@ TEST(EncodingTest, FontLoaderWithEncoding) {
std::vector<uint8_t> buffer(size);
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
// Create predefined encoding (WinAnsi)
auto encoding = std::make_unique<pdfengine::fonts::pdf_fonts::PredefinedEncoding>(
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)
);
@@ -907,7 +845,6 @@ TEST(EncodingTest, FontLoaderWithEncoding) {
const auto* retrievedEncoding = pdfFont->getEncoding();
ASSERT_NE(retrievedEncoding, nullptr);
// Verify it translates Euro symbol exception properly
EXPECT_EQ(retrievedEncoding->decode(128), 0x20AC);
}
@@ -960,14 +897,12 @@ TEST(CIDFontTest, CompositeFontInitializationAndTypes) {
auto desc = std::make_unique<FontDescriptor>();
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);
@@ -979,12 +914,10 @@ TEST(CIDFontTest, CIDToGIDTranslations) {
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<uint32_t, uint32_t> customMap = {
{10, 100},
{20, 200},
@@ -995,9 +928,8 @@ TEST(CIDFontTest, CIDToGIDTranslations) {
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
EXPECT_EQ(font.mapCIDToGID(40), 0);
// Set back to identity
font.setIdentityCIDToGIDMap();
EXPECT_TRUE(font.isIdentityMap());
EXPECT_EQ(font.mapCIDToGID(10), 10);
@@ -1006,22 +938,18 @@ TEST(CIDFontTest, CIDToGIDTranslations) {
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());
@@ -1040,30 +968,30 @@ TEST(FontFallbackTest, StandardFontFallbacks) {
auto& fallback = FontFallback::getInstance();
// Helvetica resolves to its sans-serif substitute for the host platform.
std::string path1 = fallback.getFallbackFontPath("Helvetica");
EXPECT_FALSE(path1.empty());
EXPECT_TRUE(std::filesystem::exists(path1));
#if defined(_WIN32)
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "liberationsans"));
#elif defined(__APPLE__)
// Arial may not be installed; the resolver then falls back to Helvetica.
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "helvetica") ||
containsCI(path1, "liberationsans"));
#else
EXPECT_TRUE(containsCI(path1, "liberationsans") || containsCI(path1, "dejavusans"));
#endif
// Times resolves to its serif substitute for the host platform.
std::string path2 = fallback.getFallbackFontPath("Times-Roman");
EXPECT_FALSE(path2.empty());
EXPECT_TRUE(std::filesystem::exists(path2));
#if defined(_WIN32)
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif"));
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif") ||
containsCI(path2, "tinos"));
#elif defined(__APPLE__)
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif"));
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif") ||
containsCI(path2, "tinos"));
#else
EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif"));
EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif") ||
containsCI(path2, "tinos"));
#endif
}
@@ -1072,22 +1000,20 @@ TEST(FontFallbackTest, StyleModifierResolutions) {
auto& fallback = FontFallback::getInstance();
// Bold Helvetica should map to a bold sans-serif substitute.
std::string pathBold = fallback.getFallbackFontPath("Helvetica", true, false);
EXPECT_FALSE(pathBold.empty());
EXPECT_TRUE(std::filesystem::exists(pathBold));
#if defined(_WIN32)
// Windows ships the styled variants, so assert the exact bold face.
EXPECT_TRUE(containsCI(pathBold, "arialbd") || containsCI(pathBold, "liberationsans-bold"));
#endif
// Bold-italic Times should map to a bold-italic serif substitute.
std::string pathBoldItalic = fallback.getFallbackFontPath("Times", true, true);
EXPECT_FALSE(pathBoldItalic.empty());
EXPECT_TRUE(std::filesystem::exists(pathBoldItalic));
#if defined(_WIN32)
EXPECT_TRUE(containsCI(pathBoldItalic, "timesbi") ||
containsCI(pathBoldItalic, "liberationserif-bolditalic"));
containsCI(pathBoldItalic, "liberationserif-bolditalic") ||
containsCI(pathBoldItalic, "tinos-bolditalic"));
#endif
}
@@ -1097,11 +1023,8 @@ TEST(FontFallbackTest, CustomFallbackRegistration) {
auto& fallback = FontFallback::getInstance();
fallback.resetToDefaults();
// Lookup the default substitute path for Helvetica.
std::string standardPath = fallback.getFallbackFontPath("Helvetica");
// The resolver only returns an override whose file actually exists on disk,
// so register a real temp file rather than a hardcoded OS-specific path.
std::filesystem::path overrideFont =
std::filesystem::temp_directory_path() / "pdfengine_custom_fallback.ttf";
{ std::ofstream(overrideFont) << "stub-font"; }
@@ -1111,7 +1034,6 @@ TEST(FontFallbackTest, CustomFallbackRegistration) {
std::string overridenPath = fallback.getFallbackFontPath("Helvetica");
EXPECT_EQ(overridenPath, overrideFont.string());
// Reset back to defaults and confirm the original substitute returns.
fallback.resetToDefaults();
std::string restoredPath = fallback.getFallbackFontPath("Helvetica");
EXPECT_EQ(restoredPath, standardPath);
@@ -1128,7 +1050,6 @@ TEST(FontSubsetTest, SubsetTagParsingAndStripping) {
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), "");
@@ -1138,20 +1059,16 @@ TEST(FontSubsetTest, SubsetTagParsingAndStripping) {
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
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica"));
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica"));
}
TEST(FontSubsetTest, GIDRemappingTranslations) {
@@ -1163,14 +1080,12 @@ TEST(FontSubsetTest, GIDRemappingTranslations) {
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'
subset.addGlyphMapping(1, 41);
subset.addGlyphMapping(2, 42);
subset.addGlyphMapping(3, 43);
EXPECT_EQ(subset.getMappingCount(), 3u);
EXPECT_TRUE(subset.hasGlyphMapping(1));
@@ -1178,13 +1093,12 @@ TEST(FontSubsetTest, GIDRemappingTranslations) {
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
EXPECT_EQ(subset.mapSubsetToOriginal(4), 4);
}
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);
@@ -1192,7 +1106,6 @@ TEST(FontSubsetTest, CoreFontSubsettingIntegration) {
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);
@@ -1200,7 +1113,6 @@ TEST(FontSubsetTest, CoreFontSubsettingIntegration) {
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);
@@ -1330,36 +1242,28 @@ TEST(TextExtractionLayerTest, StringUtf8Conversion) {
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<double> 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
double expectedWidthA = (600.0 / 1000.0) * fontSize;
double expectedWidthB = (500.0 / 1000.0) * fontSize;
double expectedWidthC = (550.0 / 1000.0) * fontSize;
double expectedWidthD = (400.0 / 1000.0) * fontSize;
// 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);
}
@@ -1377,12 +1281,10 @@ TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) {
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);
@@ -1390,39 +1292,32 @@ TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) {
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
EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5);
EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5);
EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5);
EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5);
}
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 'ん'
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093);
// Standard Adobe-Japan1 Katakana CIDs
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1); // Katakana 'ァ'
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6); // Katakana 'ヶ'
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6);
// Core Kanji
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00); // Kanji '一'
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00);
// GB1 Chinese simplified ideographic marks
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000); // space
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001); // comma
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000);
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001);
}
TEST(CIDAdvancedMappingTest, HarfBuzzVerticalShapingSignature) {
using namespace pdfengine::fonts;
// Validate WritingMode configurations
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Horizontal), 0);
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Vertical), 1);
}
} // namespace pdfengine::fonts
}
+3 -8
View File
@@ -32,33 +32,28 @@ TEST(MatrixTest, TransformPoint) {
float y = 5.0f;
m.transform(x, y);
EXPECT_FLOAT_EQ(x, 20.0f); // 2*5 + 10
EXPECT_FLOAT_EQ(y, 35.0f); // 3*5 + 20
EXPECT_FLOAT_EQ(x, 20.0f);
EXPECT_FLOAT_EQ(y, 35.0f);
}
TEST(GraphicsStateStackTest, PushPop) {
GraphicsStateStack stack;
// Initial state
stack.current().lineWidth = 5.0f;
// Push new state
stack.push();
EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f);
// Modify current state
stack.current().lineWidth = 10.0f;
EXPECT_FLOAT_EQ(stack.current().lineWidth, 10.0f);
// Pop back to initial
stack.pop();
EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f);
}
TEST(GraphicsStateStackTest, PopEmptyProtection) {
GraphicsStateStack stack;
// Attempting to pop the root state should be safe (ignored)
stack.pop();
stack.current().lineWidth = 2.0f; // Should still be valid
stack.current().lineWidth = 2.0f;
EXPECT_FLOAT_EQ(stack.current().lineWidth, 2.0f);
}
+1 -1
View File
@@ -406,7 +406,7 @@ std::vector<uint8_t> rgb(std::initializer_list<uint8_t> values) {
return std::vector<uint8_t>(values);
}
} // namespace
}
TEST(ImageXObjectVerification, JpegLogoAndPhotoDecodeAndMatchPdfiumRender) {
const auto logoJpeg = encodeJpegRgb(rgb({
+1 -7
View File
@@ -40,7 +40,7 @@ TEST(LexerTest, Strings) {
EXPECT_EQ(tokens[2].stringValue, "Escapes \n \t \\ ()");
EXPECT_EQ(tokens[3].type, TokenType::String);
EXPECT_EQ(tokens[3].stringValue, "Octal +"); // \053 is '+'
EXPECT_EQ(tokens[3].stringValue, "Octal +");
}
TEST(LexerTest, HexStrings) {
@@ -49,12 +49,10 @@ TEST(LexerTest, HexStrings) {
ASSERT_EQ(tokens.size(), 2);
EXPECT_EQ(tokens[0].type, TokenType::HexString);
// "Hello"
std::vector<uint8_t> expected1 = {0x48, 0x65, 0x6C, 0x6C, 0x6F};
EXPECT_EQ(tokens[0].bytesValue, expected1);
EXPECT_EQ(tokens[1].type, TokenType::HexString);
// "4A5" padded to "4A50"
std::vector<uint8_t> expected2 = {0x4A, 0x50};
EXPECT_EQ(tokens[1].bytesValue, expected2);
}
@@ -113,15 +111,11 @@ TEST(LexerTest, IntegrationHelloWorld) {
Lexer lexer(stream->decodedContent);
auto tokens = lexer.tokenize();
// We expect something like: BT /F1 12 Tf (Hello) Tj ET
// plus any graphics state like 0 0 0 rg, etc.
// Let's just find the text block.
bool foundHello = false;
for (size_t i = 0; i < tokens.size(); ++i) {
if (tokens[i].type == TokenType::String && tokens[i].stringValue == "Hello, world!") {
foundHello = true;
// The next token should be Tj or TJ
ASSERT_LT(i + 1, tokens.size());
EXPECT_EQ(tokens[i+1].type, TokenType::Operator);
EXPECT_TRUE(tokens[i+1].stringValue == "Tj" || tokens[i+1].stringValue == "TJ");
-6
View File
@@ -37,27 +37,21 @@ TEST(ParserTest, ArraysAndDicts) {
ContentParser parser(tokens);
auto ops = parser.parse();
// The dictionary and array and string are ALL pushed onto the operand stack
// until the operator 'Tj' is encountered.
// Tj will consume all of them.
ASSERT_EQ(ops.size(), 1);
EXPECT_EQ(ops[0].op, "Tj");
ASSERT_EQ(ops[0].operands.size(), 3);
// First operand: Dict
auto dictNode = ops[0].operands[0];
EXPECT_EQ(dictNode->type, AstNodeType::Dictionary);
ASSERT_TRUE(dictNode->dictItems.find("Type") != dictNode->dictItems.end());
EXPECT_EQ(dictNode->dictItems["Type"]->stringValue, "Page");
// Second operand: Array
auto arrayNode = ops[0].operands[1];
EXPECT_EQ(arrayNode->type, AstNodeType::Array);
ASSERT_EQ(arrayNode->arrayItems.size(), 2);
EXPECT_DOUBLE_EQ(arrayNode->arrayItems[0]->numberValue, 1.0);
EXPECT_DOUBLE_EQ(arrayNode->arrayItems[1]->numberValue, 2.0);
// Third operand: String
auto strNode = ops[0].operands[2];
EXPECT_EQ(strNode->type, AstNodeType::String);
EXPECT_EQ(strNode->stringValue, "Text");
+1 -3
View File
@@ -39,7 +39,7 @@ TEST_F(QpdfExtractorTest, ExtractFromMemory) {
auto stream = extractor.extractPageStream(path.string(), 0);
ASSERT_TRUE(stream.has_value());
EXPECT_EQ(stream->pageIndex, 0);
EXPECT_FALSE(stream->compressed); // Or true depending on qpdf, but we only verify success here
EXPECT_FALSE(stream->compressed);
StreamVerification v = verifyContentStream(stream.value());
EXPECT_TRUE(v.hasBT);
@@ -70,7 +70,6 @@ TEST_F(QpdfExtractorTest, CorruptPdf) {
}
TEST_F(QpdfExtractorTest, EmptyContents) {
// about_blank.pdf usually has an empty page or no text
auto path = getCorpusPath("basic", "about_blank.pdf");
auto stream = extractor.extractPageStream(path.string(), 0);
ASSERT_TRUE(stream.has_value());
@@ -81,7 +80,6 @@ TEST_F(QpdfExtractorTest, EmptyContents) {
}
TEST_F(QpdfExtractorTest, VerifyNoText) {
// black.pdf or rectangles.pdf has no text, just graphics
auto path = getCorpusPath("basic", "black.pdf");
auto stream = extractor.extractPageStream(path.string(), 0);
ASSERT_TRUE(stream.has_value());
-8
View File
@@ -20,12 +20,10 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
std::filesystem::path sourcePath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf";
std::filesystem::path destPath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world_modified.pdf";
// 1. Extract
QpdfExtractor extractor;
auto stream = extractor.extractPageStream(sourcePath.string(), 0);
ASSERT_TRUE(stream.has_value());
// 2. Lex, Parse, Build
Lexer lexer(stream->decodedContent);
auto tokens = lexer.tokenize();
ContentParser parser(tokens);
@@ -33,7 +31,6 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
ContentBuilder builder;
auto objects = builder.build(ops);
// 3. Modify Text
bool foundAndModified = false;
for (auto& obj : objects) {
if (obj->getType() == ContentObjectType::Text) {
@@ -47,20 +44,16 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
}
ASSERT_TRUE(foundAndModified) << "Could not find 'Hello, world!' to modify";
// 4. Serialize back to Ops
ContentSerializer contentSerializer;
auto newOps = contentSerializer.serialize(objects);
// 5. Serialize to raw bytes
AstSerializer astSerializer;
std::string newRawStream = astSerializer.serialize(newOps);
// 6. Write and Save PDF
QpdfWriter writer;
auto writeRes = writer.replacePageStreamAndSave(sourcePath.string(), destPath.string(), 0, newRawStream);
ASSERT_TRUE(writeRes.has_value()) << writeRes.error();
// 7. Re-open and verify modification
auto verifyStream = extractor.extractPageStream(destPath.string(), 0);
ASSERT_TRUE(verifyStream.has_value());
@@ -83,6 +76,5 @@ TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
EXPECT_TRUE(verifiedModification) << "Modified string was not successfully saved and reloaded!";
// Cleanup
std::filesystem::remove(destPath);
}
+1 -4
View File
@@ -17,21 +17,18 @@ TEST(SkiaRendererTest, RenderSimpleDisplayList) {
canvas.clear(SK_ColorWHITE);
DisplayList dl;
// Push state and set fill color to red
dl.saveState();
dl.fillRect(10, 10, 50, 50); // Will be filled with red (we need a SetFillColorCommand, but right now GraphicsState doesn't expose it directly via Command yet. It will use default black)
dl.fillRect(10, 10, 50, 50);
dl.restoreState();
SkiaRenderer renderer(&canvas);
renderer.render(dl);
// Let's verify that a pixel at (20,20) was drawn. Since default color is black (0,0,0), we check that.
SkColor c = bitmap.getColor(20, 20);
EXPECT_EQ(SkColorGetR(c), 0);
EXPECT_EQ(SkColorGetG(c), 0);
EXPECT_EQ(SkColorGetB(c), 0);
// Check outside the rect
SkColor bg = bitmap.getColor(5, 5);
EXPECT_EQ(SkColorGetR(bg), 255);
EXPECT_EQ(SkColorGetG(bg), 255);
-4
View File
@@ -1,6 +1,3 @@
// Phase 0 smoke test: proves the engine library compiles, links against its
// vcpkg dependencies, and is callable. This is what Gate G0 ("infrastructure
// compiles on all platforms") checks in CI.
#include <gtest/gtest.h>
#include <pdfengine/pdf_engine.hpp>
#include <string_view>
@@ -28,6 +25,5 @@ TEST(EngineSmoke, BuildInfoConsistentWithSkiaLinkage) {
}
TEST(EngineSmoke, LogBuildInfoDoesNotThrow) {
// Exercises the spdlog dependency end to end (compile + link + call).
EXPECT_NO_THROW(pdfengine::engineLogBuildInfo());
}
-2
View File
@@ -3,13 +3,11 @@
extern "C" {
// Exported function: add two numbers
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
// Exported function: print hello message
EMSCRIPTEN_KEEPALIVE
void hello() {
std::cout << "Hello from C++ WASM!" << std::endl;