689 lines
21 KiB
C++
689 lines
21 KiB
C++
#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;
|
||
|
|
}
|
||
|
|
}
|