feat: implemented wrapper and pdf doc page

This commit is contained in:
Furqan-14
2026-05-22 15:47:48 +05:30
parent d4fe33b007
commit fe627f4e19
55 changed files with 5074 additions and 101 deletions
+6
View File
@@ -3,6 +3,7 @@
add_executable(pdfengine_smoke
smoke_test.cpp
fonts_test.cpp
document_test.cpp
)
target_link_libraries(pdfengine_smoke
@@ -17,6 +18,11 @@ target_include_directories(pdfengine_smoke
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
)
# Locate and normalize corpus path
set(TEST_CORPUS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../corpus" CACHE PATH "Path to test corpus")
file(TO_CMAKE_PATH "${TEST_CORPUS_DIR}" TEST_CORPUS_DIR_NORM)
target_compile_definitions(pdfengine_smoke PRIVATE TEST_CORPUS_DIR="${TEST_CORPUS_DIR_NORM}")
pdfengine_set_warnings(pdfengine_smoke)
pdfengine_enable_sanitizers(pdfengine_smoke)
+346
View File
@@ -0,0 +1,346 @@
#include <gtest/gtest.h>
#include <pdfengine/pdf_document.hpp>
#include <pdfengine/pdf_engine.hpp>
#include <filesystem>
#include <fstream>
#include <vector>
#include <string>
#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<uint8_t> 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<uint8_t> buffer(size);
if (file.read(reinterpret_cast<char*>(buffer.data()), size)) {
return buffer;
}
return {};
}
const std::vector<std::string> 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<std::string> 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<std::string> 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<std::string> 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<uint8_t> 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<ExpectedPageCount> 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<std::string>& 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<std::string> 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<int>(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<int> 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<int>(ptWidth * scale);
int expectedH = static_cast<int>(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<int> rotations = {0, 90, 180, 270};
std::vector<Point2D> 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());
}
}