From 4ce94132a5043317797bff2e4a86dd5ff237ef1d Mon Sep 17 00:00:00 2001 From: Manthan Terse Date: Sat, 2 May 2026 21:36:10 +0530 Subject: [PATCH 1/2] fixed_text_button --- package-lock.json | 12 ++-- src/pages/EditPdf/EditPdf.jsx | 11 +++- src/pages/EditPdf/EditPdf.test.jsx | 95 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 src/pages/EditPdf/EditPdf.test.jsx diff --git a/package-lock.json b/package-lock.json index 0e24fd5..5869ef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3199,8 +3199,9 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -3566,8 +3567,9 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -3885,8 +3887,9 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -6565,8 +6568,9 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/pages/EditPdf/EditPdf.jsx b/src/pages/EditPdf/EditPdf.jsx index 4c72d7e..89fc2bc 100644 --- a/src/pages/EditPdf/EditPdf.jsx +++ b/src/pages/EditPdf/EditPdf.jsx @@ -22,6 +22,13 @@ const ANN_TOOLS = [ { id: "rect", label: "Rectangle" }, { id: "eraser", label: "Eraser" }, ]; +const TOOL_ICONS = { + draw: Pencil, + text: Type, + highlight: Highlighter, + rect: Square, + eraser: Eraser, +}; const COLORS = ["#ef4444","#f97316","#eab308","#22c55e","#3b82f6","#a855f7","#ec4899","#000000","#ffffff"]; const HINTS = { draw:"Click and drag freely", text:"Click to place a text box", highlight:"Drag to highlight an area", rect:"Drag to draw a rectangle", eraser:"Click any annotation to remove it" }; const CURSOR = { draw:"crosshair", text:"text", highlight:"crosshair", rect:"crosshair", eraser:"cell" }; @@ -284,9 +291,9 @@ export function EditPdf() {
{ANN_TOOLS.map(({ id, label }) => ( - ))}
diff --git a/src/pages/EditPdf/EditPdf.test.jsx b/src/pages/EditPdf/EditPdf.test.jsx new file mode 100644 index 0000000..d796ae3 --- /dev/null +++ b/src/pages/EditPdf/EditPdf.test.jsx @@ -0,0 +1,95 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { EditPdf } from "./EditPdf"; + +vi.mock("../../hooks/useSubscription", () => ({ + useSubscription: () => ({ + isPremium: true, + isWalletConnected: false, + hasReachedGlobalLimit: false, + incrementUsage: vi.fn(), + }), +})); + +vi.mock("../../services/pdf.service", () => ({ + applyEdits: vi.fn(), + editPdfText: vi.fn(), +})); + +vi.mock("../../components/pdf/Dropzone", () => ({ + Dropzone: () =>
, +})); + +vi.mock("pdfjs-dist", () => ({})); + +vi.mock("../../hooks/useFileStore", async () => { + const ReactModule = await import("react"); + + const initialState = { + EditPdf_file: new File(["pdf"], "sample.pdf", { type: "application/pdf" }), + EditPdf_pages: [ + { imageData: "data:image/png;base64,abc", width: 320, height: 480, pdfWidth: 320, pdfHeight: 480 }, + ], + EditPdf_textItems: [], + EditPdf_annotations: [], + EditPdf_textEdits: {}, + }; + + return { + useFileStore: (key, fallback) => { + const [value, setValue] = ReactModule.useState( + Object.prototype.hasOwnProperty.call(initialState, key) ? initialState[key] : fallback + ); + const clearValue = () => setValue(fallback); + return [value, setValue, clearValue]; + }, + }; +}); + +describe("EditPdf", () => { + beforeEach(() => { + vi.clearAllMocks(); + HTMLCanvasElement.prototype.getContext = vi.fn(() => ({ + clearRect: vi.fn(), + save: vi.fn(), + restore: vi.fn(), + beginPath: vi.fn(), + moveTo: vi.fn(), + lineTo: vi.fn(), + stroke: vi.fn(), + fillRect: vi.fn(), + strokeRect: vi.fn(), + fillText: vi.fn(), + set strokeStyle(_) {}, + set fillStyle(_) {}, + set lineWidth(_) {}, + set lineCap(_) {}, + set lineJoin(_) {}, + set globalAlpha(_) {}, + set font(_) {}, + })); + }); + + it("shows the annotation tools including text and lets users place a text box", () => { + const { container } = render(); + + expect(screen.getByLabelText("Text")).toBeInTheDocument(); + + fireEvent.click(screen.getByLabelText("Text")); + const canvas = container.querySelector("canvas"); + expect(canvas).not.toBeNull(); + + fireEvent.mouseDown(canvas, { clientX: 80, clientY: 120 }); + + const textbox = container.querySelector("textarea"); + expect(textbox).not.toBeNull(); + + fireEvent.change(textbox, { target: { value: "Hello PDF" } }); + fireEvent.keyDown(textbox, { key: "Enter" }); + + expect(container.querySelector("textarea")).toBeNull(); + expect(screen.getByText("1 edit")).toBeInTheDocument(); + }); +}); From 6afac8c20a630df33366c1d18b0ac5687952fb06 Mon Sep 17 00:00:00 2001 From: Manthan Terse Date: Sat, 2 May 2026 22:04:43 +0530 Subject: [PATCH 2/2] Pull request overview This PR aims to fix the Edit PDF text-annotation workflow so users can select the Text tool, place a text box on the page, and type successfully. It updates the Edit PDF toolbar/overlay behavior and adds a focused regression test around the annotation flow. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: Adds icon-based annotation tool buttons with labels and explicit button type in the Edit PDF toolbar. Adjusts the inline text-box overlay styling for text annotations. Adds a new Vitest/Testing Library regression test for selecting the Text tool and committing a text edit. Reviewed changes Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments. File Description src/pages/EditPdf/EditPdf.jsx Updates the annotation tool button rendering and tweaks the text-box overlay used when placing text annotations. src/pages/EditPdf/EditPdf.test.jsx Adds a regression test covering Text tool selection, textbox placement, and committing an edit. package-lock.json Records lockfile metadata updates for existing transitive dependencies. 💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started. src/pages/EditPdf/EditPdf.jsx onKeyDown={e => { if (e.key==="Enter" && !e.shiftKey) { commitTextBox(); e.preventDefault(); } if (e.key==="Escape") setTextBox(null); }} onBlur={commitTextBox} style={{ position:"absolute", left:textBox.x, top:textBox.y - fontSize, background:"transparent", border:"1px dashed rgba(255,255,255,0.4)", color, fontSize, fontFamily:"sans-serif", lineHeight:1.2, outline:"none", resize:"none", padding:"2px 4px", minWidth:80, minHeight:fontSize+8 }} style={{ position:"absolute", left:textBox.x, top:textBox.y - fontSize, background:"transparent", border:"1px dashed rgba(255,255,255,0.4)", color:"#ffffff", fontSize, fontFamily:"sans-serif", lineHeight:1.2, outline:"none", resize:"none", padding:"2px 4px", minWidth:80, minHeight:fontSize+8, zIndex:15 }} Copilot commented 2 minutes ago Copilot Copilot AI 2 minutes ago Hard-coding the textarea preview color to white breaks the text annotation workflow: the text box no longer reflects the currently selected annotation color, and on the common white PDF background the user's typed text becomes nearly invisible until after commit. commitTextBox() still saves with color, so the preview and saved output can now disagree. Suggested changeset 1 (1) src/pages/EditPdf/EditPdf.jsx Original file line number Diff line number Diff line change onChange={e => setTextBox(t => ({ ...t, value: e.target.value }))} onKeyDown={e => { if (e.key==="Enter" && !e.shiftKey) { commitTextBox(); e.preventDefault(); } if (e.key==="Escape") setTextBox(null); }} onBlur={commitTextBox} style={{ position:"absolute", left:textBox.x, top:textBox.y - fontSize, background:"transparent", border:"1px dashed rgba(255,255,255,0.4)", color:"#ffffff", fontSize, fontFamily:"sans-serif", lineHeight:1.2, outline:"none", resize:"none", padding:"2px 4px", minWidth:80, minHeight:fontSize+8, zIndex:15 }} style={{ position:"absolute", left:textBox.x, top:textBox.y - fontSize, background:"transparent", border:"1px dashed rgba(255,255,255,0.4)", color, caretColor: color, fontSize, fontFamily:"sans-serif", lineHeight:1.2, outline:"none", resize:"none", padding:"2px 4px", minWidth:80, minHeight:fontSize+8, zIndex:15 }} /> )}
Copilot uses AI. Check for mistakes. @ManthanTerse Reply... src/pages/EditPdf/EditPdf.jsx
{ANN_TOOLS.map(({ id, label }) => ( Copilot uses AI. Check for mistakes. @ManthanTerse Reply... src/pages/EditPdf/EditPdf.jsx Comment on lines +25 to +31 const TOOL_ICONS = { draw: Pencil, text: Type, highlight: Highlighter, rect: Square, eraser: Eraser, }; Copilot commented 2 minutes ago Copilot Copilot AI 2 minutes ago TOOL_ICONS duplicates the IDs already defined in ANN_TOOLS, so future tool changes now have to update two separate data structures. If one is missed, React.createElement(TOOL_ICONS[id]) will receive undefined and fail at runtime, so keeping the icon alongside each tool definition would be safer to maintain.updated textarea overlay --- src/pages/EditPdf/EditPdf.jsx | 175 +++++++++++++++++++++++------ src/pages/EditPdf/EditPdf.test.jsx | 90 ++++++++++++++- src/services/pdf.service.js | 2 +- 3 files changed, 229 insertions(+), 38 deletions(-) diff --git a/src/pages/EditPdf/EditPdf.jsx b/src/pages/EditPdf/EditPdf.jsx index 89fc2bc..66053b7 100644 --- a/src/pages/EditPdf/EditPdf.jsx +++ b/src/pages/EditPdf/EditPdf.jsx @@ -16,22 +16,17 @@ import { FREE_LIMITS, mbToBytes } from "../../config/limits"; // ── constants ──────────────────────────────────────────────────────────────── const RENDER_SCALE = 1.5; const ANN_TOOLS = [ - { id: "draw", label: "Draw" }, - { id: "text", label: "Text" }, - { id: "highlight", label: "Highlight" }, - { id: "rect", label: "Rectangle" }, - { id: "eraser", label: "Eraser" }, + { id: "draw", label: "Draw", icon: Pencil }, + { id: "text", label: "Add Text Box", icon: Type }, + { id: "highlight", label: "Highlight", icon: Highlighter }, + { id: "rect", label: "Rectangle", icon: Square }, + { id: "eraser", label: "Eraser", icon: Eraser }, ]; -const TOOL_ICONS = { - draw: Pencil, - text: Type, - highlight: Highlighter, - rect: Square, - eraser: Eraser, -}; const COLORS = ["#ef4444","#f97316","#eab308","#22c55e","#3b82f6","#a855f7","#ec4899","#000000","#ffffff"]; -const HINTS = { draw:"Click and drag freely", text:"Click to place a text box", highlight:"Drag to highlight an area", rect:"Drag to draw a rectangle", eraser:"Click any annotation to remove it" }; +const HINTS = { draw:"Click and drag freely", text:"Click anywhere on the page to place a new text box", highlight:"Drag to highlight an area", rect:"Drag to draw a rectangle", eraser:"Click any annotation to remove it" }; const CURSOR = { draw:"crosshair", text:"text", highlight:"crosshair", rect:"crosshair", eraser:"cell" }; +const TEXTBOX_PADDING_X = 4; +const TEXTBOX_PADDING_Y = 2; // ── canvas helpers ──────────────────────────────────────────────────────────── function drawAnn(ctx, ann) { @@ -55,9 +50,7 @@ function drawAnn(ctx, ann) { ctx.strokeRect(x, y, Math.abs((ann.x2??ann.x)-ann.x), Math.abs((ann.y2??ann.y)-ann.y)); break; } case "text": - ctx.globalAlpha = ann.opacity ?? 1; - ctx.font = `${ann.fontSize??18}px sans-serif`; - ctx.fillText(ann.text, ann.x, ann.y); break; + break; } ctx.restore(); } @@ -66,11 +59,14 @@ function hitTest(ann, x, y) { const R = Math.max(10, (ann.strokeWidth??2)*3); switch (ann.type) { case "draw": return ann.points?.some(p => Math.hypot(p.x-x, p.y-y) < R); - case "text": { const fs = ann.fontSize??18; return x>=ann.x-4 && x<=ann.x+ann.text.length*fs*0.6 && y>=ann.y-fs && y<=ann.y+4; } + case "text": { const fs = ann.fontSize??18; return x>=ann.x-4 && x<=ann.x+ann.text.length*fs*0.6 && y>=ann.y-4 && y<=ann.y+fs+4; } default: return x>=Math.min(ann.x,ann.x2??ann.x)-4&&x<=Math.max(ann.x,ann.x2??ann.x)+4&&y>=Math.min(ann.y,ann.y2??ann.y)-4&&y<=Math.max(ann.y,ann.y2??ann.y)+4; } } function getPos(e, el) { const r = el.getBoundingClientRect(); return { x: e.clientX-r.left, y: e.clientY-r.top }; } +function isFormTarget(target) { + return target instanceof HTMLElement && ["TEXTAREA", "INPUT", "BUTTON"].includes(target.tagName); +} // ── component ───────────────────────────────────────────────────────────────── export function EditPdf() { @@ -100,6 +96,8 @@ export function EditPdf() { const canvasRefs = useRef({}); const drawing = useRef(null); + const draggingText = useRef(null); + const skipTextPlacement = useRef(false); const { isPremium, isWalletConnected: isConn, hasReachedGlobalLimit, incrementUsage } = useSubscription(); const LIMIT_MB = FREE_LIMITS.editPdf.maxFileSizeMb; @@ -176,15 +174,45 @@ export function EditPdf() { }); // ── 2. Draw annotation strokes on top ──────────────────────────────────── - annotations.filter(a => a.pageIndex === pi).forEach(a => drawAnn(ctx, a)); + annotations.filter(a => a.pageIndex === pi && a.type !== "text").forEach(a => drawAnn(ctx, a)); if (inProgress) drawAnn(ctx, inProgress); } // ── annotation mouse handlers ──────────────────────────────────────────────── + function openTextBox(pi, pos) { + setTextBox({ + pageIndex: pi, + x: Math.max(8, pos.x), + y: Math.max(8, pos.y), + value: "", + }); + } + function removeAnnotation(id) { + setAnnotations(prev => prev.filter(ann => ann.id !== id)); + } + function startTextDrag(e, ann) { + if (mode !== "annotate") return; + if (tool === "eraser") { + e.stopPropagation(); + removeAnnotation(ann.id); + return; + } + if (tool !== "text") return; + const pos = getPos(e, canvasRefs.current[ann.pageIndex]); + draggingText.current = { + id: ann.id, + pageIndex: ann.pageIndex, + offsetX: pos.x - ann.x, + offsetY: pos.y - ann.y, + }; + skipTextPlacement.current = true; + e.stopPropagation(); + } + function onMouseDown(e, pi) { if (mode !== "annotate") return; const pos = getPos(e, canvasRefs.current[pi]); - if (tool === "text") { setTextBox({ pageIndex: pi, x: pos.x, y: pos.y, value: "" }); return; } + if (tool === "text") return; if (tool === "eraser") { setAnnotations(prev => { const idx = [...prev].reverse().findIndex(a => a.pageIndex === pi && hitTest(a, pos.x, pos.y)); @@ -193,7 +221,26 @@ export function EditPdf() { } drawing.current = { pageIndex: pi, type: tool, color, strokeWidth: stroke, opacity, x: pos.x, y: pos.y, x2: pos.x, y2: pos.y, points: [pos] }; } + function onPageClick(e, pi) { + if (skipTextPlacement.current) { + skipTextPlacement.current = false; + return; + } + if (mode !== "annotate" || tool !== "text" || textBox || isFormTarget(e.target)) return; + const holder = e.currentTarget; + openTextBox(pi, getPos(e, holder)); + } function onMouseMove(e, pi) { + if (draggingText.current?.pageIndex === pi) { + const pos = getPos(e, canvasRefs.current[pi]); + const { id, offsetX, offsetY } = draggingText.current; + setAnnotations(prev => prev.map(ann => ( + ann.id === id + ? { ...ann, x: Math.max(8, pos.x - offsetX), y: Math.max(8, pos.y - offsetY) } + : ann + ))); + return; + } if (!drawing.current || drawing.current.pageIndex !== pi) return; const pos = getPos(e, canvasRefs.current[pi]); drawing.current.x2 = pos.x; drawing.current.y2 = pos.y; @@ -201,15 +248,35 @@ export function EditPdf() { redraw(pi, drawing.current); } function onMouseUp(e, pi) { + if (draggingText.current?.pageIndex === pi) { + draggingText.current = null; + return; + } if (!drawing.current || drawing.current.pageIndex !== pi) return; const d = drawing.current; drawing.current = null; const ok = d.type === "draw" ? d.points.length >= 2 : Math.abs(d.x2-d.x) > 3 || Math.abs(d.y2-d.y) > 3; if (ok) setAnnotations(prev => [...prev, { ...d, id: Date.now() }]); else redraw(pi, null); } - function commitTextBox() { - if (!textBox) return; - if (textBox.value.trim()) setAnnotations(prev => [...prev, { id: Date.now(), type: "text", pageIndex: textBox.pageIndex, x: textBox.x, y: textBox.y + fontSize, text: textBox.value, color, fontSize, opacity, strokeWidth: 1 }]); - setTextBox(null); + function commitTextBox(nextValue) { + setTextBox(current => { + if (!current) return null; + const value = typeof nextValue === "string" ? nextValue : current.value; + if (value.trim()) { + setAnnotations(prev => [...prev, { + id: Date.now(), + type: "text", + pageIndex: current.pageIndex, + x: current.x + TEXTBOX_PADDING_X, + y: current.y + TEXTBOX_PADDING_Y, + text: value, + color, + fontSize, + opacity, + strokeWidth: 1, + }]); + } + return null; + }); } // ── save PDF (applies both annotations and text edits) ────────────────────── @@ -248,7 +315,7 @@ export function EditPdf() {

Edit PDF

-

Draw, annotate, highlight — or click existing text to edit it directly in the browser.

+

Add text boxes, draw, and annotate your PDF pages — or switch modes to edit existing text already in the file.

{error &&
{error}
} { const f = fs[0]; if (f) { setFile(f); loadFile(f); } }} multiple={false} text="Drop a PDF to start editing" /> @@ -282,7 +349,7 @@ export function EditPdf() { Annotate
@@ -290,10 +357,10 @@ export function EditPdf() { {mode === "annotate" && <>
- {ANN_TOOLS.map(({ id, label }) => ( - ))}
@@ -308,6 +375,12 @@ export function EditPdf() { onChange={e => tool==="text"?setFontSize(+e.target.value):setStroke(+e.target.value)} className="w-16 accent-white" /> {tool==="text"?fontSize:stroke}
+ {tool === "text" && ( +
+ + This adds a new text box. Click on the page to place it, then drag the placed text to adjust. +
+ )} } {/* Edit text mode info */} @@ -358,7 +431,15 @@ export function EditPdf() { {/* ── Page area ── */}
{pages.map((page, pi) => ( -
+
onPageClick(e, pi)} + onMouseMove={e => onMouseMove(e, pi)} + onMouseUp={e => onMouseUp(e, pi)} + onMouseLeave={e => onMouseUp(e, pi)} + >
Page {pi + 1} @@ -377,6 +458,35 @@ export function EditPdf() { onMouseLeave={e => onMouseUp(e, pi)} /> + {annotations.filter(a => a.pageIndex === pi && a.type === "text").map(ann => ( +
startTextDrag(e, ann)} + onClick={e => { + if (mode === "annotate" && tool === "eraser") { + e.stopPropagation(); + removeAnnotation(ann.id); + } + }} + style={{ + position:"absolute", + left:ann.x, + top:ann.y, + color:ann.color, + fontSize:ann.fontSize, + fontFamily:"sans-serif", + lineHeight:1.2, + whiteSpace:"pre-wrap", + cursor: mode === "annotate" && tool === "text" ? "move" : mode === "annotate" && tool === "eraser" ? "cell" : "default", + pointerEvents: mode === "annotate" && (tool === "text" || tool === "eraser") ? "auto" : "none", + zIndex:12, + userSelect:"none", + }} + > + {ann.text} +
+ ))} + {/* Text item overlays (Edit Text mode) */} {mode === "edittext" && textItems.filter(t => t.pageIndex === pi).map(item => { const isEditing = editingId === item.id; @@ -420,9 +530,10 @@ export function EditPdf() { {textBox?.pageIndex === pi && (