#include #include #include #include #include #include #include #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); // Page index 1 is out of bounds for 1-page doc 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()); } }