684 lines
22 KiB
C++
684 lines
22 KiB
C++
#include <gtest/gtest.h>
|
|
#include <pdfengine/pdf_document.hpp>
|
|
#include <pdfengine/pdf_engine.hpp>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
#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());
|
|
}
|
|
|
|
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"({
|
|
"operations": [
|
|
{
|
|
"type": "add_text",
|
|
"pageIndex": 0,
|
|
"data": {
|
|
"text": "UniqueEditedTextAnnotation123",
|
|
"x": 100.0,
|
|
"y": 150.0,
|
|
"fontSize": 14.0
|
|
}
|
|
}
|
|
]
|
|
})";
|
|
|
|
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 annotsRes = newPage->extractAnnotationsText();
|
|
ASSERT_TRUE(annotsRes.has_value());
|
|
bool found = false;
|
|
for (const auto& annotText : *annotsRes) {
|
|
if (annotText.find("UniqueEditedTextAnnotation123") != std::string::npos) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
EXPECT_TRUE(found);
|
|
}
|
|
|
|
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"({
|
|
"operations": [
|
|
{
|
|
"type": "add_text",
|
|
"pageIndex": 0,
|
|
"data": {
|
|
"text": "IntrospectionDiagnosticsNewText",
|
|
"x": 10.0,
|
|
"y": 20.0,
|
|
"fontSize": 12.0
|
|
}
|
|
}
|
|
]
|
|
})";
|
|
|
|
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<std::thread> 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();
|
|
|
|
// Test Part 1: Introspection & Metadata verification using utf-8.pdf
|
|
{
|
|
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) {
|
|
// 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) {
|
|
EXPECT_TRUE(std::isupper(static_cast<unsigned char>(c)));
|
|
}
|
|
EXPECT_EQ(f.sourceType, "Embedded");
|
|
EXPECT_TRUE(f.isEmbedded);
|
|
EXPECT_EQ(f.internalFontId, f.subsetTag + "_" + f.fontName);
|
|
} else {
|
|
EXPECT_TRUE(f.subsetTag.empty());
|
|
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());
|
|
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
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test Part 2: Vertical Writing Mode Detection
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
// Ensure at least one vertical font is found in vertical_text.pdf
|
|
EXPECT_TRUE(foundVertical);
|
|
}
|
|
}
|
|
|
|
// Test Part 3: Font Size and Glyph Bounds Handling
|
|
{
|
|
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<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
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|