Skip to content

feat: Write, the direction that did not exist - #7

Merged
tannevaled merged 1 commit into
mainfrom
feat/write
Nov 5, 2026
Merged

tannevaled merged 1 commit into
mainfrom
feat/write

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The direction that did not exist

This package reads a PDF back as a document. Everything in the fleet that produces a richdoc — Word, Excel and PowerPoint through go-ooxml, ODT, ODS, ODP and ODG through go-odf, Markdown, CSV, JSON, XML, plain text, EPUB, MOBI, FB2 and email through go-richdoc — had nowhere to go afterwards.

fleetverbs --quiet --match ToPDF clones
→ every ToPDF in the fleet is in go-pdfkit/convert, and every one of them
  takes pictures, an SVG, a ZIP of pictures or an XPS package.

Seventeen capabilities were missing for want of this (go-pdfkit/app#71).

func Write(d *richdoc.Document, opt WriteOptions) ([]byte, error)

It composes rather than typesets: go-richdoc/html.Write (v0.3.0, written for this) renders the document as a page and go-pdfkit/html2pdf.Export lays it out. Neither half is new, and a typesetter written here would be a third implementation of line breaking, page breaking and table layout in one organisation. What that pair cannot do, this cannot either — the doc comment says so rather than leaving it to be found.

⛔ A conversion must not reach the network because the file said so

Fetch nil means refuse, and that is the design, not an oversight for a caller to fix.

A richdoc arrives from something somebody was handed — a .docx, an EPUB, an email — and its images carry URLs that whoever wrote the file chose. Fetching them by default would make every conversion a request to an address of the author's choosing, from the converting machine and with its network position. That is a server-side request forgery with a document as the payload.

TestAnImageURLIsNotFetchedByDefault hands it http://169.254.169.254/latest/meta-data/ and asserts the conversion still succeeds and that OnRefused was told — because a page with holes in it must not be silent. A caller that wants sub-resources supplies its own Fetcher, which is the only way the network is reached.

⛔ The round trip found a defect that is not ours

The PDF comes back through this package's own Parse — and the words have no spaces:

Chapterone
Asentencethathastoreachthepage.
run ends at next starts gap Run.Space
Chapter → one 118.7 122.6 3.9 15.06 (= the Tf size)
A → sentence 65.2 67.2 2.0 7.53 (= the Tf size)

go-pdfkit/extract reports Run.Space as the font size rather than a space's advance (~0.25 em). No gap can exceed a whole em, so no word break is ever found. It reaches pdfops text, markdown, word, excel and the workbench's what this page says the same way, silently — as prose without spaces rather than as an error.

Filed as go-pdfkit/extract#28 with the measurement this test produced.

⛔ The test here therefore asserts every word, in order, and names what it cannot assert yet, with the issue number and the condition for tightening it — rather than quietly comparing something weaker and leaving the next reader to think spacing was never checked.

Coverage

100.0 % of statements. The three step failures are reachable through seams (renderHTML, layOut, writePDF), because a branch no test can enter is a claim nobody checks — and each failure is asserted to say which step it was, since "laying the page out" and "writing the PDF" are different problems.

🤖 Generated with Claude Code

This package reads a PDF back as a document. Everything in the fleet that
PRODUCES a richdoc -- Word, Excel and PowerPoint through go-ooxml, ODT,
ODS, ODP and ODG through go-odf, Markdown, CSV, JSON, XML, plain text,
EPUB, MOBI, FB2 and email through go-richdoc -- had nowhere to go
afterwards.

Measured on 2026-11-05 with fleetverbs: every ToPDF in the fleet was in
go-pdfkit/convert, and every one of them took pictures, an SVG, a ZIP of
pictures or an XPS package. Seventeen capabilities were missing for want
of this (go-pdfkit/app#71).

    func Write(d *richdoc.Document, opt WriteOptions) ([]byte, error)

It COMPOSES rather than typesets: go-richdoc/html.Write renders the
document as a page and go-pdfkit/html2pdf.Export lays that page out.
Neither half is new, and a typesetter written here would be a third
implementation of line breaking, page breaking and table layout in one
organisation. What that pair cannot do, this cannot do either, and the
doc comment says so rather than leaving it to be discovered.

⛔ NIL Fetch MEANS REFUSE, AND THAT IS THE POINT. A richdoc arrives from
something somebody was handed -- a .docx, an EPUB, an email -- and its
images carry URLs that whoever wrote the file chose. Fetching them by
default would make every conversion a request to an address of the
author's choosing, from the converting machine and with its network
position: a server-side request forgery with a document as the payload.
A caller that wants sub-resources supplies its own Fetcher, and
OnRefused says what the document asked for and did not get, so a page
with holes in it is not silent.

⛔ AND THE ROUND TRIP FOUND A DEFECT THAT IS NOT OURS: go-pdfkit/extract
reports Run.Space as the font SIZE rather than a space's advance (15.06
for a 15.0591 pt font, where the measured gap between "Chapter" and "one"
is 3.9). No gap can exceed a whole em, so no word break is ever found and
a converted document reads back as "Chapterone". It reaches pdfops text,
markdown, word and excel the same way, silently. Filed as
go-pdfkit/extract#28 with the measurement this test produced; the test
here asserts every word in order and NAMES what it cannot assert yet,
rather than quietly comparing something weaker.

100.0% of statements. The three step failures are reachable through
seams, because a branch no test can enter is a claim nobody checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 9e8fb69 into main Nov 5, 2026
1 check passed
@tannevaled
tannevaled deleted the feat/write branch November 5, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant