feat: implement PDF page parsing, text extraction, and document modeling logic

This commit is contained in:
azeeee05
2026-07-07 19:03:11 +05:30
parent a383bd8704
commit 99f87eb7cf
8 changed files with 109 additions and 24 deletions
+15 -1
View File
@@ -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_<pdfengine::PdfPage, std::shared_ptr<pdfengine::PdfPage>>(m, "PdfPage")
.def_property_readonly("width", &pdfengine::PdfPage::width)
@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include <cstdint>
#include <array>
namespace pdfengine {
@@ -198,6 +199,7 @@ public:
std::string timestamp;
int pageIndex = 0;
std::vector<std::vector<Point2D>> paths;
std::vector<std::array<Point2D, 4>> quadPoints;
std::string fieldName;
std::string fieldValue;
+5 -11
View File
@@ -133,22 +133,19 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_pageRotation(const nloh
#endif
}
std::expected<void, EngineError> PdfiumDocument::applyOp_pageDeletion(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
std::expected<void, EngineError> 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<void, EngineError> PdfiumDocument::applyOp_pageReorder(const nlohmann::json& op, int pageIndex) {
#ifdef PDFENGINE_WITH_PDFIUM
std::expected<void, EngineError> 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<void, EngineError> PdfiumDocument::applyOp_pageReorder(const nlohm
return std::unexpected(EngineError::Unknown);
}
return {};
#else
(void)op; (void)pageIndex;
return std::unexpected(EngineError::Unknown);
#endif
}
#endif
}
}
+15
View File
@@ -614,6 +614,21 @@ std::expected<std::vector<PdfPage::AnnotationInfo>, 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<Point2D, 4> pts = {{
{static_cast<double>(quad.x1), pageH - static_cast<double>(quad.y1)},
{static_cast<double>(quad.x2), pageH - static_cast<double>(quad.y2)},
{static_cast<double>(quad.x3), pageH - static_cast<double>(quad.y3)},
{static_cast<double>(quad.x4), pageH - static_cast<double>(quad.y4)}
}};
info.quadPoints.push_back(pts);
}
}
}
result.push_back(info);
+18 -6
View File
@@ -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) => {
+52 -6
View File
@@ -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<AnnotationLayerProps> = ({
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 <div key={i} className="absolute" style={{ left, top, width: (xMax - xMin) * zoom, height: (yMax - yMin) * zoom, backgroundColor: anno.color || '#ffeb3b', opacity: anno.opacity ?? 0.4, mixBlendMode: 'multiply' }} />
})}
{anno.type === 'comment' && (
<div className="comment-icon" style={{ width: '100%', height: '100%', color: anno.color || '#facc15' }}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-full h-full drop-shadow-md">
@@ -134,9 +144,27 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
</svg>
</div>
)}
{anno.type === 'strikeout' && <div className="w-full h-[1.5px] opacity-[0.85] absolute top-1/2 -translate-y-1/2" style={{ backgroundColor: anno.color || '#dc2626' }} />}
{anno.type === 'underline' && <div className="w-full h-[1.5px] opacity-[0.85] absolute bottom-0" style={{ backgroundColor: anno.color || '#2563eb' }} />}
{anno.type === 'squiggly' && (
{anno.type === 'strikeout' && (!anno.quadPoints || anno.quadPoints.length === 0) && <div className="w-full h-[1.5px] opacity-[0.85] absolute top-1/2 -translate-y-1/2" style={{ backgroundColor: anno.color || '#dc2626' }} />}
{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 <div key={i} className="absolute h-[1.5px] opacity-[0.85] -translate-y-1/2" style={{ left, top, width: (xMax - xMin) * zoom, backgroundColor: anno.color || '#dc2626' }} />
})}
{anno.type === 'underline' && (!anno.quadPoints || anno.quadPoints.length === 0) && <div className="w-full h-[1.5px] opacity-[0.85] absolute bottom-0" style={{ backgroundColor: anno.color || '#2563eb' }} />}
{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 <div key={i} className="absolute h-[1.5px] opacity-[0.85]" style={{ left, top, width: (xMax - xMin) * zoom, backgroundColor: anno.color || '#2563eb' }} />
})}
{anno.type === 'squiggly' && (!anno.quadPoints || anno.quadPoints.length === 0) && (
<svg width="100%" height="4" xmlns="http://www.w3.org/2000/svg" style={{position: 'absolute', bottom: 0, left: 0, opacity: 0.85}}>
<defs>
<pattern id={`sq-${anno.id}`} x="0" y="0" width="6" height="4" patternUnits="userSpaceOnUse">
@@ -146,6 +174,24 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
<rect x="0" y="0" width="100%" height="4" fill={`url(#sq-${anno.id})`} />
</svg>
)}
{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 (
<svg key={i} style={{position: 'absolute', left, top, width: (xMax - xMin) * zoom, height: 4, opacity: 0.85}} xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id={`sq-${anno.id}-${i}`} x="0" y="0" width="6" height="4" patternUnits="userSpaceOnUse">
<path d="M 0 2 Q 1.5 0 3 2 T 6 2" fill="none" stroke={anno.color || '#16a34a'} strokeWidth="1.2" />
</pattern>
</defs>
<rect x="0" y="0" width="100%" height="4" fill={`url(#sq-${anno.id}-${i})`} />
</svg>
)
})}
{anno.type === 'signature' && (
<div className="text-[rgba(37,99,235,0.85)] bg-[rgba(255,255,255,0.85)] rounded-full p-1 shadow-[0_1px_2px_rgba(16,24,40,0.06),0_1px_3px_rgba(16,24,40,0.10)]">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+1
View File
@@ -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),
+1
View File
@@ -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