fix
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user