2026-05-22 15:47:48 +05:30
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
#include <pdfengine/pdf_document.hpp>
|
|
|
|
|
#include <pdfengine/pdf_engine.hpp>
|
2026-06-02 11:16:49 +05:30
|
|
|
#include "parser/pdfium_document.hpp"
|
|
|
|
|
#include "fonts/pdf_fonts/encoding/cjk_collection_db.hpp"
|
2026-05-22 15:47:48 +05:30
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <string>
|
2026-05-26 15:55:48 +05:30
|
|
|
#include <thread>
|
2026-06-02 11:16:49 +05:30
|
|
|
#include <atomic>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include "fonts/cache/glyph_cache.hpp"
|
2026-06-11 18:19:26 +05:30
|
|
|
#include "fonts/pdf_fonts/font.hpp"
|
2026-05-22 15:47:48 +05:30
|
|
|
#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());
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
auto pageRes = (*docRes)->getPage(1);
|
2026-05-22 15:47:48 +05:30
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 14:47:38 +05:30
|
|
|
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());
|
2026-05-26 11:23:29 +05:30
|
|
|
if (glyph.text != " " && glyph.text != "\r" && glyph.text != "\n" && glyph.text != "\t") {
|
|
|
|
|
EXPECT_GT(glyph.w, 0.0);
|
|
|
|
|
EXPECT_GT(glyph.h, 0.0);
|
|
|
|
|
}
|
2026-05-25 14:47:38 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 11:23:29 +05:30
|
|
|
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"({
|
2026-06-02 11:38:57 +05:30
|
|
|
"version": "1.0",
|
2026-05-26 11:23:29 +05:30
|
|
|
"operations": [
|
|
|
|
|
{
|
2026-06-02 11:38:57 +05:30
|
|
|
"id": "op_test_1",
|
|
|
|
|
"type": "text_overlay",
|
2026-05-26 11:23:29 +05:30
|
|
|
"pageIndex": 0,
|
|
|
|
|
"data": {
|
|
|
|
|
"text": "UniqueEditedTextAnnotation123",
|
|
|
|
|
"x": 100.0,
|
|
|
|
|
"y": 150.0,
|
2026-06-02 11:38:57 +05:30
|
|
|
"width": 200.0,
|
|
|
|
|
"height": 20.0,
|
|
|
|
|
"fontSize": 14.0,
|
|
|
|
|
"fontFamily": "Helvetica",
|
|
|
|
|
"color": "#000000"
|
2026-05-26 11:23:29 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})";
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-06-02 11:38:57 +05:30
|
|
|
auto textRes = newPage->extractText();
|
|
|
|
|
ASSERT_TRUE(textRes.has_value());
|
|
|
|
|
EXPECT_NE(textRes->find("UniqueEditedTextAnnotation123"), std::string::npos);
|
2026-05-26 11:23:29 +05:30
|
|
|
}
|
|
|
|
|
|
2026-06-08 16:53:02 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 11:22:41 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 15:55:48 +05:30
|
|
|
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"({
|
2026-06-02 11:38:57 +05:30
|
|
|
"version": "1.0",
|
2026-05-26 15:55:48 +05:30
|
|
|
"operations": [
|
|
|
|
|
{
|
2026-06-02 11:38:57 +05:30
|
|
|
"id": "op_test_2",
|
|
|
|
|
"type": "text_overlay",
|
2026-05-26 15:55:48 +05:30
|
|
|
"pageIndex": 0,
|
|
|
|
|
"data": {
|
|
|
|
|
"text": "IntrospectionDiagnosticsNewText",
|
|
|
|
|
"x": 10.0,
|
|
|
|
|
"y": 20.0,
|
2026-06-02 11:38:57 +05:30
|
|
|
"width": 200.0,
|
|
|
|
|
"height": 20.0,
|
|
|
|
|
"fontSize": 12.0,
|
|
|
|
|
"fontFamily": "Helvetica",
|
|
|
|
|
"color": "#000000"
|
2026-05-26 15:55:48 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
})";
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 19:20:02 +05:30
|
|
|
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<unsigned char>(c)));
|
|
|
|
|
}
|
|
|
|
|
EXPECT_EQ(f.sourceType, "Embedded");
|
|
|
|
|
EXPECT_TRUE(f.isEmbedded);
|
2026-06-03 10:51:33 +05:30
|
|
|
EXPECT_EQ(f.internalFontId, f.fontName);
|
2026-05-26 19:20:02 +05:30
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 11:16:49 +05:30
|
|
|
{
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 19:20:02 +05:30
|
|
|
{
|
2026-06-01 14:54:55 +05:30
|
|
|
auto path = getCorpusPath("fonts", "utf-8.pdf");
|
2026-05-26 19:20:02 +05:30
|
|
|
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) {
|
|
|
|
|
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);
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_LT(glyph.fontSize, 100.0);
|
2026-05-26 19:20:02 +05:30
|
|
|
|
|
|
|
|
if (std::find(uniqueSizes.begin(), uniqueSizes.end(), glyph.fontSize) == uniqueSizes.end()) {
|
|
|
|
|
uniqueSizes.push_back(glyph.fontSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 14:54:55 +05:30
|
|
|
if (path.filename().string() == "utf-8.pdf") {
|
2026-05-26 19:20:02 +05:30
|
|
|
EXPECT_GE(uniqueSizes.size(), 2u);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 11:16:49 +05:30
|
|
|
|
|
|
|
|
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<std::string> 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 << "'";
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_LE(f.capHeight, f.ascent + 1.0)
|
2026-06-02 11:16:49 +05:30
|
|
|
<< "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<const char16_t*>(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<const char16_t*>(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<const char16_t*>(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<const char16_t*>(utf16.data()), utf16.size() - 1);
|
|
|
|
|
EXPECT_EQ(mixed_out, mixed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TEST(CjkResolutionTest, AdobeCNS1) {
|
|
|
|
|
using pdfengine::fonts::pdf_fonts::CjkCollectionDB;
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 100), 0x4E00);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 112), 0x4E2D);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 130), 0x4ED7);
|
2026-06-02 11:16:49 +05:30
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 131), 0x4ED8);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 135), 0x4EDF);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-CNS1", 140), 0x4F01);
|
2026-06-02 11:16:49 +05:30
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Identity-H-CNS1", 137), 0x4EE3);
|
2026-06-02 11:16:49 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(CjkResolutionTest, AdobeKorea1) {
|
|
|
|
|
using pdfengine::fonts::pdf_fonts::CjkCollectionDB;
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 101), 0xAC00);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 119), 0xAC1C);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 150), 0xAC90);
|
2026-06-02 11:16:49 +05:30
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 151), 0xAC94);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 156), 0xACA9);
|
|
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Korea1", 160), 0xACBD);
|
2026-06-02 11:16:49 +05:30
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("UniKS-UTF16-H-Korea1", 153), 0xACA0);
|
2026-06-02 11:16:49 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<int> start_flag{0};
|
|
|
|
|
std::vector<std::thread> 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<double> diff = end_time - start_time;
|
|
|
|
|
return diff.count();
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
run_benchmark(2, 1000);
|
2026-06-02 11:16:49 +05:30
|
|
|
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";
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
EXPECT_LE(cache.size(), 1000 + 16);
|
2026-06-02 11:16:49 +05:30
|
|
|
}
|
|
|
|
|
|
2026-06-11 18:19:26 +05:30
|
|
|
TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
|
|
|
|
|
SKIP_IF_NO_PDFIUM();
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> 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<FT_Face>(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<uint32_t> 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<double>(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";
|
2026-05-22 15:47:48 +05:30
|
|
|
}
|
2026-06-11 18:19:26 +05:30
|
|
|
|
|
|
|
|
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<int> 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<int> 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<int> 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<int> 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
}
|