feat: implement PDF page parsing, text extraction, and document modeling logic
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user