Files

689 lines
21 KiB
C++
Raw Permalink Normal View History

2026-06-29 10:51:04 +05:30
#include "document_test_helpers.hpp"
namespace pdfengine {
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"({
"version": "1.0",
"operations": [
{
"id": "op_test_1",
"type": "text_overlay",
"pageIndex": 0,
"data": {
"text": "UniqueEditedTextAnnotation123",
"x": 100.0,
"y": 150.0,
"width": 200.0,
"height": 20.0,
"fontSize": 14.0,
"fontFamily": "Helvetica",
"color": "#000000"
}
}
]
})";
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 textRes = newPage->extractText();
ASSERT_TRUE(textRes.has_value());
EXPECT_NE(textRes->find("UniqueEditedTextAnnotation123"), std::string::npos);
}
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);
}
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);
}
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;
}
}