Until now "editing" a PDF here meant laying things over it. You could put a word on top of a word, but the document underneath never changed, and the result read like a sticker because it was one. This adds the thing the word Edit actually promises: click a line of the document's text, type different words, and they land where the old ones were, in the old size and the old colour. The position and size come from pdf.js's text layer, which has already placed a span over every run and carries that run's size in unscaled PDF points — so the size is right whatever the zoom, which reading it off the rendered box would not be. The colours come from the rendered page, because nothing in the text API reports them: the background is the average of the most common colour bucket in the run's box, since type is a minority of the pixels even when it is dense, and the ink is whatever sits furthest from that background. On the test fixture it recovers the marker's red exactly. Two things that look like details and are not. The bucket only chooses WHICH pixels are background; the colour itself is their average, because rebuilding it from the bucket index rounds white down to #f8f8f8 and a not-quite-white patch on a white page is a visible seam. And the cover reaches below the baseline by a quarter of the font size, because pdf.js sizes its spans to the em box: cut the cover to the span and every descender in the original line survives as a little hook under the replacement. A replacement is a cover plus text, so it is a mark like any other — movable, resizable, undoable, and rendered on screen from the same numbers the writer uses, which is what makes the preview trustworthy. Said plainly in the dialog and again in the save summary: this hides the original, it does not remove it. The old glyphs are still in the content stream underneath. Redact is the tool that takes text away, and it says so too.
263 lines
15 KiB
HTML
263 lines
15 KiB
HTML
<!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="edittext" title="Replace the document's own text (E)">
|
||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h7M5.5 4v7"/><path d="M11.5 3.5l2 2L9 10l-2.5.5L7 8z"/><path d="M2 14h12"/></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" id="text-size-field">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-note" hidden></p>
|
||
<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>
|