From 99f87eb7cfd27dbe2350c5b8e8a7a7cd99804c49 Mon Sep 17 00:00:00 2001 From: azeeee05 Date: Tue, 7 Jul 2026 19:03:11 +0530 Subject: [PATCH] feat: implement PDF page parsing, text extraction, and document modeling logic --- bindings/python/pdfengine_py.cpp | 16 ++++++- engine/include/pdfengine/pdf_document.hpp | 2 + engine/src/parser/pdfium_edit_pages.cpp | 16 ++----- engine/src/parser/pdfium_page_model.cpp | 15 ++++++ frontend/src/App.tsx | 24 +++++++--- frontend/src/viewer/AnnotationLayer.tsx | 58 ++++++++++++++++++++--- gateway/app/routers/documents/content.py | 1 + gateway/app/schemas/annotation.py | 1 + 8 files changed, 109 insertions(+), 24 deletions(-) diff --git a/bindings/python/pdfengine_py.cpp b/bindings/python/pdfengine_py.cpp index 96fea69..d650a5a 100644 --- a/bindings/python/pdfengine_py.cpp +++ b/bindings/python/pdfengine_py.cpp @@ -346,7 +346,21 @@ PYBIND11_MODULE(pdfengine, m) { .def_readonly("field_value", &pdfengine::PdfPage::AnnotationInfo::fieldValue) .def_readonly("field_type", &pdfengine::PdfPage::AnnotationInfo::fieldType) .def_readonly("field_flags", &pdfengine::PdfPage::AnnotationInfo::fieldFlags) - .def_readonly("field_options", &pdfengine::PdfPage::AnnotationInfo::fieldOptions); + .def_readonly("field_options", &pdfengine::PdfPage::AnnotationInfo::fieldOptions) + .def_property_readonly("quad_points", [](const pdfengine::PdfPage::AnnotationInfo& self) { + py::list out; + for (const auto& quad : self.quadPoints) { + py::list quad_list; + for (const auto& pt : quad) { + py::dict d; + d["x"] = pt.x; + d["y"] = pt.y; + quad_list.append(d); + } + out.append(quad_list); + } + return out; + }); py::class_>(m, "PdfPage") .def_property_readonly("width", &pdfengine::PdfPage::width) diff --git a/engine/include/pdfengine/pdf_document.hpp b/engine/include/pdfengine/pdf_document.hpp index 67eed1b..30d3b5e 100644 --- a/engine/include/pdfengine/pdf_document.hpp +++ b/engine/include/pdfengine/pdf_document.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace pdfengine { @@ -198,6 +199,7 @@ public: std::string timestamp; int pageIndex = 0; std::vector> paths; + std::vector> quadPoints; std::string fieldName; std::string fieldValue; diff --git a/engine/src/parser/pdfium_edit_pages.cpp b/engine/src/parser/pdfium_edit_pages.cpp index 0abeff0..0e255b1 100644 --- a/engine/src/parser/pdfium_edit_pages.cpp +++ b/engine/src/parser/pdfium_edit_pages.cpp @@ -133,22 +133,19 @@ std::expected PdfiumDocument::applyOp_pageRotation(const nloh #endif } -std::expected PdfiumDocument::applyOp_pageDeletion(const nlohmann::json& op, int pageIndex) { #ifdef PDFENGINE_WITH_PDFIUM +std::expected PdfiumDocument::applyOp_pageDeletion(const nlohmann::json& op, int pageIndex) { if (pageCount() <= 1) { spdlog::error("Cannot delete the only page in the document"); return std::unexpected(EngineError::Unknown); } FPDFPage_Delete(doc_, pageIndex); return {}; -#else - (void)op; (void)pageIndex; - return std::unexpected(EngineError::Unknown); -#endif } +#endif -std::expected PdfiumDocument::applyOp_pageReorder(const nlohmann::json& op, int pageIndex) { #ifdef PDFENGINE_WITH_PDFIUM +std::expected PdfiumDocument::applyOp_pageReorder(const nlohmann::json& op, int pageIndex) { if (!op.contains("data") || !op["data"].is_object()) { spdlog::error("page_reorder operation missing 'data' object"); return std::unexpected(EngineError::InvalidFormat); @@ -170,10 +167,7 @@ std::expected PdfiumDocument::applyOp_pageReorder(const nlohm return std::unexpected(EngineError::Unknown); } return {}; -#else - (void)op; (void)pageIndex; - return std::unexpected(EngineError::Unknown); -#endif } +#endif -} \ No newline at end of file +} \ No newline at end of file diff --git a/engine/src/parser/pdfium_page_model.cpp b/engine/src/parser/pdfium_page_model.cpp index 9adac58..46f9a80 100644 --- a/engine/src/parser/pdfium_page_model.cpp +++ b/engine/src/parser/pdfium_page_model.cpp @@ -614,6 +614,21 @@ std::expected, EngineError> PdfiumPage::ext } if (!stroke.empty()) info.paths.push_back(std::move(stroke)); } + } else if (subtype == FPDF_ANNOT_HIGHLIGHT || subtype == FPDF_ANNOT_STRIKEOUT || subtype == FPDF_ANNOT_UNDERLINE || subtype == FPDF_ANNOT_SQUIGGLY) { + const double pageH = height(); + size_t quadCount = FPDFAnnot_CountAttachmentPoints(annot); + for (size_t q = 0; q < quadCount; ++q) { + FS_QUADPOINTSF quad; + if (FPDFAnnot_GetAttachmentPoints(annot, q, &quad)) { + std::array pts = {{ + {static_cast(quad.x1), pageH - static_cast(quad.y1)}, + {static_cast(quad.x2), pageH - static_cast(quad.y2)}, + {static_cast(quad.x3), pageH - static_cast(quad.y3)}, + {static_cast(quad.x4), pageH - static_cast(quad.y4)} + }}; + info.quadPoints.push_back(pts); + } + } } result.push_back(info); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 08aa95b..65b1921 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -140,6 +140,7 @@ function App() { content: a.content, timestamp: a.timestamp, pageIndex: a.pageIndex, + quadPoints: a.quad_points || a.quadPoints, paths: Array.isArray(a.paths) && a.paths.length > 0 ? a.paths : undefined, fieldName: a.fieldName, fieldValue: a.fieldValue, @@ -267,22 +268,33 @@ function App() { const handleDecorateText = (pageIndex: number, lines: Rect[], type: 'underline' | 'strikeout' | 'squiggly', color: string) => { if (!can('canAnnotate')) { denyToast('Text decorations'); return; } - const quadPoints = lines.map((line) => { + const quadPointsBackend = lines.map((line) => { const lx = line.x / zoom, ly = line.y / zoom, lw = line.width / zoom, lh = line.height / zoom; return { x1: lx, y1: ly + lh, x2: lx + lw, y2: ly + lh, x3: lx + lw, y3: ly, x4: lx, y4: ly }; }); - const newAnnos = lines.map(line => ({ + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + const qPointsFront = lines.map((line) => { + const lx = line.x / zoom, ly = line.y / zoom, lw = line.width / zoom, lh = line.height / zoom; + if (lx < minX) minX = lx; + if (ly < minY) minY = ly; + if (lx + lw > maxX) maxX = lx + lw; + if (ly + lh > maxY) maxY = ly + lh; + return [{ x: lx, y: ly + lh }, { x: lx + lw, y: ly + lh }, { x: lx + lw, y: ly }, { x: lx, y: ly }]; + }); + + const newAnno = { id: rid('locdec'), type, pageIndex, - bbox: { x: line.x / zoom, y: line.y / zoom, width: line.width / zoom, height: line.height / zoom }, + bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }, + quadPoints: qPointsFront, color, author: 'Current User', - } as Annotation)); - setAnnotations(prev => [...prev, ...newAnnos]); + } as Annotation; + setAnnotations(prev => [...prev, newAnno]); - applyOps([{ id: rid('decor'), type, pageIndex, data: { quadPoints, color, author: 'Current User' } }]); + applyOps([{ id: rid('decor'), type, pageIndex, data: { quadPoints: quadPointsBackend, color, author: 'Current User' } }]); }; const handlePlaceText = (pageIndex: number, rectPts: Rect, text: string) => { diff --git a/frontend/src/viewer/AnnotationLayer.tsx b/frontend/src/viewer/AnnotationLayer.tsx index a320d38..651e89f 100644 --- a/frontend/src/viewer/AnnotationLayer.tsx +++ b/frontend/src/viewer/AnnotationLayer.tsx @@ -12,6 +12,7 @@ export interface Annotation { content?: string; timestamp?: string; paths?: { x: number; y: number }[][]; + quadPoints?: { x: number; y: number }[][]; pageIndex?: number; fieldName?: string; @@ -120,13 +121,22 @@ export const AnnotationLayer: React.FC = ({ top: `${scaledBbox.y + currentOffset.y * zoom}px`, width: `${scaledBbox.width}px`, height: `${scaledBbox.height}px`, - backgroundColor: anno.type === 'highlight' ? (anno.color || '#ffeb3b') : undefined, - opacity: anno.type === 'highlight' ? (anno.opacity ?? 0.4) : undefined, - mixBlendMode: anno.type === 'highlight' ? 'multiply' : undefined, + backgroundColor: anno.type === 'highlight' && (!anno.quadPoints || anno.quadPoints.length === 0) ? (anno.color || '#ffeb3b') : undefined, + opacity: anno.type === 'highlight' && (!anno.quadPoints || anno.quadPoints.length === 0) ? (anno.opacity ?? 0.4) : undefined, + mixBlendMode: anno.type === 'highlight' && (!anno.quadPoints || anno.quadPoints.length === 0) ? 'multiply' : undefined, pointerEvents: 'auto', }} title={tooltipText} > + {anno.type === 'highlight' && anno.quadPoints && anno.quadPoints.map((q, i) => { + const xMin = Math.min(q[0].x, q[1].x, q[2].x, q[3].x); + const xMax = Math.max(q[0].x, q[1].x, q[2].x, q[3].x); + const yMin = Math.min(q[0].y, q[1].y, q[2].y, q[3].y); + const yMax = Math.max(q[0].y, q[1].y, q[2].y, q[3].y); + const left = xMin * zoom - scaledBbox.x; + const top = yMin * zoom - scaledBbox.y; + return
+ })} {anno.type === 'comment' && (
@@ -134,9 +144,27 @@ export const AnnotationLayer: React.FC = ({
)} - {anno.type === 'strikeout' &&
} - {anno.type === 'underline' &&
} - {anno.type === 'squiggly' && ( + {anno.type === 'strikeout' && (!anno.quadPoints || anno.quadPoints.length === 0) &&
} + {anno.type === 'strikeout' && anno.quadPoints && anno.quadPoints.map((q, i) => { + const xMin = Math.min(q[0].x, q[1].x, q[2].x, q[3].x); + const xMax = Math.max(q[0].x, q[1].x, q[2].x, q[3].x); + const yMin = Math.min(q[0].y, q[1].y, q[2].y, q[3].y); + const yMax = Math.max(q[0].y, q[1].y, q[2].y, q[3].y); + const left = xMin * zoom - scaledBbox.x; + const top = yMin * zoom - scaledBbox.y + ((yMax - yMin) * zoom / 2); + return
+ })} + {anno.type === 'underline' && (!anno.quadPoints || anno.quadPoints.length === 0) &&
} + {anno.type === 'underline' && anno.quadPoints && anno.quadPoints.map((q, i) => { + const xMin = Math.min(q[0].x, q[1].x, q[2].x, q[3].x); + const xMax = Math.max(q[0].x, q[1].x, q[2].x, q[3].x); + const yMin = Math.min(q[0].y, q[1].y, q[2].y, q[3].y); + const yMax = Math.max(q[0].y, q[1].y, q[2].y, q[3].y); + const left = xMin * zoom - scaledBbox.x; + const top = yMax * zoom - scaledBbox.y; + return
+ })} + {anno.type === 'squiggly' && (!anno.quadPoints || anno.quadPoints.length === 0) && ( @@ -146,6 +174,24 @@ export const AnnotationLayer: React.FC = ({ )} + {anno.type === 'squiggly' && anno.quadPoints && anno.quadPoints.map((q, i) => { + const xMin = Math.min(q[0].x, q[1].x, q[2].x, q[3].x); + const xMax = Math.max(q[0].x, q[1].x, q[2].x, q[3].x); + const yMin = Math.min(q[0].y, q[1].y, q[2].y, q[3].y); + const yMax = Math.max(q[0].y, q[1].y, q[2].y, q[3].y); + const left = xMin * zoom - scaledBbox.x; + const top = yMax * zoom - scaledBbox.y; + return ( + + + + + + + + + ) + })} {anno.type === 'signature' && (
diff --git a/gateway/app/routers/documents/content.py b/gateway/app/routers/documents/content.py index 64dbfb1..926c9f8 100644 --- a/gateway/app/routers/documents/content.py +++ b/gateway/app/routers/documents/content.py @@ -112,6 +112,7 @@ def get_document_annotations(document_id: str) -> list[AnnotationResponse]: timestamp=getattr(a, "timestamp", None), pageIndex=a.page_index, paths=[[{"x": p.x, "y": p.y} for p in stroke] for stroke in getattr(a, "paths", [])], + quadPoints=getattr(a, "quad_points", []), fieldName=getattr(a, "field_name", None), fieldValue=getattr(a, "field_value", None), fieldType=getattr(a, "field_type", None), diff --git a/gateway/app/schemas/annotation.py b/gateway/app/schemas/annotation.py index 0e9ee5e..d64de6a 100644 --- a/gateway/app/schemas/annotation.py +++ b/gateway/app/schemas/annotation.py @@ -14,6 +14,7 @@ class AnnotationResponse(BaseModel): timestamp: str | None = None pageIndex: int paths: list[list[dict[str, float]]] = [] + quadPoints: list[list[dict[str, float]]] = [] fieldName: str | None = None fieldValue: str | None = None