feat: implemented page rotation and deletion

This commit is contained in:
Furqan-14
2026-06-08 11:22:41 +05:30
parent dc6e538c81
commit ed12275976
18 changed files with 1317 additions and 328 deletions
+166 -2
View File
@@ -13,6 +13,8 @@
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <fstream>
#include <filesystem>
#include <csetjmp>
namespace pdfengine::parser {
@@ -560,6 +562,29 @@ void parseHexColor(const std::string& hex, unsigned int& r, unsigned int& g, uns
}
}
std::vector<uint8_t> base64Decode(const std::string& encoded) {
std::vector<uint8_t> decoded;
int T[256];
std::fill(std::begin(T), std::end(T), -1);
for (int i = 0; i < 64; ++i) {
T[static_cast<unsigned char>("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[i])] = i;
}
int val = 0;
int valb = -8;
for (char c : encoded) {
unsigned char uc = static_cast<unsigned char>(c);
if (T[uc] == -1) continue;
val = (val << 6) + T[uc];
valb += 6;
if (valb >= 0) {
decoded.push_back(static_cast<uint8_t>((val >> valb) & 0xFF));
valb -= 8;
}
}
return decoded;
}
}
namespace pdfengine {
@@ -1085,7 +1110,99 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
} else if (type == "redaction") {
spdlog::info("Parsed redaction edit operation (stub)");
} else if (type == "image_overlay") {
spdlog::info("Parsed image_overlay edit operation (stub)");
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("image_overlay operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
double x = data.value("x", 0.0);
double y = data.value("y", 0.0);
double width = data.value("width", 100.0);
double height = data.value("height", 100.0);
int pixelWidth = data.value("pixelWidth", 0);
int pixelHeight = data.value("pixelHeight", 0);
std::string rawPixelData = data.value("rawPixelData", "");
std::string pixelDataPath = data.value("pixelDataPath", "");
std::vector<uint8_t> decodedBytes;
if (!pixelDataPath.empty()) {
std::ifstream infile(pixelDataPath, std::ios::binary);
if (!infile) {
spdlog::error("Failed to open pixel data path: {}", pixelDataPath);
return std::unexpected(EngineError::Unknown);
}
infile.seekg(0, std::ios::end);
std::streamsize size = infile.tellg();
infile.seekg(0, std::ios::beg);
decodedBytes.resize(static_cast<size_t>(size));
if (!infile.read(reinterpret_cast<char*>(decodedBytes.data()), size)) {
spdlog::error("Failed to read pixel data from path: {}", pixelDataPath);
return std::unexpected(EngineError::Unknown);
}
infile.close();
std::error_code ec;
std::filesystem::remove(pixelDataPath, ec);
} else if (!rawPixelData.empty()) {
decodedBytes = base64Decode(rawPixelData);
} else {
spdlog::warn("image_overlay operation contains invalid raw pixels or dimensions");
continue;
}
if (decodedBytes.size() != static_cast<size_t>(pixelWidth * pixelHeight * 4)) {
spdlog::error("Decoded image bytes size mismatch. Expected: {}, Got: {}",
pixelWidth * pixelHeight * 4, decodedBytes.size());
return std::unexpected(EngineError::InvalidFormat);
}
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for image insertion", pageIndex);
return std::unexpected(EngineError::Unknown);
}
FPDF_PAGEOBJECT imgObj = FPDFPageObj_NewImageObj(doc_);
if (!imgObj) {
spdlog::error("Failed to create new image object");
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDF_BITMAP bitmap = FPDFBitmap_Create(pixelWidth, pixelHeight, 4); // 4 = FPDFBitmap_BGRA
if (!bitmap) {
spdlog::error("Failed to create FPDF_BITMAP");
FPDFPageObj_Destroy(imgObj);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
uint8_t* dest = static_cast<uint8_t*>(FPDFBitmap_GetBuffer(bitmap));
std::memcpy(dest, decodedBytes.data(), decodedBytes.size());
if (!FPDFImageObj_SetBitmap(&page, 1, imgObj, bitmap)) {
spdlog::error("Failed to set bitmap on image object");
FPDFBitmap_Destroy(bitmap);
FPDFPageObj_Destroy(imgObj);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
// Apply positioning and scaling to the image object
FPDFPageObj_Transform(imgObj, width, 0.0, 0.0, height, x, y);
// Insert into page
FPDFPage_InsertObject(page, imgObj);
// Regenerate page contents
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after image insertion");
FPDFBitmap_Destroy(bitmap);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
FPDFBitmap_Destroy(bitmap);
FPDF_ClosePage(page);
} else if (type == "highlight") {
spdlog::info("Parsed highlight edit operation (stub)");
} else if (type == "free_text") {
@@ -1095,7 +1212,54 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
} else if (type == "freehand") {
spdlog::info("Parsed freehand edit operation (stub)");
} else if (type == "page_rotation") {
spdlog::info("Parsed page_rotation edit operation (stub)");
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("page_rotation operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
int rotation = data.value("rotation", 0);
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for page rotation", pageIndex);
return std::unexpected(EngineError::Unknown);
}
int currentCode = FPDFPage_GetRotation(page);
int currentDegrees = currentCode * 90;
int newDegrees = currentDegrees + rotation;
newDegrees = (newDegrees % 360 + 360) % 360;
int newCode = newDegrees / 90;
FPDFPage_SetRotation(page, newCode);
FPDF_ClosePage(page);
} else if (type == "page_deletion") {
if (pageCount() <= 1) {
spdlog::error("Cannot delete the only page in the document");
return std::unexpected(EngineError::Unknown);
}
FPDFPage_Delete(doc_, pageIndex);
} else if (type == "page_reorder") {
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("page_reorder operation missing 'data' object");
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
if (!data.contains("destPageIndex")) {
spdlog::error("page_reorder data missing 'destPageIndex'");
return std::unexpected(EngineError::InvalidFormat);
}
int destPageIndex = data["destPageIndex"];
if (destPageIndex < 0 || destPageIndex >= pageCount()) {
spdlog::error("Destination page index {} out of bounds (total pages: {})", destPageIndex, pageCount());
return std::unexpected(EngineError::PageOutOfBounds);
}
int fromIndex = pageIndex;
if (!FPDF_MovePages(doc_, &fromIndex, 1, destPageIndex)) {
spdlog::error("FPDF_MovePages failed from {} to {}", fromIndex, destPageIndex);
return std::unexpected(EngineError::Unknown);
}
} else {
spdlog::warn("Unsupported edit operation type: {}", type);
}
+231
View File
@@ -435,6 +435,237 @@ TEST(DocumentEditTest, ApplyEditsAndIncrementalSave) {
EXPECT_NE(textRes->find("UniqueEditedTextAnnotation123"), 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);
// 1. Rotate by 90 degrees (90 total)
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());
// 2. Rotate by another 90 degrees (180 total)
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());
// Save and load back to verify 180 degree rotation (dimensions should be original again)
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);
// 3. Now rotate by -90 degrees (back to 90 total)
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);
// Swap the pages: move page 1 (index 1) to page 0 (index 0)
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(FontDiagnosticsTest, IntrospectionAccuracy) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");