feat: implemented wrapper and pdf doc page
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user