#include #include #include #include "parser/pdfium_document.hpp" #include "fonts/pdf_fonts/encoding/cjk_collection_db.hpp" #include #include #include #include #include #include #include #include "fonts/cache/glyph_cache.hpp" #include "fonts/pdf_fonts/font.hpp" #ifndef TEST_CORPUS_DIR #define TEST_CORPUS_DIR "../../corpus" #endif namespace { #define SKIP_IF_NO_PDFIUM() \ if (!pdfengine::engineHasPdfium()) { \ GTEST_SKIP() << "Skipping PDFium tests because PDFium is not linked."; \ } std::filesystem::path getCorpusPath(const std::string& subfolder, const std::string& filename) { return std::filesystem::path(TEST_CORPUS_DIR) / subfolder / filename; } std::vector readFile(const std::filesystem::path& path) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file.is_open()) { return {}; } std::streamsize size = file.tellg(); file.seekg(0, std::ios::beg); std::vector buffer(size); if (file.read(reinterpret_cast(buffer.data()), size)) { return buffer; } return {}; } const std::vector corpus_basic = { "about_blank.pdf", "black.pdf", "clip_path.pdf", "dashed_lines.pdf", "hello_world.pdf", "hello_world_2_pages.pdf", "many_rectangles.pdf", "rectangles.pdf", "rectangles_multi_pages.pdf", "whitespace.pdf" }; const std::vector corpus_fonts = { "latin_extended.pdf", "rotated_text.pdf", "rotated_text_90.pdf", "text_font.pdf", "utf-8.pdf", "vertical_text.pdf", "hebrew_mirrored.pdf" }; const std::vector corpus_edge = { "annots.pdf", "bookmarks.pdf", "combobox_form.pdf", "embedded_attachments.pdf", "empty_xref.pdf", "encrypted.pdf", "linearized.pdf", "listbox_form.pdf", "no_page_count.pdf", "page_labels.pdf", "text_form.pdf", "unsupported_feature.pdf", "zero_length_stream.pdf" }; } namespace pdfengine { TEST(DocumentLoadTest, NonExistentFileReturnsFileNotFound) { SKIP_IF_NO_PDFIUM(); auto result = PdfDocument::loadFromFile("nonexistent_file_12345.pdf"); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), EngineError::FileNotFound); } TEST(DocumentLoadTest, InvalidFileReturnsInvalidFormat) { SKIP_IF_NO_PDFIUM(); std::string path = "invalid_format_test.pdf"; { std::ofstream out(path, std::ios::binary); out << "NOT A PDF FILE!"; } auto result = PdfDocument::loadFromFile(path); std::filesystem::remove(path); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), EngineError::InvalidFormat); } TEST(DocumentLoadTest, EncryptedPdfRequiresPassword) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("edge-cases", "encrypted.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "encrypted.pdf not found in corpus."; } auto result = PdfDocument::loadFromFile(path.string()); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), EngineError::PasswordRequired); } TEST(DocumentLoadTest, EncryptedPdfInvalidPassword) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("edge-cases", "encrypted.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "encrypted.pdf not found in corpus."; } auto result = PdfDocument::loadFromFile(path.string(), "wrong_password"); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), EngineError::InvalidPassword); } TEST(DocumentLoadTest, EncryptedPdfCorrectPassword) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("edge-cases", "encrypted.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "encrypted.pdf not found in corpus."; } std::vector candidates = {"tessy", "test", "password", "123456", "1234", "foobar", "user", "owner", ""}; bool success = false; for (const auto& pw : candidates) { auto result = PdfDocument::loadFromFile(path.string(), pw); if (result.has_value()) { EXPECT_GT((*result)->pageCount(), 0); success = true; break; } } EXPECT_TRUE(success) << "Failed to open encrypted.pdf with any of the candidate passwords."; } TEST(DocumentLoadTest, LoadFromMemorySuccess) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto buffer = readFile(path); ASSERT_FALSE(buffer.empty()); auto result = PdfDocument::loadFromMemory(buffer); ASSERT_TRUE(result.has_value()); EXPECT_EQ((*result)->pageCount(), 1); } TEST(DocumentLoadTest, LoadFromMemoryEmptyBuffer) { SKIP_IF_NO_PDFIUM(); std::vector empty_buf; auto result = PdfDocument::loadFromMemory(empty_buf); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), EngineError::InvalidFormat); } TEST(PageCountTest, CorpusPageCounts) { SKIP_IF_NO_PDFIUM(); struct ExpectedPageCount { std::string folder; std::string file; int count; }; std::vector targets = { {"basic", "about_blank.pdf", 1}, {"basic", "black.pdf", 1}, {"basic", "hello_world.pdf", 1}, {"basic", "hello_world_2_pages.pdf", 2}, {"basic", "rectangles_multi_pages.pdf", 5} }; for (const auto& t : targets) { auto path = getCorpusPath(t.folder, t.file); if (!std::filesystem::exists(path)) { continue; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()) << "Failed to open " << t.file; EXPECT_EQ((*docRes)->pageCount(), t.count) << "Mismatch for " << t.file; } auto verifyCorpusFolder = [](const std::string& folder, const std::vector& files) { for (const auto& f : files) { auto path = getCorpusPath(folder, f); if (!std::filesystem::exists(path)) { ADD_FAILURE() << "Missing corpus file: " << path.string(); continue; } std::vector passwords = {""}; if (f == "encrypted.pdf") { passwords = {"tessy", "test", "password", "123456", "1234", "foobar", "user", "owner"}; } bool success = false; EngineError lastErr = EngineError::Unknown; for (const auto& pw : passwords) { auto docRes = PdfDocument::loadFromFile(path.string(), pw); if (docRes.has_value()) { EXPECT_GE((*docRes)->pageCount(), 0) << "Page count negative for " << f; success = true; break; } lastErr = docRes.error(); } EXPECT_TRUE(success) << "Failed to open " << f << " error: " << static_cast(lastErr); } }; verifyCorpusFolder("basic", corpus_basic); verifyCorpusFolder("fonts", corpus_fonts); verifyCorpusFolder("edge-cases", corpus_edge); } TEST(PageRenderTest, RenderAtDifferentDPI) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto pageRes = (*docRes)->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto page = *pageRes; double ptWidth = page->width(); double ptHeight = page->height(); EXPECT_GT(ptWidth, 0.0); EXPECT_GT(ptHeight, 0.0); std::vector dpis = {72, 144, 288}; for (int dpi : dpis) { auto imgRes = page->render(dpi); ASSERT_TRUE(imgRes.has_value()) << "Failed to render at DPI " << dpi; double scale = dpi / 72.0; int expectedW = static_cast(ptWidth * scale); int expectedH = static_cast(ptHeight * scale); EXPECT_EQ(imgRes->width, expectedW); EXPECT_EQ(imgRes->height, expectedH); ASSERT_GE(imgRes->data.size(), 8U); EXPECT_EQ(imgRes->data[0], 0x89); EXPECT_EQ(imgRes->data[1], 'P'); EXPECT_EQ(imgRes->data[2], 'N'); EXPECT_EQ(imgRes->data[3], 'G'); EXPECT_EQ(imgRes->data[4], 0x0D); EXPECT_EQ(imgRes->data[5], 0x0A); EXPECT_EQ(imgRes->data[6], 0x1A); EXPECT_EQ(imgRes->data[7], 0x0A); } } TEST(PageRenderTest, InvalidPageIndexReturnsPageOutOfBounds) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto pageRes = (*docRes)->getPage(1); ASSERT_FALSE(pageRes.has_value()); EXPECT_EQ(pageRes.error(), EngineError::PageOutOfBounds); auto pageResNeg = (*docRes)->getPage(-1); ASSERT_FALSE(pageResNeg.has_value()); EXPECT_EQ(pageResNeg.error(), EngineError::PageOutOfBounds); } TEST(CoordinateTransformTest, PageToDeviceAndBackRoundtrip) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto pageRes = (*docRes)->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto page = *pageRes; int deviceW = 1024; int deviceH = 768; std::vector rotations = {0, 90, 180, 270}; std::vector testPoints = { {0.0, 0.0}, {50.0, 50.0}, {100.0, 200.0}, {page->width() / 2.0, page->height() / 2.0}, {page->width() - 10.0, page->height() - 10.0} }; for (int rotation : rotations) { for (const auto& pt : testPoints) { auto devPt = page->pageToDevice(pt, deviceW, deviceH, rotation); auto pagePt = page->deviceToPage(devPt, deviceW, deviceH, rotation); EXPECT_NEAR(pt.x, pagePt.x, 2.0) << "Failed roundtrip for x at rotation " << rotation; EXPECT_NEAR(pt.y, pagePt.y, 2.0) << "Failed roundtrip for y at rotation " << rotation; } } } TEST(TextExtractionTest, ExtractSimpleText) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto pageRes = (*docRes)->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto textRes = (*pageRes)->extractText(); ASSERT_TRUE(textRes.has_value()); std::string text = *textRes; EXPECT_NE(text.find("Hello"), std::string::npos); EXPECT_NE(text.find("world"), std::string::npos); } TEST(TextExtractionTest, ExtractUtf8ExtendedText) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "latin_extended.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "latin_extended.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto pageRes = (*docRes)->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto textRes = (*pageRes)->extractText(); ASSERT_TRUE(textRes.has_value()); std::string text = *textRes; EXPECT_FALSE(text.empty()); } TEST(TextExtractionTest, ExtractTextWithBounds) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto pageRes = (*docRes)->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto boundsRes = (*pageRes)->extractTextWithBounds(); ASSERT_TRUE(boundsRes.has_value()); const auto& glyphs = *boundsRes; ASSERT_FALSE(glyphs.empty()); bool found_hello = false; for (const auto& glyph : glyphs) { EXPECT_FALSE(glyph.text.empty()); 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); if (glyph.text == "H" || glyph.text == "e" || glyph.text == "l" || glyph.text == "o") { found_hello = true; } } EXPECT_TRUE(found_hello); } TEST(DocumentEditTest, ApplyEditsAndIncrementalSave) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_test_1", "type": "text_overlay", "pageIndex": 0, "data": { "text": "UniqueEditedTextAnnotation123", "x": 100.0, "y": 150.0, "width": 200.0, "height": 20.0, "fontSize": 14.0, "fontFamily": "Helvetica", "color": "#000000" } } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; EXPECT_EQ(newDoc->pageCount(), 1); auto newPageRes = newDoc->getPage(0); ASSERT_TRUE(newPageRes.has_value()); auto newPage = *newPageRes; auto textRes = newPage->extractText(); ASSERT_TRUE(textRes.has_value()); EXPECT_NE(textRes->find("UniqueEditedTextAnnotation123"), std::string::npos); } TEST(DocumentEditTest, ApplyRedactionAndFullSave) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; { auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto textRes = (*pageRes)->extractText(); ASSERT_TRUE(textRes.has_value()); EXPECT_NE(textRes->find("Hello"), std::string::npos); } std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_redact_test_1", "type": "redaction", "pageIndex": 0, "data": { "x": 0.0, "y": 0.0, "width": 612.0, "height": 792.0, "fillColor": "#ffffff" } } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto saveRes = doc->saveFull(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; auto newPageRes = newDoc->getPage(0); ASSERT_TRUE(newPageRes.has_value()); auto newPage = *newPageRes; auto textRes = newPage->extractText(); ASSERT_TRUE(textRes.has_value()); EXPECT_EQ(textRes->find("Hello"), std::string::npos); EXPECT_EQ(textRes->find("world"), std::string::npos); } TEST(DocumentEditTest, ApplyImageOverlayAndIncrementalSave) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_test_img_1", "type": "image_overlay", "pageIndex": 0, "data": { "x": 100.0, "y": 150.0, "width": 200.0, "height": 150.0, "pixelWidth": 2, "pixelHeight": 2, "rawPixelData": "AAD//wAA//8AAP//AAD//w==" } } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; EXPECT_EQ(newDoc->pageCount(), 1); } TEST(DocumentEditTest, ApplyPageRotationAndIncrementalSave) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "about_blank.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "about_blank.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); double origW = (*pageRes)->width(); double origH = (*pageRes)->height(); EXPECT_GT(origW, 0.0); EXPECT_GT(origH, origW); std::string editsJson1 = R"({ "version": "1.0", "operations": [ { "id": "op_test_rot_1", "type": "page_rotation", "pageIndex": 0, "data": { "rotation": 90 } } ] })"; auto editRes1 = doc->applyEdits(editsJson1); ASSERT_TRUE(editRes1.has_value()); std::string editsJson2 = R"({ "version": "1.0", "operations": [ { "id": "op_test_rot_2", "type": "page_rotation", "pageIndex": 0, "data": { "rotation": 90 } } ] })"; auto editRes2 = doc->applyEdits(editsJson2); ASSERT_TRUE(editRes2.has_value()); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; EXPECT_EQ(newDoc->pageCount(), 1); auto newPageRes = newDoc->getPage(0); ASSERT_TRUE(newPageRes.has_value()); double rotatedW = (*newPageRes)->width(); double rotatedH = (*newPageRes)->height(); EXPECT_NEAR(rotatedW, origW, 0.01); EXPECT_NEAR(rotatedH, origH, 0.01); std::string editsJson3 = R"({ "version": "1.0", "operations": [ { "id": "op_test_rot_3", "type": "page_rotation", "pageIndex": 0, "data": { "rotation": -90 } } ] })"; auto editRes3 = newDoc->applyEdits(editsJson3); ASSERT_TRUE(editRes3.has_value()); auto saveRes3 = newDoc->saveIncremental(); ASSERT_TRUE(saveRes3.has_value()); const auto& savedBytes3 = *saveRes3; auto finalDocRes = PdfDocument::loadFromMemory(savedBytes3); ASSERT_TRUE(finalDocRes.has_value()); auto finalPageRes = (*finalDocRes)->getPage(0); ASSERT_TRUE(finalPageRes.has_value()); double finalW = (*finalPageRes)->width(); double finalH = (*finalPageRes)->height(); EXPECT_NEAR(finalW, origH, 0.01); EXPECT_NEAR(finalH, origW, 0.01); } TEST(DocumentEditTest, ApplyPageDeletionAndIncrementalSave) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world_2_pages.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world_2_pages.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; EXPECT_EQ(doc->pageCount(), 2); std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_del_test_1", "type": "page_deletion", "pageIndex": 1, "data": {} } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); EXPECT_EQ(doc->pageCount(), 1); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); EXPECT_EQ((*newDocRes)->pageCount(), 1); } TEST(DocumentEditTest, ApplyPageReorderAndIncrementalSave) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world_2_pages.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world_2_pages.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; EXPECT_EQ(doc->pageCount(), 2); std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_reorder_test_1", "type": "page_reorder", "pageIndex": 1, "data": { "destPageIndex": 0 } } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); EXPECT_EQ(doc->pageCount(), 2); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); EXPECT_EQ((*newDocRes)->pageCount(), 2); } TEST(FontDiagnosticsTest, IntrospectionAccuracy) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto page = *pageRes; auto fontsRes = page->getFonts(); ASSERT_TRUE(fontsRes.has_value()); auto fonts = *fontsRes; if (!fonts.empty()) { auto firstFont = fonts[0]; EXPECT_FALSE(firstFont.fontName.empty()); EXPECT_FALSE(firstFont.type.empty()); EXPECT_FALSE(firstFont.normalizedFamily.empty()); EXPECT_FALSE(firstFont.internalFontId.empty()); } auto docFontsRes = doc->getFonts(); ASSERT_TRUE(docFontsRes.has_value()); auto docFonts = *docFontsRes; EXPECT_EQ(docFonts.size(), fonts.size()); } TEST(FontDiagnosticsTest, SubsetAndVerticalTextIntrospection) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "vertical_text.pdf"); if (!std::filesystem::exists(path)) { path = getCorpusPath("fonts", "utf-8.pdf"); } if (!std::filesystem::exists(path)) { GTEST_SKIP() << "vertical_text.pdf or utf-8.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes = doc->getFonts(); ASSERT_TRUE(fontsRes.has_value()); for (const auto& f : *fontsRes) { if (f.isSubset) { EXPECT_FALSE(f.subsetTag.empty()); EXPECT_EQ(f.subsetTag.size(), 6); } if (f.isVertical) { EXPECT_TRUE(f.isVertical); EXPECT_NE(f.encoding.find("Identity-V"), std::string::npos); } } } TEST(FontDiagnosticsTest, CacheInvalidationAfterEdits) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes1 = doc->getFonts(); ASSERT_TRUE(fontsRes1.has_value()); std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_test_2", "type": "text_overlay", "pageIndex": 0, "data": { "text": "IntrospectionDiagnosticsNewText", "x": 10.0, "y": 20.0, "width": 200.0, "height": 20.0, "fontSize": 12.0, "fontFamily": "Helvetica", "color": "#000000" } } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto fontsRes2 = doc->getFonts(); ASSERT_TRUE(fontsRes2.has_value()); } TEST(FontDiagnosticsTest, ConcurrencyThreadSafety) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; std::vector threads; for (int i = 0; i < 8; ++i) { threads.emplace_back([&doc]() { auto res = doc->getFonts(); ASSERT_TRUE(res.has_value()); }); } for (auto& t : threads) { t.join(); } } TEST(FontDiagnosticsTest, DeepIntrospectionAndFontSizeVerification) { SKIP_IF_NO_PDFIUM(); { auto path = getCorpusPath("fonts", "utf-8.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes = doc->getFonts(); ASSERT_TRUE(fontsRes.has_value()); auto fonts = *fontsRes; for (const auto& f : fonts) { EXPECT_FALSE(f.fontName.empty()); EXPECT_FALSE(f.type.empty()); EXPECT_FALSE(f.normalizedFamily.empty()); EXPECT_FALSE(f.internalFontId.empty()); if (f.isSubset) { EXPECT_EQ(f.subsetTag.size(), 6); for (char c : f.subsetTag) { EXPECT_TRUE(std::isupper(static_cast(c))); } EXPECT_EQ(f.sourceType, "Embedded"); EXPECT_TRUE(f.isEmbedded); EXPECT_EQ(f.internalFontId, f.fontName); } else { EXPECT_TRUE(f.subsetTag.empty()); EXPECT_EQ(f.internalFontId, f.fontName + "_" + f.type + "_" + std::to_string(f.flags)); } if (f.sourceType == "SystemFallback") { EXPECT_FALSE(f.isEmbedded); EXPECT_TRUE(f.substitutedFrom.empty()); EXPECT_TRUE(f.substitutedTo.empty()); } else if (f.sourceType == "Substituted") { EXPECT_FALSE(f.isEmbedded); EXPECT_EQ(f.substitutedFrom, f.fontName); #if defined(_WIN32) EXPECT_EQ(f.substitutedTo, "Arial"); #else EXPECT_EQ(f.substitutedTo, "Liberation Sans"); #endif } EXPECT_GT(f.ascent, 0.0); EXPECT_LT(f.descent, 0.0); EXPECT_GT(f.capHeight, 0.0); } } } { auto path = getCorpusPath("fonts", "vertical_text.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto fontsRes = doc->getFonts(); ASSERT_TRUE(fontsRes.has_value()); bool foundVertical = false; for (const auto& f : *fontsRes) { if (f.isVertical) { foundVertical = true; EXPECT_TRUE(f.encoding.find("-V") != std::string::npos || f.cmapName.find("-V") != std::string::npos); } } EXPECT_TRUE(foundVertical); } } { auto path1 = getCorpusPath("fonts", "vertical_identity_v.pdf"); if (std::filesystem::exists(path1)) { auto docRes = PdfDocument::loadFromFile(path1.string()); ASSERT_TRUE(docRes.has_value()); auto fontsRes = (*docRes)->getFonts(); ASSERT_TRUE(fontsRes.has_value()); bool foundVertical = false; for (const auto& f : *fontsRes) { if (f.isVertical) foundVertical = true; } EXPECT_TRUE(foundVertical) << "Failed to detect vertical font in vertical_identity_v.pdf"; } } { auto path = getCorpusPath("fonts", "utf-8.pdf"); if (!std::filesystem::exists(path)) { path = getCorpusPath("basic", "hello_world.pdf"); } if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto page = *pageRes; auto boundsRes = page->extractTextWithBounds(); ASSERT_TRUE(boundsRes.has_value()); const auto& glyphs = *boundsRes; ASSERT_FALSE(glyphs.empty()); std::vector uniqueSizes; for (const auto& glyph : glyphs) { if (glyph.text != " " && glyph.text != "\r" && glyph.text != "\n" && glyph.text != "\t") { EXPECT_GT(glyph.w, 0.0); EXPECT_GT(glyph.h, 0.0); } EXPECT_GT(glyph.fontSize, 0.0); EXPECT_LT(glyph.fontSize, 100.0); if (std::find(uniqueSizes.begin(), uniqueSizes.end(), glyph.fontSize) == uniqueSizes.end()) { uniqueSizes.push_back(glyph.fontSize); } } if (path.filename().string() == "utf-8.pdf") { EXPECT_GE(uniqueSizes.size(), 2u); } } } } TEST(FontDiagnosticsTest, RealPDFiumEmbeddingAndTypeAccuracy) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "text_font.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "text_font.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()) << "Failed to open text_font.pdf"; auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto fontsRes = (*pageRes)->getFonts(); ASSERT_TRUE(fontsRes.has_value()); const auto& fonts = *fontsRes; ASSERT_FALSE(fonts.empty()) << "text_font.pdf must expose at least one font"; for (const auto& f : fonts) { EXPECT_FALSE(f.fontName.empty()); EXPECT_FALSE(f.type.empty()); static const std::vector kValidTypes = { "Type1", "TrueType", "CIDFontType0", "CIDFontType2" }; bool typeValid = std::find(kValidTypes.begin(), kValidTypes.end(), f.type) != kValidTypes.end(); EXPECT_TRUE(typeValid) << "Unexpected type '" << f.type << "' for font '" << f.fontName << "'"; if (f.isSubset) { EXPECT_TRUE(f.isEmbedded) << "Subset font '" << f.fontName << "' must be embedded (FPDFFont_GetIsEmbedded should return 1)"; EXPECT_EQ(f.sourceType, "Embedded") << "sourceType must be 'Embedded' when isEmbedded=true"; EXPECT_TRUE(f.substitutedFrom.empty()); EXPECT_TRUE(f.substitutedTo.empty()); } if (f.isEmbedded) { EXPECT_EQ(f.sourceType, "Embedded"); } } } TEST(FontDiagnosticsTest, RealPDFiumMetricsAccuracy) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "text_font.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "text_font.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto fontsRes = (*docRes)->getFonts(); ASSERT_TRUE(fontsRes.has_value()); for (const auto& f : *fontsRes) { EXPECT_GT(f.ascent, 0.0) << "ascent must be positive for font '" << f.fontName << "'"; EXPECT_LT(f.descent, 0.0) << "descent must be negative for font '" << f.fontName << "'"; EXPECT_GT(f.capHeight, 0.0) << "capHeight must be positive for font '" << f.fontName << "'"; EXPECT_LE(f.capHeight, f.ascent + 1.0) << "capHeight should not exceed ascent for font '" << f.fontName << "'"; EXPECT_LT(f.ascent, 1500.0) << "Implausibly large ascent for '" << f.fontName << "'"; EXPECT_GT(f.descent, -1500.0) << "Implausibly deep descent for '" << f.fontName << "'"; } } TEST(FontDiagnosticsTest, ToUnicodePresenceAccuracy) { SKIP_IF_NO_PDFIUM(); { auto path = getCorpusPath("fonts", "with_tounicode.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); if (docRes.has_value()) { auto pageRes = (*docRes)->getPage(0); if (pageRes.has_value()) { auto fontsRes = (*pageRes)->getFonts(); if (fontsRes.has_value() && !fontsRes->empty()) { bool anyTrue = false; for (const auto& f : *fontsRes) { if (f.hasToUnicode) { anyTrue = true; break; } } EXPECT_TRUE(anyTrue) << "At least one font in with_tounicode.pdf must have hasToUnicode=true"; } } } } } { auto path = getCorpusPath("fonts", "no_tounicode.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); if (docRes.has_value()) { auto pageRes = (*docRes)->getPage(0); if (pageRes.has_value()) { auto fontsRes = (*pageRes)->getFonts(); if (fontsRes.has_value() && !fontsRes->empty()) { for (const auto& f : *fontsRes) { EXPECT_FALSE(f.hasToUnicode) << "Font '" << f.fontName << "' in no_tounicode.pdf must have hasToUnicode=false"; } } } } } } { auto path = getCorpusPath("fonts", "latin_extended.pdf"); if (std::filesystem::exists(path)) { auto docRes = PdfDocument::loadFromFile(path.string()); if (docRes.has_value()) { auto pageRes = (*docRes)->getPage(0); if (pageRes.has_value()) { auto fontsRes = (*pageRes)->getFonts(); if (fontsRes.has_value() && !fontsRes->empty()) { bool anyTrue = false; for (const auto& f : *fontsRes) { if (f.hasToUnicode) { anyTrue = true; break; } } EXPECT_TRUE(anyTrue) << "At least one font in latin_extended.pdf must decode to Unicode"; } } } } } } TEST(UtfConversionTest, EmojiSurrogatePairs) { std::string utf8_grinning = "\xF0\x9F\x98\x80"; auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_grinning); ASSERT_EQ(utf16.size(), 3); EXPECT_EQ(utf16[0], 0xD83D); EXPECT_EQ(utf16[1], 0xDE00); EXPECT_EQ(utf16[2], 0x0000); std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast(utf16.data()), utf16.size() - 1); EXPECT_EQ(utf8_out, utf8_grinning); std::string utf8_rocket = "\xF0\x9F\x9A\x80"; utf16 = pdfengine::parser::utf8_to_utf16le(utf8_rocket); ASSERT_EQ(utf16.size(), 3); EXPECT_EQ(utf16[0], 0xD83D); EXPECT_EQ(utf16[1], 0xDE80); EXPECT_EQ(utf16[2], 0x0000); utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast(utf16.data()), utf16.size() - 1); EXPECT_EQ(utf8_out, utf8_rocket); } TEST(UtfConversionTest, CJKExtensionB) { std::string utf8_cjk = "\xF0\xA0\x80\x80"; auto utf16 = pdfengine::parser::utf8_to_utf16le(utf8_cjk); ASSERT_EQ(utf16.size(), 3); EXPECT_EQ(utf16[0], 0xD840); EXPECT_EQ(utf16[1], 0xDC00); EXPECT_EQ(utf16[2], 0x0000); std::string utf8_out = pdfengine::parser::utf16le_to_utf8(reinterpret_cast(utf16.data()), utf16.size() - 1); EXPECT_EQ(utf8_out, utf8_cjk); } TEST(UtfConversionTest, RoundtripMixed) { 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(utf16.data()), utf16.size() - 1); EXPECT_EQ(mixed_out, mixed); } TEST(CjkResolutionTest, AdobeCNS1) { using pdfengine::fonts::pdf_fonts::CjkCollectionDB; 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", 131), 0x4ED8); EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF); EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01); EXPECT_EQ(CjkCollectionDB::resolveCID("Identity-H-CNS1", 137), 0x4EE3); } TEST(CjkResolutionTest, AdobeKorea1) { using pdfengine::fonts::pdf_fonts::CjkCollectionDB; 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", 151), 0xAC94); EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9); EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD); EXPECT_EQ(CjkCollectionDB::resolveCID("UniKS-UTF16-H-Korea1", 153), 0xACA0); } TEST(GlyphCacheTest, ConcurrencyBench) { using namespace pdfengine::fonts; FontFace face; bool loaded = face.loadFromFile("C:\\Windows\\Fonts\\arial.ttf"); if (!loaded) { GTEST_SKIP() << "Skipping benchmark: Arial font not found."; } GlyphCache cache(1000); auto run_benchmark = [&](int num_threads, int ops_per_thread) { std::atomic start_flag{0}; std::vector threads; for (int i = 0; i < num_threads; ++i) { threads.emplace_back([&, i]() { while (start_flag.load() == 0) { std::this_thread::yield(); } for (int op = 0; op < ops_per_thread; ++op) { unsigned int glyphIndex = (op + i) % 2000; unsigned int fontSize = 12 + (op % 5); auto hit = cache.get(face, glyphIndex, fontSize); if (!hit) { GlyphBitmap bmp; bmp.width = 10; bmp.height = 10; cache.insert(face, glyphIndex, fontSize, bmp); } } }); } auto start_time = std::chrono::high_resolution_clock::now(); start_flag.store(1); for (auto& t : threads) { t.join(); } auto end_time = std::chrono::high_resolution_clock::now(); std::chrono::duration diff = end_time - start_time; return diff.count(); }; run_benchmark(2, 1000); cache.clear(); double time_10 = run_benchmark(10, 10000); std::cout << "[ BENCHMARK ] 10 Threads Time: " << time_10 << " seconds (" << (100000.0 / time_10) << " ops/sec)\n"; cache.clear(); double time_50 = run_benchmark(50, 10000); std::cout << "[ BENCHMARK ] 50 Threads Time: " << time_50 << " seconds (" << (500000.0 / time_50) << " ops/sec)\n"; EXPECT_LE(cache.size(), 1000 + 16); } TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) { SKIP_IF_NO_PDFIUM(); std::vector testFiles = { "text_font.pdf", "embedded_truetype.pdf", "embedded_cid_font.pdf", "subset_font.pdf", "latin_extended.pdf" }; bool foundAnyEmbedded = false; for (const auto& fileName : testFiles) { auto path = getCorpusPath("fonts", fileName); if (!std::filesystem::exists(path)) { continue; } std::cout << "\n========================================\n"; std::cout << "Testing PDF: " << fileName << "\n"; std::cout << "========================================\n"; auto docRes = PdfDocument::loadFromFile(path.string()); if (!docRes.has_value()) { std::cout << "Failed to load document: " << fileName << std::endl; continue; } auto doc = *docRes; auto fontsRes = doc->getFonts(); if (!fontsRes.has_value()) { std::cout << "Failed to get fonts for: " << fileName << std::endl; continue; } const auto& fonts = *fontsRes; for (const auto& fontInfo : fonts) { std::cout << "Font: " << fontInfo.fontName << ", type: " << fontInfo.type << ", isEmbedded: " << (fontInfo.isEmbedded ? "yes" : "no") << ", flags: " << fontInfo.flags << std::endl; if (fontInfo.isEmbedded) { foundAnyEmbedded = true; auto resolvedFontRes = doc->getResolvedFont(fontInfo); if (!resolvedFontRes.has_value()) { std::cout << " Failed to resolve font: " << resolvedFontRes.error() << std::endl; continue; } auto resolvedFont = *resolvedFontRes; std::cout << " Resolved font successfully." << std::endl; auto face = static_cast(resolvedFont->getFontFace().getFace()); if (face) { std::cout << " FreeType Face Num Glyphs: " << face->num_glyphs << std::endl; std::cout << " FreeType Charmaps Count: " << face->num_charmaps << std::endl; for (int i = 0; i < face->num_charmaps; ++i) { FT_CharMap cm = face->charmaps[i]; std::cout << " Charmap " << i << ": platform_id=" << cm->platform_id << ", encoding_id=" << cm->encoding_id << std::endl; FT_Error err = FT_Set_Charmap(face, cm); if (err) { std::cout << " FT_Set_Charmap failed: " << err << std::endl; continue; } FT_UInt gindex; FT_ULong charcode = FT_Get_First_Char(face, &gindex); std::cout << " Mapped characters under charmap " << i << ": "; int count = 0; while (gindex != 0 && count < 10) { std::cout << charcode << "->" << gindex << " "; charcode = FT_Get_Next_Char(face, charcode, &gindex); count++; } std::cout << std::endl; } if (face->num_charmaps > 0) { FT_Set_Charmap(face, face->charmaps[0]); } std::cout << " Glyph names: "; for (int i = 0; i < face->num_glyphs; ++i) { char nameBuf[64] = {0}; if (FT_Get_Glyph_Name(face, i, nameBuf, sizeof(nameBuf)) == 0) { std::cout << i << ":" << nameBuf << " "; } else { std::cout << i << ":[unknown] "; } } std::cout << std::endl; } else { std::cout << " No FreeType Face available." << std::endl; } EXPECT_TRUE(resolvedFont->isEmbedded()); std::vector testChars = {32, 48, 65, 97}; for (uint32_t cp : testChars) { bool hasG = resolvedFont->hasGlyph(cp); double w = resolvedFont->getAdvanceWidth(cp, 12.0); std::cout << " char(" << cp << "): hasGlyph=" << (hasG ? "yes" : "no") << ", advanceWidth=" << w << std::endl; } auto metrics = resolvedFont->getMetrics(12.0); std::cout << " Metrics: ascent=" << metrics.ascent << ", descent=" << metrics.descent << ", capHeight=" << metrics.capHeight << std::endl; EXPECT_NE(metrics.ascent, 0.0); EXPECT_NE(metrics.descent, 0.0); EXPECT_NE(metrics.capHeight, 0.0); if (fileName == "text_font.pdf") { EXPECT_TRUE(resolvedFont->hasGlyph(1)); double w = resolvedFont->getAdvanceWidth(1, 12.0); EXPECT_GT(w, 0.0); std::cout << " [VERIFIED] text_font.pdf char(1): hasGlyph=yes, advanceWidth=" << w << std::endl; } if (face->num_glyphs > 1) { bool foundNonZeroWidth = false; for (int gid = 1; gid < face->num_glyphs; ++gid) { FT_Error err = FT_Load_Glyph(face, gid, FT_LOAD_DEFAULT); if (err == 0) { double directWidth = static_cast(face->glyph->advance.x) / 64.0; if (directWidth > 0.0) { foundNonZeroWidth = true; std::cout << " [VERIFIED] Direct glyph " << gid << " load: advanceWidth=" << directWidth << std::endl; break; } } } EXPECT_TRUE(foundNonZeroWidth) << "Expected to find at least one glyph with a non-zero advance width"; } } } } EXPECT_TRUE(foundAnyEmbedded) << "Expected to find at least one embedded font in test files"; } TEST(DocumentEditTest, ReplaceTextMVPStandardFont) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto pageObj = *pageRes; auto modelRes = pageObj->extractDocumentModel(); ASSERT_TRUE(modelRes.has_value()); const auto& model = *modelRes; std::vector objectIndices; for (const auto& p : model.paragraphs) { for (const auto& line : p.lines) { for (const auto& run : line.runs) { if (run.text.find("Hello") != std::string::npos) { objectIndices = run.objectIndices; break; } } if (!objectIndices.empty()) break; } if (!objectIndices.empty()) break; } ASSERT_FALSE(objectIndices.empty()) << "Could not find a text object in hello_world.pdf"; std::string indicesStr = ""; for (size_t i = 0; i < objectIndices.size(); ++i) { indicesStr += std::to_string(objectIndices[i]); if (i + 1 < objectIndices.size()) indicesStr += ","; } std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_mvp_1", "type": "replace_text", "pageIndex": 0, "objectIndices": [)" + indicesStr + R"(], "text": "Greeting, universe!" } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; auto newPageRes = newDoc->getPage(0); ASSERT_TRUE(newPageRes.has_value()); auto newPage = *newPageRes; auto textRes = newPage->extractText(); ASSERT_TRUE(textRes.has_value()); EXPECT_NE(textRes->find("Greeting, universe!"), std::string::npos); EXPECT_EQ(textRes->find("Hello"), std::string::npos); } TEST(DocumentEditTest, ReplaceTextRuntimeFontEngine) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "latin_extended.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "latin_extended.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto pageObj = *pageRes; auto modelRes = pageObj->extractDocumentModel(); ASSERT_TRUE(modelRes.has_value()); const auto& model = *modelRes; std::vector objectIndices; std::string originalFontId = ""; for (const auto& p : model.paragraphs) { for (const auto& line : p.lines) { for (const auto& run : line.runs) { if (run.fontName.find("Roboto-Regular") != std::string::npos) { objectIndices = run.objectIndices; originalFontId = run.internalFontId; break; } } if (!objectIndices.empty()) break; } if (!objectIndices.empty()) break; } ASSERT_FALSE(objectIndices.empty()) << "Could not find target text run in latin_extended.pdf"; std::string indicesStr = ""; for (size_t i = 0; i < objectIndices.size(); ++i) { indicesStr += std::to_string(objectIndices[i]); if (i + 1 < objectIndices.size()) indicesStr += ","; } std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_engine_1", "type": "replace_text", "pageIndex": 0, "objectIndices": [)" + indicesStr + R"(], "text": "Font Engine Active!", "internalFontId": ")" + originalFontId + R"(" } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; auto newPageRes = newDoc->getPage(0); ASSERT_TRUE(newPageRes.has_value()); auto newPage = *newPageRes; auto textRes = newPage->extractText(); ASSERT_TRUE(textRes.has_value()); EXPECT_NE(textRes->find("Font Engine Active!"), std::string::npos); } TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("basic", "hello_world.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "hello_world.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto pageObj = *pageRes; auto modelRes = pageObj->extractDocumentModel(); ASSERT_TRUE(modelRes.has_value()); const auto& model = *modelRes; std::vector objectIndices; std::string originalFontId = ""; for (const auto& p : model.paragraphs) { for (const auto& line : p.lines) { for (const auto& run : line.runs) { if (!run.objectIndices.empty()) { objectIndices = run.objectIndices; originalFontId = run.internalFontId; break; } } if (!objectIndices.empty()) break; } if (!objectIndices.empty()) break; } ASSERT_FALSE(objectIndices.empty()) << "Could not find a text run in hello_world.pdf"; std::string indicesStr = ""; for (size_t i = 0; i < objectIndices.size(); ++i) { indicesStr += std::to_string(objectIndices[i]); if (i + 1 < objectIndices.size()) indicesStr += ","; } std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_reuse_1", "type": "replace_text", "pageIndex": 0, "objectIndices": [)" + indicesStr + R"(], "text": "Embedded Arial", "internalFontId": ")" + originalFontId + R"(" } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; auto fontsRes = newDoc->getFonts(0, 0); ASSERT_TRUE(fontsRes.has_value()); bool foundEmbeddedArial = false; for (const auto& f : *fontsRes) { if (f.isEmbedded && (f.fontName.find("Arial") != std::string::npos || f.fontName.find("LiberationSans") != std::string::npos)) { foundEmbeddedArial = true; } } std::cout << "Font Embedding Test: foundEmbeddedArial = " << foundEmbeddedArial << std::endl; } TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) { SKIP_IF_NO_PDFIUM(); auto path = getCorpusPath("fonts", "latin_extended.pdf"); if (!std::filesystem::exists(path)) { GTEST_SKIP() << "latin_extended.pdf not found in corpus."; } auto docRes = PdfDocument::loadFromFile(path.string()); ASSERT_TRUE(docRes.has_value()); auto doc = *docRes; auto pageRes = doc->getPage(0); ASSERT_TRUE(pageRes.has_value()); auto pageObj = *pageRes; auto modelRes = pageObj->extractDocumentModel(); ASSERT_TRUE(modelRes.has_value()); const auto& model = *modelRes; std::vector targetIndices; std::string originalFontId = ""; std::string runBText = ""; double runBOrigX = 0.0; double runBOrigY = 0.0; for (const auto& p : model.paragraphs) { for (const auto& line : p.lines) { if (line.runs.size() >= 2) { const auto& runA = line.runs[0]; const auto& runB = line.runs[1]; if (runA.fontName.find("Roboto-Regular") != std::string::npos && !runA.objectIndices.empty() && runB.x > runA.x) { targetIndices = runA.objectIndices; originalFontId = runA.internalFontId; runBText = runB.text; runBOrigX = runB.x; runBOrigY = runB.y; break; } } } if (!targetIndices.empty()) break; } if (targetIndices.empty()) { GTEST_SKIP() << "Could not find a suitable line with multiple runs to test reflow."; } std::string indicesStr = ""; for (size_t i = 0; i < targetIndices.size(); ++i) { indicesStr += std::to_string(targetIndices[i]); if (i + 1 < targetIndices.size()) indicesStr += ","; } std::string editsJson = R"({ "version": "1.0", "operations": [ { "id": "op_reflow_1", "type": "replace_text", "pageIndex": 0, "objectIndices": [)" + indicesStr + R"(], "text": "This is an extremely long replacement text to force the Reflow Engine to shift subsequent runs!", "internalFontId": ")" + originalFontId + R"(" } ] })"; auto editRes = doc->applyEdits(editsJson); ASSERT_TRUE(editRes.has_value()); auto saveRes = doc->saveIncremental(); ASSERT_TRUE(saveRes.has_value()); const auto& savedBytes = *saveRes; ASSERT_FALSE(savedBytes.empty()); auto newDocRes = PdfDocument::loadFromMemory(savedBytes); ASSERT_TRUE(newDocRes.has_value()); auto newDoc = *newDocRes; auto newPageRes = newDoc->getPage(0); ASSERT_TRUE(newPageRes.has_value()); auto newPage = *newPageRes; auto newModelRes = newPage->extractDocumentModel(); ASSERT_TRUE(newModelRes.has_value()); const auto& newModel = *newModelRes; bool foundRunB = false; double runBNewX = 0.0; for (const auto& p : newModel.paragraphs) { for (const auto& line : p.lines) { for (const auto& run : line.runs) { if (run.text == runBText && std::abs(run.y - runBOrigY) < 5.0) { foundRunB = true; runBNewX = run.x; break; } } if (foundRunB) break; } if (foundRunB) break; } ASSERT_TRUE(foundRunB) << "Could not find the subsequent text run '" << runBText << "' in the reflowed document."; EXPECT_GT(runBNewX, runBOrigX + 10.0) << "The subsequent text run did not shift to the right by at least 10 points."; std::cout << "Reflow Engine verified: '" << runBText << "' shifted from X=" << runBOrigX << " to X=" << runBNewX << std::endl; } }