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 >
feat(pdf-editor): convert a PDF to Word, and say what that costs
A PDF does not contain paragraphs. It contains glyphs with coordinates, and
there is no heading, no list, no table and no guaranteed reading order —
only runs of characters that happen to sit near each other. Converting to
Word means working out where the paragraphs were, from geometry. That
inference is the whole feature, and it is sometimes wrong, so this is called
a conversion and never an edit, and the dialog reports what it found before
anything is written.
Lines are grouped by baseline, runs joined with the spaces a PDF only implies
by leaving a gap, and paragraphs ended where the next line sits unusually far
below, is indented, or where the previous one stopped short of the measure.
Headings come from size relative to the body — which is the most common size
on the page, not the average, because a page of 11 pt under a 28 pt title
averages to something that is neither. Bold and italic come from the font's
name, the only place a PDF records them.
What it refuses to fake is as important. A page set in columns is reported,
not silently interleaved. A page with no text says so, and says why: it is an
image of writing, and reading that needs character recognition this editor
does not have. Tables become plain paragraphs rather than an invented grid,
because a wrong table is harder to repair than no table.
The .docx is written here rather than by a vendored builder: a Word file is a
zip of five XML parts, and the subset that can honestly be produced —
paragraphs of styled runs — is about two hundred lines. Vendoring a document
library would have added another megabyte on top of the four pdf.js and
pdf-lib already weigh, to generate markup we would still have to get right.
Entries are stored rather than deflated, which keeps a compressor out of the
add-on; the CRCs are the part that cannot be skipped, since Word calls the
file corrupt rather than naming the part that upset it.
Text replaced in place converts as replaced. Converting would otherwise hand
back the words the user had just edited away.
Checked by taking the output apart — every CRC verified, both XML parts run
through a real parser — and then, because that is still marking my own
homework, by opening the result in the Word editor extension, where mammoth
reads it with none of my code involved.
2026-09-22 22:06:54 +02:00
< button class = "btn wide" id = "convert" title = "Convert this document to an editable Word file" disabled >
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.5" stroke-linecap = "round" stroke-linejoin = "round" > < path d = "M3 2h6l4 4v8H3z" / > < path d = "M9 2v4h4" / > < path d = "M5.5 8.5l1 3 1.5-3 1.5 3 1-3" / > < / svg >
< span > Convert to Word…< / span >
< / button >
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
< 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" >
feat(pdf-editor): the five things you open a PDF to do, named and in front
Select, Text, Highlight, Draw, Sign — labelled, in that order, at the head
of the toolbar. Each one leads a kit rather than hiding it: the shapes sit
behind Draw, underline and strike-through behind Highlight, and nothing is
offered twice.
Draw arms the pen, which is what Draw means when there is one button for
it. The names drop out below 1180px, where five of them would start
pushing the zoom and colour controls off the end.
2026-09-27 18:21:55 +02:00
<!-- The five things people open a PDF to do, in front and named. Each one
leads a kit: Highlight is the first of the text markers, Draw is the
first of the drawing tools, and the rest of each kit follows behind. -->
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< div class = "tgroup primary" role = "group" aria-label = "Main tools" >
< button class = "tool big active" data-tool = "select" title = "Select (V) — click a mark, drag an area around several; double-click to type or to edit what is there" >
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
< 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 >
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< span class = "tlabel" > Select< / span >
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
< / button >
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< button class = "tool big" data-tool = "text" title = "Text (T) — click the document's own words to replace them, or anywhere else to type new text" >
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.6" stroke-linecap = "round" > < path d = "M3 3h10M8 3v10M6 13h4" / > < / svg >
< span class = "tlabel" > Text< / span >
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
< / button >
feat(pdf-editor): the five things you open a PDF to do, named and in front
Select, Text, Highlight, Draw, Sign — labelled, in that order, at the head
of the toolbar. Each one leads a kit rather than hiding it: the shapes sit
behind Draw, underline and strike-through behind Highlight, and nothing is
offered twice.
Draw arms the pen, which is what Draw means when there is one button for
it. The names drop out below 1180px, where five of them would start
pushing the zoom and colour controls off the end.
2026-09-27 18:21:55 +02:00
< button class = "tool big" data-tool = "highlight" title = "Highlight (H) — drag across the document's text" >
< 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 >
< span class = "tlabel" > Highlight< / span >
< / button >
< button class = "tool big" data-tool = "pen" title = "Draw (P) — freehand; lines, arrows and shapes are alongside" >
< 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 >
< span class = "tlabel" > Draw< / span >
< / button >
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< button class = "tool big" data-tool = "signature" title = "Signature (G) — choose one, or draw a new one" >
< svg viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" stroke-width = "1.4" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M1.5 12.5c2.2 0 2.6-7 4.2-7s1 6 2.6 6 2-3.5 3-3.5" / >
< path d = "M12.6 2.6l1.8 1.8-4.2 4.2-2.4.6.6-2.4z" / >
< path d = "M2 15h12" / >
< / svg >
< span class = "tlabel" > Sign< / span >
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
< / button >
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< / div >
feat(pdf-editor): the five things you open a PDF to do, named and in front
Select, Text, Highlight, Draw, Sign — labelled, in that order, at the head
of the toolbar. Each one leads a kit rather than hiding it: the shapes sit
behind Draw, underline and strike-through behind Highlight, and nothing is
offered twice.
Draw arms the pen, which is what Draw means when there is one button for
it. The names drop out below 1180px, where five of them would start
pushing the zoom and colour controls off the end.
2026-09-27 18:21:55 +02:00
< div class = "tgroup" role = "group" aria-label = "Shapes" >
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
< 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 >
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< 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 >
< / div >
< div class = "tgroup" role = "group" aria-label = "Mark up text" >
< 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 >
feat(pdf-editor): replace the document's own text, on its own baseline
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.
2026-09-22 21:49:37 +02:00
< / button >
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< 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 >
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
< / 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 >
feat(pdf-editor): the dock opens the editor, and the editor offers more than one way in
Clicking the dock raised a native file browser, which was the right answer
while the editor had exactly one thing to offer an empty tab. It is the
wrong answer now: a file browser can only ask which PDF, and the answer is
sometimes none of them.
So the dock opens the editor, and the empty editor says what it can do.
The drop zone stays, and learns to read what it is given — pictures become
pages, several PDFs become one document. Beside it sit the three ways in
as buttons.
Not included: compress, which cannot be done honestly without re-encoding
the images, and split, which is the page rail plus Save a copy.
A document built from pictures or joins has never been on disk, so it is
marked unsaved from the moment it opens — otherwise closing the tab would
bin it without asking. An empty editor also stops claiming to hold a file
called document.pdf.
2026-09-23 00:51:45 +02:00
< div class = "dz-sub" > Drop pictures instead and they become the pages. Drop several PDFs and they join up.< / div >
< div class = "dz-actions" >
< button class = "tile" id = "start-open" type = "button" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.6" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M6 3h8l4 4v14H6z" / > < path d = "M14 3v4h4" / > < path d = "M9 12h6M9 16h6" / >
< / svg >
< span class = "tile-t" > Open a PDF< / span >
< span class = "tile-s" > Read it, mark it up, fill it in, sign it< / span >
< / button >
< button class = "tile" id = "start-images" type = "button" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.6" stroke-linecap = "round" stroke-linejoin = "round" >
< rect x = "3" y = "5" width = "14" height = "11" rx = "1.5" / > < circle cx = "7.5" cy = "9" r = "1.3" / >
< path d = "M3.5 14l3.5-3 3 2.5 2.5-2 4 4.5" / > < path d = "M19 9v10H8" / >
< / svg >
< span class = "tile-t" > Pictures to PDF< / span >
< span class = "tile-s" > One page per picture, in the order you pick< / span >
< / button >
< button class = "tile" id = "start-merge" type = "button" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "1.6" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M4 4h7v7H4z" / > < path d = "M13 13h7v7h-7z" / > < path d = "M11 7.5h4a2 2 0 0 1 2 2V13" / > < path d = "M15 11l2 2 2-2" / >
< / svg >
< span class = "tile-t" > Join PDFs< / span >
< span class = "tile-s" > Several files, end to end, as one document< / span >
< / button >
< / div >
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
< / 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 >
feat(pdf-editor): type on the page, keep more than one signature, move what you drew
Three things the editor made you work around.
Text was typed into a dialog and then placed, so you chose a size and a
weight for words you could not see against the page they were going on.
The click now opens a caret where you clicked, in the font, size and
colour the words will have, with the style bar over it; double-clicking a
stamp reopens it in place. Lining a CSS line box up with a PDF baseline is
measured from the font's own metrics, not guessed.
A signature lived in a single slot. There was nowhere to keep initials as
well as a name, nowhere to change the one you had, and reaching for the
tool again simply stamped the first one — which is the same fault three
times: one slot. It is a library now, with redraw, rename and delete, and
the choice is made when the tool is picked up, so placing stays one click.
An existing single signature is carried into it rather than dropped.
The mark you had just drawn could be resized by its handles and not moved
by its middle, because only the select tool let marks be hit-tested at
all. The SELECTED mark now takes a press whatever tool is armed. A press
anywhere else still draws, and an unfilled shape is still grabbed by its
outline — the same rule select has always followed.
Also: words default to dark ink rather than highlighter yellow, which was
unreadable on white and is now impossible to miss, since you watch
yourself type it.
2026-09-27 13:16:07 +02:00
< h2 id = "sign-title" > Your signatures< / h2 >
<!-- The library. Each one is stored as lines, not a picture, so a
thumbnail is the same drawing at a smaller size. -->
< div id = "sign-library" >
< p class = "mhint" > Pick one to place, or draw another. They are kept in this browser,
as lines rather than pictures, so they stay sharp at any size.< / p >
< ul class = "siglist" id = "sign-list" > < / ul >
< p class = "mhint" id = "sign-empty" hidden > Nothing saved yet.< / p >
< div class = "mrow" >
< button class = "btn wide" id = "sign-new" > Draw a new one< / button >
< span class = "spacer" > < / span >
< button class = "btn wide" id = "sign-close" > Cancel< / button >
< / div >
< / div >
<!-- The pad, shown for a new signature or to redraw an existing one. -->
< div id = "sign-draw" hidden >
< p class = "mhint" > Draw in the box. The line under it is a guide, not part of what is saved.< / p >
feat(pdf-editor): one select, one text tool, and a signature you can ink and turn
Select does what selecting does in every editor people already know.
Double-click bare page and a caret opens there; double-click the document's
own words and they open for replacement; what is selected copies with
Ctrl+C, pastes with Ctrl+V and goes with Del. Nothing was taken away — a
drag on empty page still gathers an area, and a drag that starts on words
still selects words to copy.
Type text and Edit text were two buttons for one question the click already
answers. They are one Text tool: land on the document's own words and it
offers to replace them, land anywhere else and it starts new text. The
words light up under the cursor so which is which is visible before
clicking, not after.
The toolbar says what it is for. Select, Text and Sign are labelled and set
apart; the drawing kit and the markup kit are their own groups. Sign gets a
pen icon over a signature rather than a squiggle that could have been
anything.
Signatures take ink — black, blue, red, green — chosen while drawing and
kept with the signature, because people sign in a particular colour and it
belongs to the signature, not to whichever swatch was armed. And they turn:
a grip above the box, free rotation, Shift to snap to 15°, for the signing
line that is not square to the page.
Two faults the tests found, both invisible by eye:
The rotate grip was drawn in the right place and could not be grabbed —
the selection bar floats directly above a mark, which is exactly where the
grip sits, and it swallowed every click. The bar now stands clear of it.
Undo would not undo a first rotation. Restoring a mark with Object.assign
copies the keys the original HAD, so a property the drag introduced
survived the restore; the journal then recorded the rotated state as the
state to go back to. Restoring now forgets keys the original never had,
which fixes every future property with the same shape.
Also: building a document from pictures or joins refuses to start a second
one on top of the first, and says so rather than failing quietly.
2026-09-27 15:09:17 +02:00
< div class = "sigink" id = "sign-colors" role = "group" aria-label = "Ink colour" >
< button class = "swatch active" data-ink = "#101418" style = "background:#101418" title = "Black" > < / button >
< button class = "swatch" data-ink = "#1c4ed8" style = "background:#1c4ed8" title = "Blue" > < / button >
< button class = "swatch" data-ink = "#c81e1e" style = "background:#c81e1e" title = "Red" > < / button >
< button class = "swatch" data-ink = "#166534" style = "background:#166534" title = "Green" > < / button >
< / div >
feat(pdf-editor): type on the page, keep more than one signature, move what you drew
Three things the editor made you work around.
Text was typed into a dialog and then placed, so you chose a size and a
weight for words you could not see against the page they were going on.
The click now opens a caret where you clicked, in the font, size and
colour the words will have, with the style bar over it; double-clicking a
stamp reopens it in place. Lining a CSS line box up with a PDF baseline is
measured from the font's own metrics, not guessed.
A signature lived in a single slot. There was nowhere to keep initials as
well as a name, nowhere to change the one you had, and reaching for the
tool again simply stamped the first one — which is the same fault three
times: one slot. It is a library now, with redraw, rename and delete, and
the choice is made when the tool is picked up, so placing stays one click.
An existing single signature is carried into it rather than dropped.
The mark you had just drawn could be resized by its handles and not moved
by its middle, because only the select tool let marks be hit-tested at
all. The SELECTED mark now takes a press whatever tool is armed. A press
anywhere else still draws, and an unfilled shape is still grabbed by its
outline — the same rule select has always followed.
Also: words default to dark ink rather than highlighter yellow, which was
unreadable on white and is now impossible to miss, since you watch
yourself type it.
2026-09-27 13:16:07 +02:00
< canvas id = "sign-pad" width = "640" height = "220" > < / canvas >
< label class = "mfield block" > Name
< input type = "text" id = "sign-name" spellcheck = "false" placeholder = "Signature" >
< / label >
< div class = "mrow" >
< button class = "btn wide" id = "sign-clear" > Clear< / button >
< span class = "spacer" > < / span >
< button class = "btn wide" id = "sign-cancel" > Back< / button >
< button class = "btn wide primary" id = "sign-ok" disabled > Save< / button >
< / div >
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
< / div >
< / div >
feat(pdf-editor): convert a PDF to Word, and say what that costs
A PDF does not contain paragraphs. It contains glyphs with coordinates, and
there is no heading, no list, no table and no guaranteed reading order —
only runs of characters that happen to sit near each other. Converting to
Word means working out where the paragraphs were, from geometry. That
inference is the whole feature, and it is sometimes wrong, so this is called
a conversion and never an edit, and the dialog reports what it found before
anything is written.
Lines are grouped by baseline, runs joined with the spaces a PDF only implies
by leaving a gap, and paragraphs ended where the next line sits unusually far
below, is indented, or where the previous one stopped short of the measure.
Headings come from size relative to the body — which is the most common size
on the page, not the average, because a page of 11 pt under a 28 pt title
averages to something that is neither. Bold and italic come from the font's
name, the only place a PDF records them.
What it refuses to fake is as important. A page set in columns is reported,
not silently interleaved. A page with no text says so, and says why: it is an
image of writing, and reading that needs character recognition this editor
does not have. Tables become plain paragraphs rather than an invented grid,
because a wrong table is harder to repair than no table.
The .docx is written here rather than by a vendored builder: a Word file is a
zip of five XML parts, and the subset that can honestly be produced —
paragraphs of styled runs — is about two hundred lines. Vendoring a document
library would have added another megabyte on top of the four pdf.js and
pdf-lib already weigh, to generate markup we would still have to get right.
Entries are stored rather than deflated, which keeps a compressor out of the
add-on; the CRCs are the part that cannot be skipped, since Word calls the
file corrupt rather than naming the part that upset it.
Text replaced in place converts as replaced. Converting would otherwise hand
back the words the user had just edited away.
Checked by taking the output apart — every CRC verified, both XML parts run
through a real parser — and then, because that is still marking my own
homework, by opening the result in the Word editor extension, where mammoth
reads it with none of my code involved.
2026-09-22 22:06:54 +02:00
< div class = "modal" id = "modal-convert" hidden >
< h2 > Convert to Word< / h2 >
< p > A PDF stores glyphs with coordinates, not paragraphs. Converting means working out
where the paragraphs were, so the result is a < strong > rebuilt< / strong > document that reads
the same rather than a copy that looks the same.< / p >
< label class = "mfield block" > File name
< input type = "text" id = "convert-name" spellcheck = "false" >
< / label >
< ul class = "summary" id = "convert-summary" > < / ul >
< div class = "mrow" >
< span class = "spacer" > < / span >
< button class = "btn wide" id = "convert-cancel" > Cancel< / button >
< button class = "btn wide primary" id = "convert-go" > Convert< / button >
< / div >
< / div >
feat(pdf-editor): the dock opens the editor, and the editor offers more than one way in
Clicking the dock raised a native file browser, which was the right answer
while the editor had exactly one thing to offer an empty tab. It is the
wrong answer now: a file browser can only ask which PDF, and the answer is
sometimes none of them.
So the dock opens the editor, and the empty editor says what it can do.
The drop zone stays, and learns to read what it is given — pictures become
pages, several PDFs become one document. Beside it sit the three ways in
as buttons.
Not included: compress, which cannot be done honestly without re-encoding
the images, and split, which is the page rail plus Save a copy.
A document built from pictures or joins has never been on disk, so it is
marked unsaved from the moment it opens — otherwise closing the tab would
bin it without asking. An empty editor also stops claiming to hold a file
called document.pdf.
2026-09-23 00:51:45 +02:00
< div class = "modal" id = "modal-images" hidden >
< h2 > Pictures to PDF< / h2 >
< p id = "images-count" > < / p >
< div class = "mrow" >
< label class = "mfield" > Page
< select id = "images-page" >
< option value = "fit" selected > Shaped like the picture< / option >
< option value = "a4" > A4< / option >
< option value = "letter" > Letter< / option >
< / select >
< / label >
< label class = "mfield" id = "images-margin-field" > Border
< select id = "images-margin" >
< option value = "0" > None< / option >
< option value = "18" selected > Narrow< / option >
< option value = "48" > Wide< / option >
< / select >
< / label >
< / div >
< p class = "mhint" id = "images-note" > < / p >
< div class = "mrow" >
< span class = "spacer" > < / span >
< button class = "btn wide" id = "images-cancel" > Cancel< / button >
< button class = "btn wide primary" id = "images-go" > Make the PDF< / button >
< / div >
< / div >
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
< 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 >
feat(pdf-editor): the dock opens the editor, and the editor offers more than one way in
Clicking the dock raised a native file browser, which was the right answer
while the editor had exactly one thing to offer an empty tab. It is the
wrong answer now: a file browser can only ask which PDF, and the answer is
sometimes none of them.
So the dock opens the editor, and the empty editor says what it can do.
The drop zone stays, and learns to read what it is given — pictures become
pages, several PDFs become one document. Beside it sit the three ways in
as buttons.
Not included: compress, which cannot be done honestly without re-encoding
the images, and split, which is the page rail plus Save a copy.
A document built from pictures or joins has never been on disk, so it is
marked unsaved from the moment it opens — otherwise closing the tab would
bin it without asking. An empty editor also stops claiming to hold a file
called document.pdf.
2026-09-23 00:51:45 +02:00
< input type = "file" id = "img-input" accept = "image/*" multiple hidden >
< input type = "file" id = "merge-input" accept = "application/pdf,.pdf" multiple hidden >
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
< a id = "download-link" style = "display:none" > < / a >
< script type = "module" src = "editor.js" > < / script >
< / body >
< / html >