feat(pdf-editor): a mark you placed is something you can still work on
Everything the editor put on a page was final. A text stamp could not be
corrected without deleting it and typing it again, nothing could be resized,
and the only way to remove a mark was a Delete key nobody had been told
about — the selection drew a dashed box and offered no action at all. Placing
a stamp also left its tool armed, so the next click stamped a second copy.
Marks are now editable objects. Selecting one gives it grab handles and a
small bar pinned above it: delete and duplicate for anything, and for text an
edit button, a size stepper and bold and italic. Double-clicking text reopens
it for rewriting in place rather than adding a second one. Placing a text
stamp or a signature drops straight back to the select tool with the new mark
live, which is both what people expect and what puts it immediately within
reach of a nudge.
Resizing is one function over every mark type rather than a special case per
kind: a handle drag produces a new bounding box, and the mark is mapped from
its old box into that one. Text scales by font size instead of stretching its
glyphs, signatures keep their aspect on a corner, and lines offer their two
endpoints instead of a box that would let you stretch them in ways you never
aimed at. A whole gesture lands on the undo stack as one step.
Selecting a thin mark used to mean clicking its outline exactly — about one
screen pixel. Each stroked mark now carries an invisible fat copy of itself
purely to catch the pointer.
New marks to go with it: underline and strike-through, which share the
highlight's text-selection geometry and differ only in where the rule sits; a
plain line; and a fill toggle for rectangles and ellipses. Bold and italic
mean three more Helvetica variants embedded at save time, since a PDF treats
them as separate fonts rather than as a style.
Double-click is detected from the pointer stream rather than from a dblclick
listener, because selecting a mark calls preventDefault() on the pointerdown
and that suppresses the compatibility mouse events the browser would have
synthesised the dblclick from.
2026-09-21 03:13:06 +02:00
|
|
|
|
<!doctype html>
|
|
|
|
|
|
<html lang="en">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
<title>PDF Editor</title>
|
|
|
|
|
|
<link rel="stylesheet" href="vendor/pdfjs/web/pdf_viewer.css">
|
|
|
|
|
|
<link rel="stylesheet" href="editor.css">
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body data-empty="1">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Top bar: document identity on the left, destructive/close actions right.
|
|
|
|
|
|
Same split as the screenshot editor's topbar. -->
|
|
|
|
|
|
<div class="topbar">
|
|
|
|
|
|
<button class="btn wide" id="open" title="Open a PDF (Ctrl+O)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 5h4l1.5 2H14v6H2z"/><path d="M2 5V3h5"/></svg>
|
|
|
|
|
|
<span>Open</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span class="sep"></span>
|
|
|
|
|
|
<span class="docname" id="docname">No document</span>
|
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
|
<button class="btn wide primary" id="save" title="Save a copy (Ctrl+S)" disabled>
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9M4 7l4 4 4-4M2 14h12"/></svg>
|
|
|
|
|
|
<span>Save a copy</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn danger" id="discard" title="Close this tab without saving (Esc)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Tool bar. `.tool-cluster` centres the drawing groups so the view
|
|
|
|
|
|
controls can right-anchor without the tools crowding the left edge. -->
|
|
|
|
|
|
<div class="toolbar" id="toolbar">
|
|
|
|
|
|
<div class="tool-cluster">
|
|
|
|
|
|
<div class="tgroup" role="group" aria-label="Tools">
|
|
|
|
|
|
<button class="tool active" data-tool="select" title="Select — move or delete a mark, select text (V)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2l4 11 1.6-4.4L13 7z"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="highlight" title="Highlight — drag across text (H)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h10"/><path d="M5 9.5l4.5-6.5 3 2L8 11.5z"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="underline" title="Underline — drag across text (U)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 2v5a4 4 0 0 0 8 0V2"/><path d="M3 14h10"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="strikeout" title="Strike through — drag across text (K)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 3.5h8"/><path d="M8 3.5V12"/><path d="M2.5 8h11"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="pen" title="Pen — freehand (P)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 14c1-3 3-6 6-8s5-3 6-3l-3 4c-2 1-4 3-6 4s-2 2-3 3z"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="rect" title="Rectangle (R)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="3.5" width="11" height="9"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="ellipse" title="Ellipse (O)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><ellipse cx="8" cy="8" rx="5.5" ry="4"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="line" title="Line (L)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 13L13 3"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="arrow" title="Arrow (A)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13L13 3M13 3H8M13 3v5"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="text" title="Text stamp (T)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 3h10M8 3v10M6 13h4"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="signature" title="Signature stamp (G)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 11c2 0 3-7 4.5-7S8 10 9.5 10 12 6 13.5 6"/><path d="M2 14h12"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="tool" data-tool="redact" title="Redact — drag a box over content to remove it" data-warn="1">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="4.5" width="11" height="7" fill="currentColor"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tgroup" role="group" aria-label="Colours">
|
|
|
|
|
|
<button class="swatch active" data-color="#ffd400" style="background:#ffd400" title="Yellow"></button>
|
|
|
|
|
|
<button class="swatch" data-color="#ff3b30" style="background:#ff3b30" title="Red"></button>
|
|
|
|
|
|
<button class="swatch" data-color="#0aa8ff" style="background:#0aa8ff" title="Blue"></button>
|
|
|
|
|
|
<button class="swatch" data-color="#2bd97c" style="background:#2bd97c" title="Green"></button>
|
|
|
|
|
|
<button class="swatch" data-color="#1a1f2b" style="background:#1a1f2b" title="Ink"></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tgroup" role="group" aria-label="Widths">
|
|
|
|
|
|
<button class="width" data-width="1.5" title="Thin"><span class="dot" style="width:4px;height:4px"></span></button>
|
|
|
|
|
|
<button class="width active" data-width="2.5" title="Medium"><span class="dot" style="width:7px;height:7px"></span></button>
|
|
|
|
|
|
<button class="width" data-width="4.5" title="Thick"><span class="dot" style="width:11px;height:11px"></span></button>
|
|
|
|
|
|
<button class="btn" id="toggle-fill" title="Fill shapes as well as outline them">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="3.5" width="11" height="9"/><path d="M2.5 9.5h11v3h-11z" fill="currentColor" stroke="none"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tgroup" role="group" aria-label="History">
|
|
|
|
|
|
<button class="btn" id="undo" title="Undo (Ctrl+Z)" disabled>
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 8c0-3 2-5 5-5s5 2 5 5-2 5-5 5"/><path d="M6 5L3 8l3 3"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="redo" title="Redo (Ctrl+Shift+Z)" disabled>
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M13 8c0-3-2-5-5-5S3 5 3 8s2 5 5 5"/><path d="M10 5l3 3-3 3"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tgroup zoomgroup" role="group" aria-label="Zoom">
|
|
|
|
|
|
<button class="btn" id="zoom-out" title="Zoom out (−)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3 8h10"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<select id="zoom" title="Zoom">
|
|
|
|
|
|
<option value="page-width">Fit width</option>
|
|
|
|
|
|
<option value="page-fit">Fit page</option>
|
|
|
|
|
|
<option value="0.5">50%</option>
|
|
|
|
|
|
<option value="0.75">75%</option>
|
|
|
|
|
|
<option value="1">100%</option>
|
|
|
|
|
|
<option value="1.25">125%</option>
|
|
|
|
|
|
<option value="1.5">150%</option>
|
|
|
|
|
|
<option value="2">200%</option>
|
|
|
|
|
|
<option value="4">400%</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button class="btn" id="zoom-in" title="Zoom in (+)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="toggle-rail" title="Show or hide the page thumbnails">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="7" height="12"/><rect x="11" y="2" width="3" height="5"/><rect x="11" y="9" width="3" height="5"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="body">
|
|
|
|
|
|
<!-- Scroll container. pdf.js's own CSS keys off #viewer .page, and reads
|
|
|
|
|
|
--scale-factor from the container, so the structure below mirrors what
|
|
|
|
|
|
the pdf.js viewer builds. -->
|
|
|
|
|
|
<div class="viewerContainer" id="viewerContainer">
|
|
|
|
|
|
<div id="viewer" class="pdfViewer"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="dropzone" id="dropzone">
|
|
|
|
|
|
<div class="dz-inner">
|
|
|
|
|
|
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
|
<path d="M12 6h16l8 8v28H12z"/><path d="M28 6v8h8"/><path d="M24 22v12M19 29l5 5 5-5"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<div class="dz-title">Drop a PDF here</div>
|
|
|
|
|
|
<div class="dz-sub">or <button class="linkish" id="open2">choose a file</button></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Page rail. Thumbnails are draggable to reorder; each carries rotate
|
|
|
|
|
|
and delete buttons. -->
|
|
|
|
|
|
<aside class="rail" id="rail">
|
|
|
|
|
|
<div class="rail-head">
|
|
|
|
|
|
<span id="rail-count">0 pages</span>
|
|
|
|
|
|
<button class="fbtn" id="restore-pages" hidden title="Bring back every deleted page">Undo deletes</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rail-list" id="rail-list"></div>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="footer">
|
|
|
|
|
|
<span class="fstat" id="status">Ready</span>
|
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
|
<span class="fstat" id="pageinfo"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Modal shell, reused by the redaction warning, the signature pad and the
|
|
|
|
|
|
text-stamp prompt. One at a time; `data-modal` names the open one. -->
|
|
|
|
|
|
<div class="scrim" id="scrim" hidden>
|
|
|
|
|
|
<div class="modal" id="modal-redact" hidden>
|
|
|
|
|
|
<h2>About redaction</h2>
|
|
|
|
|
|
<p>A redaction box hides content visually. On its own it does <strong>not</strong> remove the
|
|
|
|
|
|
text underneath, which stays extractable by anyone who copies from the file.</p>
|
|
|
|
|
|
<p>To actually remove it, this editor rebuilds each redacted page as an image when you save.
|
|
|
|
|
|
That page's text stops being selectable and any form fields on it become static. Pages you
|
|
|
|
|
|
did not redact are untouched.</p>
|
|
|
|
|
|
<label class="check"><input type="checkbox" id="redact-flatten" checked>
|
|
|
|
|
|
<span>Rebuild redacted pages as images when saving (recommended)</span></label>
|
|
|
|
|
|
<div class="mrow">
|
|
|
|
|
|
<button class="btn wide" id="redact-cancel">Cancel</button>
|
|
|
|
|
|
<button class="btn wide primary" id="redact-ok">Use the redaction tool</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="modal" id="modal-sign" hidden>
|
|
|
|
|
|
<h2>Draw your signature</h2>
|
|
|
|
|
|
<p class="mhint">Draw in the box, then place it on the page. It is stored as lines, not a
|
|
|
|
|
|
picture, so it stays sharp at any zoom.</p>
|
|
|
|
|
|
<canvas id="sign-pad" width="640" height="220"></canvas>
|
|
|
|
|
|
<div class="mrow">
|
|
|
|
|
|
<button class="btn wide" id="sign-clear">Clear</button>
|
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
|
<button class="btn wide" id="sign-cancel">Cancel</button>
|
|
|
|
|
|
<button class="btn wide primary" id="sign-ok" disabled>Use this signature</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="modal" id="modal-text" hidden>
|
|
|
|
|
|
<h2 id="modal-text-title">Text stamp</h2>
|
|
|
|
|
|
<textarea id="text-body" rows="4" placeholder="Type the text to place…"></textarea>
|
|
|
|
|
|
<div class="mrow">
|
|
|
|
|
|
<label class="mfield">Size
|
|
|
|
|
|
<select id="text-size">
|
|
|
|
|
|
<option value="8">8</option><option value="10">10</option>
|
|
|
|
|
|
<option value="12" selected>12</option><option value="14">14</option>
|
|
|
|
|
|
<option value="18">18</option><option value="24">24</option>
|
|
|
|
|
|
<option value="36">36</option><option value="48">48</option><option value="72">72</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button class="btn styl" id="text-bold" type="button" title="Bold"><b>B</b></button>
|
|
|
|
|
|
<button class="btn styl" id="text-italic" type="button" title="Italic"><i>I</i></button>
|
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
|
<button class="btn wide" id="text-cancel">Cancel</button>
|
|
|
|
|
|
<button class="btn wide primary" id="text-ok">Place</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="mhint" id="text-warn" hidden></p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="modal" id="modal-save" hidden>
|
|
|
|
|
|
<h2>Save a copy</h2>
|
|
|
|
|
|
<label class="mfield block">File name
|
|
|
|
|
|
<input type="text" id="save-name" spellcheck="false">
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<ul class="summary" id="save-summary"></ul>
|
|
|
|
|
|
<div class="mrow">
|
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
|
<button class="btn wide" id="save-cancel">Cancel</button>
|
|
|
|
|
|
<button class="btn wide primary" id="save-go">Save</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Pinned to the current selection; see paintSelectionBar in editor.js. -->
|
|
|
|
|
|
<div class="selbar" id="selbar" hidden>
|
|
|
|
|
|
<button class="sb" id="sb-edit" title="Edit this text (or double-click it)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11 2.5l2.5 2.5L6 12.5 3 13l.5-3z"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span class="sb-group" id="sb-textsize">
|
|
|
|
|
|
<button class="sb" id="sb-smaller" title="Smaller">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M3.5 8h9"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span class="sb-val" id="sb-size">12</span>
|
|
|
|
|
|
<button class="sb" id="sb-bigger" title="Bigger">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M8 3.5v9M3.5 8h9"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button class="sb" id="sb-bold" title="Bold"><b>B</b></button>
|
|
|
|
|
|
<button class="sb" id="sb-italic" title="Italic"><i>I</i></button>
|
|
|
|
|
|
<button class="sb" id="sb-fill" title="Fill">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="3.5" width="11" height="9"/><path d="M2.5 9.5h11v3h-11z" fill="currentColor" stroke="none"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span class="sb-sep"></span>
|
|
|
|
|
|
<button class="sb" id="sb-dup" title="Duplicate">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5.5" y="5.5" width="8" height="8" rx="1"/><path d="M3.5 10.5V3a1 1 0 0 1 1-1H11"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="sb danger" id="sb-del" title="Delete (Del)">
|
|
|
|
|
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4h10"/><path d="M6 4V2h4v2"/><path d="M4 4l1 10h6l1-10"/><path d="M7 6.5v5M9 6.5v5"/></svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
|
|
<input type="file" id="file-input" accept="application/pdf,.pdf" hidden>
|
|
|
|
|
|
<a id="download-link" style="display:none"></a>
|
|
|
|
|
|
|
|
|
|
|
|
<script type="module" src="editor.js"></script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|