feat: implemented base editing, underline and striking

This commit is contained in:
Furqan-14
2026-06-12 15:04:09 +05:30
parent 6678f55e46
commit d47895c990
18 changed files with 730 additions and 91 deletions
+20
View File
@@ -28,6 +28,25 @@ struct DocumentMetadata {
std::string modificationDate;
};
// Encryption + permission state of a loaded document. Booleans are the EFFECTIVE
// permissions for how the doc was opened (an owner-unlocked doc reports all true).
// The engine only surfaces these — enforcement happens in the gateway/app layers.
struct DocumentPermissions {
bool isEncrypted = false;
std::string encryption = "None"; // "None", "RC4-40", "RC4-128", "AES-128", "AES-256"
int securityRevision = -1; // FPDF_GetSecurityHandlerRevision (-1 if unencrypted)
bool ownerUnlocked = false; // encrypted but opened with full (owner) access
bool canPrint = true;
bool canPrintHighRes = true;
bool canModify = true;
bool canCopy = true; // extract text / graphics
bool canAnnotate = true; // add/modify annotations
bool canFillForms = true;
bool canExtractForAccessibility = true;
bool canAssemble = true; // insert / rotate / delete pages
};
struct PageImage {
int width;
int height;
@@ -214,6 +233,7 @@ public:
[[nodiscard]] virtual int pageCount() const noexcept = 0;
[[nodiscard]] virtual DocumentMetadata metadata() const noexcept = 0;
[[nodiscard]] virtual DocumentPermissions permissions() const noexcept = 0;
// A single entry in the document outline (bookmarks), flattened with a depth level.
struct OutlineItem {
+100 -52
View File
@@ -1299,6 +1299,10 @@ std::expected<std::vector<PdfPage::AnnotationInfo>, EngineError> PdfiumPage::ext
info.type = "ink";
} else if (subtype == FPDF_ANNOT_STRIKEOUT) {
info.type = "strikeout";
} else if (subtype == FPDF_ANNOT_UNDERLINE) {
info.type = "underline";
} else if (subtype == FPDF_ANNOT_SQUIGGLY) {
info.type = "squiggly";
} else if (subtype == FPDF_ANNOT_WIDGET) {
info.type = "widget";
@@ -1605,6 +1609,49 @@ DocumentMetadata PdfiumDocument::metadata() const noexcept {
return meta;
}
DocumentPermissions PdfiumDocument::permissions() const noexcept {
DocumentPermissions perms; // defaults: unencrypted, everything allowed
#ifdef PDFENGINE_WITH_PDFIUM
if (!doc_) return perms;
const int rev = FPDF_GetSecurityHandlerRevision(doc_);
perms.securityRevision = rev;
perms.isEncrypted = (rev != -1);
if (!perms.isEncrypted) {
return perms; // "None", all true
}
switch (rev) {
case 2: perms.encryption = "RC4-40"; break;
case 3: perms.encryption = "RC4-128"; break;
case 4: perms.encryption = "AES-128"; break; // R4 may be RC4-128; PDFium hides /CFM
case 5:
case 6: perms.encryption = "AES-256"; break;
default: perms.encryption = "Unknown"; break;
}
// FPDF_GetDocPermissions returns the EFFECTIVE /P flags for how the doc was
// opened; FPDF_GetDocUserPermissions always returns the user-level flags. When
// they differ, the doc was unlocked with the owner password (full access).
// (PDFium grants full effective perms without always returning exactly
// 0xFFFFFFFF, so a direct equality check is unreliable.)
const unsigned long p = FPDF_GetDocPermissions(doc_);
const unsigned long up = FPDF_GetDocUserPermissions(doc_);
perms.ownerUnlocked = (p != up);
auto allowed = [p](unsigned long bit) { return (p & bit) != 0ul; };
perms.canPrint = allowed(0x4); // bit 3
perms.canModify = allowed(0x8); // bit 4
perms.canCopy = allowed(0x10); // bit 5
perms.canAnnotate = allowed(0x20); // bit 6
perms.canFillForms = allowed(0x100); // bit 9
perms.canExtractForAccessibility = allowed(0x200);// bit 10
perms.canAssemble = allowed(0x400); // bit 11
perms.canPrintHighRes = allowed(0x800); // bit 12
#endif
return perms;
}
#ifdef PDFENGINE_WITH_PDFIUM
namespace {
// Depth-first flatten of the bookmark tree into OutlineItems with a depth level.
@@ -2269,13 +2316,6 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
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", 0.0);
double thickness = data.value("thickness", 1.0);
std::string color = data.value("color", "#000000");
spdlog::info("Parsed {} operation: x={}, y={}, width={}", type, x, y, width);
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
@@ -2283,59 +2323,67 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
return std::unexpected(EngineError::Unknown);
}
pdfengine::Path path;
if (type == "underline") {
path = DecorationBuilder::buildUnderline(x, y, width, thickness);
} else if (type == "strikeout") {
path = DecorationBuilder::buildStrikeout(x, y, width, thickness);
} else if (type == "squiggly") {
path = DecorationBuilder::buildSquiggly(x, y, width);
// Adobe-grade: create a real text-markup annotation (editable, deletable,
// round-trips, and positioned within the text quad by PDFium's appearance
// generator) instead of baking a fixed-offset path. Mirrors the highlight handler.
int subtype = FPDF_ANNOT_UNDERLINE;
if (type == "strikeout") subtype = FPDF_ANNOT_STRIKEOUT;
else if (type == "squiggly") subtype = FPDF_ANNOT_SQUIGGLY;
FPDF_ANNOTATION annot = FPDFPage_CreateAnnot(page, subtype);
if (!annot) {
spdlog::error("Failed to create {} annotation", type);
FPDF_ClosePage(page);
return std::unexpected(EngineError::Unknown);
}
if (!path.empty()) {
const auto& segments = path.segments();
float startX = segments[0].points[0].x;
float startY = segments[0].points[0].y;
FPDF_PAGEOBJECT pathObj = FPDFPageObj_CreateNewPath(startX, startY);
double pageHeight = FPDF_GetPageHeightF(page);
unsigned int r = 0, g = 0, b = 0;
parseHexColor(data.value("color", "#000000"), r, g, b);
FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, r, g, b, 255);
bool isFill = (type != "squiggly");
bool isStroke = (type == "squiggly");
if (data.contains("quadPoints") && data["quadPoints"].is_array()) {
FS_RECTF boundingBox;
boundingBox.left = 99999.0f;
boundingBox.right = -99999.0f;
boundingBox.top = -99999.0f;
boundingBox.bottom = 99999.0f;
for (size_t i = 1; i < segments.size(); ++i) { // Start from 1 to skip first MoveTo
const auto& seg = segments[i];
if (seg.verb == Path::Verb::MoveTo) {
FPDFPath_MoveTo(pathObj, seg.points[0].x, seg.points[0].y);
} else if (seg.verb == Path::Verb::LineTo) {
FPDFPath_LineTo(pathObj, seg.points[0].x, seg.points[0].y);
} else if (seg.verb == Path::Verb::CubicBezierTo) {
FPDFPath_BezierTo(pathObj, seg.points[0].x, seg.points[0].y,
seg.points[1].x, seg.points[1].y,
seg.points[2].x, seg.points[2].y);
} else if (seg.verb == Path::Verb::Close) {
FPDFPath_Close(pathObj);
}
for (const auto& quad : data["quadPoints"]) {
FS_QUADPOINTSF points;
// Frontend is top-down; backend is bottom-up.
points.x1 = static_cast<float>(quad.value("x1", 0.0));
points.y1 = static_cast<float>(pageHeight - quad.value("y1", 0.0));
points.x2 = static_cast<float>(quad.value("x2", 0.0));
points.y2 = static_cast<float>(pageHeight - quad.value("y2", 0.0));
points.x3 = static_cast<float>(quad.value("x3", 0.0));
points.y3 = static_cast<float>(pageHeight - quad.value("y3", 0.0));
points.x4 = static_cast<float>(quad.value("x4", 0.0));
points.y4 = static_cast<float>(pageHeight - quad.value("y4", 0.0));
FPDFAnnot_AppendAttachmentPoints(annot, &points);
float minX = (std::min)({points.x1, points.x2, points.x3, points.x4});
float maxX = (std::max)({points.x1, points.x2, points.x3, points.x4});
float minY = (std::min)({points.y1, points.y2, points.y3, points.y4});
float maxY = (std::max)({points.y1, points.y2, points.y3, points.y4});
if (minX < boundingBox.left) boundingBox.left = minX;
if (maxX > boundingBox.right) boundingBox.right = maxX;
if (minY < boundingBox.bottom) boundingBox.bottom = minY;
if (maxY > boundingBox.top) boundingBox.top = maxY;
}
FPDFPath_SetDrawMode(pathObj, isFill ? FPDF_FILLMODE_ALTERNATE : FPDF_FILLMODE_NONE, isStroke);
unsigned int r = 0, g = 0, b = 0;
parseHexColor(color, r, g, b);
if (isFill) {
FPDFPageObj_SetFillColor(pathObj, r, g, b, 255);
}
if (isStroke) {
FPDFPageObj_SetStrokeColor(pathObj, r, g, b, 255);
FPDFPageObj_SetStrokeWidth(pathObj, thickness);
}
FPDFPage_InsertObject(page, pathObj);
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after adding decoration");
if (boundingBox.left <= boundingBox.right) {
FPDFAnnot_SetRect(annot, &boundingBox);
}
}
std::string author = data.value("author", "");
if (!author.empty()) {
auto utf16 = utf8_to_utf16le(author);
FPDFAnnot_SetStringValue(annot, "T", reinterpret_cast<FPDF_WIDESTRING>(utf16.data()));
}
FPDFPage_CloseAnnot(annot);
FPDF_ClosePage(page);
} else if (type == "redaction") {
if (!op.contains("data") || !op["data"].is_object()) {
+1
View File
@@ -85,6 +85,7 @@ public:
int pageCount() const noexcept override;
DocumentMetadata metadata() const noexcept override;
DocumentPermissions permissions() const noexcept override;
std::expected<std::vector<OutlineItem>, EngineError> extractOutline() const override;
std::expected<std::shared_ptr<PdfPage>, EngineError> getPage(int pageIndex) override;