feat: implement backend and frontend support for PDF text overlays and annotations

This commit is contained in:
azeeee05
2026-07-08 15:53:17 +05:30
parent 99f87eb7cf
commit 62ad42b116
5 changed files with 25 additions and 6 deletions
@@ -129,6 +129,7 @@ std::expected<void, EngineError> PdfiumDocument::applyOp_decoration(const nlohma
int subtype = FPDF_ANNOT_UNDERLINE;
if (type == "strikeout") subtype = FPDF_ANNOT_STRIKEOUT;
else if (type == "squiggly") subtype = FPDF_ANNOT_SQUIGGLY;
else if (type == "highlight") subtype = FPDF_ANNOT_HIGHLIGHT;
FPDF_ANNOTATION annot = FPDFPage_CreateAnnot(page, subtype);
if (!annot) {
+2 -2
View File
@@ -270,7 +270,7 @@ function App() {
if (!can('canAnnotate')) { denyToast('Text decorations'); return; }
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 };
return { x1: lx, y1: ly, x2: lx + lw, y2: ly, x3: lx, y3: ly + lh, x4: lx + lw, y4: ly + lh };
});
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
@@ -280,7 +280,7 @@ function App() {
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 }];
return [{ x: lx, y: ly }, { x: lx + lw, y: ly }, { x: lx, y: ly + lh }, { x: lx + lw, y: ly + lh }];
});
const newAnno = {
+4 -2
View File
@@ -194,8 +194,10 @@ export class TextSelectionModel {
}
const rects: SelRect[] = [];
for (const arr of byLine.values()) {
const x = Math.min(...arr.map((g) => g.x));
const right = Math.max(...arr.map((g) => g.right));
const nonSpace = arr.filter(g => !/^\s+$/.test(g.text));
const measureArr = nonSpace.length > 0 ? nonSpace : arr;
const x = Math.min(...measureArr.map((g) => g.x));
const right = Math.max(...measureArr.map((g) => g.right));
const band = this.lines[arr[0].line];
rects.push({ x, y: band.top, w: right - x, h: band.bottom - band.top });
}
+1 -1
View File
@@ -115,7 +115,7 @@ export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
onPointerDown={(e) => handlePointerDown(e, anno)}
onPointerMove={(e) => handlePointerMove(e, anno.id)}
onPointerUp={(e) => handlePointerUp(e, anno)}
className={`absolute ${isDraggable(anno.type) ? 'cursor-move' : 'cursor-pointer'} rounded-[2px] transition-[opacity,box-shadow] duration-150 hover:shadow-[0_2px_8px_rgba(16,24,40,0.18)] type-${anno.type} ${isDragging ? 'shadow-lg z-50' : ''}`}
className={`absolute ${isDraggable(anno.type) ? 'cursor-move' : 'cursor-pointer'} rounded-[2px] transition-[opacity,box-shadow] duration-150 ${['highlight', 'strikeout', 'underline', 'squiggly'].includes(anno.type) ? '' : 'hover:shadow-[0_2px_8px_rgba(16,24,40,0.18)]'} type-${anno.type} ${isDragging ? 'shadow-lg z-50' : ''}`}
style={{
left: `${scaledBbox.x + currentOffset.x * zoom}px`,
top: `${scaledBbox.y + currentOffset.y * zoom}px`,
+17 -1
View File
@@ -283,6 +283,20 @@ class SquigglyOperation(BaseModel):
pageIndex: int = Field(..., ge=0)
data: DecorationData
class SignatureData(BaseModel):
x: float
y: float
width: float
height: float
image_data: str | None = None
author: str = "Signer"
class SignatureOperation(BaseModel):
id: str
type: Literal["signature"]
pageIndex: int = Field(..., ge=0)
data: SignatureData
EditOperation = Annotated[
TextOverlayOperation
| RedactionOperation
@@ -301,7 +315,9 @@ EditOperation = Annotated[
| ReflowParagraphOperation
| UnderlineOperation
| StrikeoutOperation
| SquigglyOperation,
| SquigglyOperation
| SignatureOperation
,
Field(discriminator="type"),
]