Skip to content

Give a page a time budget, and take the fixes from underneath - #6

Merged
tannevaled merged 1 commit into
mainfrom
bump-pdfkit-deps
Aug 27, 2026
Merged

Give a page a time budget, and take the fixes from underneath#6
tannevaled merged 1 commit into
mainfrom
bump-pdfkit-deps

Conversation

@tannevaled

@tannevaled tannevaled commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

ops v0.5.0 → v0.7.0, render v0.7.0 → v0.10.0, reader v0.4.1 →
v0.5.0, pdffontv0.3.0 (indirect). opentype comes along to v0.10.0
because render asks for it. One code change, in renderPage.

What a person sees

Two binaries drawing the first page of every one of the 2 268 forms in
/Users/Shared/pdfforms at 72 dpi, each verified with go version -m as
carrying exactly the graph this branch resolves before and after:

old  render v0.7.0    reader v0.4.1   pdffont v0.2.0   opentype v0.9.0
new  render v0.10.0   reader v0.5.0   pdffont v0.3.0   opentype v0.10.0

Pixels are hashed; "ink" is pixels that are not the paper.

first page of the 1 633 real documents
compared 1 633
pixels differ 607 (37.2%)
more ink after 485
less ink after 99
failed to draw, either version 0 (unchanged)

Over all 2 268 files including the 635 vendor fixtures: 870 differ, 0 status
changes.

The page that was drawing its own compressed bytes

fr-cerfa/cerfa_11012.pdf, page 1: 86.9% of the page covered in ink → 5.4%.
435 115 inked pixels → 26 917.

Before, a /CCITTFaxDecode image arrived still compressed and was painted through
a stencil: a black rectangle with a band of static across the top. Noise looks
like content
, so nothing reported this as a failure — the page drew, exited
zero, and was wrong.

After, it is a legible EU certificate of origin: the boxes, the six-language
headings, the diagonal POUR INFORMATION : DOCUMENT S° CURIS° watermark.

I rendered both to PNG and looked at them.

Of the 136 real first pages that were more than half covered in ink under
render v0.7.0, 89 are drawn identically by v0.10.0 — those are genuinely dark
pages — and 47 are drawn differently. Of the 71 real forms carrying a fax,
68 first pages changed and 24 of them lost ink.

The other 485

More ink, from what render gained across v0.8.0–v0.10.0: annotations are drawn,
so a filled-in form no longer appears empty (fr-cerfa/cerfa_12766.pdf 67 256 →
277 655 inked pixels; five uk-govuk student-finance forms all roughly 65 000 →
205 000); soft masks and transparency groups; a CIDFontType2 subset with no
cmap no longer hiding every glyph.

The one code change

render will stop drawing a page when its time is up and hand back as far as it
got — but only if the caller says how long, and until now nothing here did. A
browser tab is exactly the caller that cannot wait: of 59 432 pages render was
measured against, 1 131 were still being drawn after twenty seconds and one took
273. renderPage now sets five seconds and shows the part that was drawn
rather than treating ErrTimedOut as a page that cannot be drawn at all.

Showing half a page without saying so would be the one thing worse than showing
nothing, so the status line says which it is:

this page was still being drawn after 5s; this is as far as it got

Two tests cover it — a timeout that returns a partial image, and a timeout that
returns none — and both assert the note as well as the pixels.

render.Options gained AllLayers in v0.10.0. I left it at its default: a viewer
should show the configuration the document itself asks for.

Checked, not changed

reader.ImageFilter no longer answers true for /CCITTFaxDecode or /CCF.
Nothing in this module calls ImageFilter or switches on a filter name at all
render does that work now.

Not measured

Wall clock and peak memory. Three other corpus jobs were running on this machine,
so any timing figure would be fiction. Pixel hashes do not depend on load.

Gates

go vet, gofmt, -race, exact 100% statement coverage, the browsercheck
module's own vet and gofmt, the js/wasm build, and vet on all nine targets pass
locally with GOWORK=off CGO_ENABLED=0. CI's Drive it in a real browser job —
which starts Chrome, hands the file picker a document and reads the canvas back —
passes on this branch.

@tannevaled
tannevaled force-pushed the bump-pdfkit-deps branch 2 times, most recently from a3260b8 to d78b221 Compare August 27, 2026 13:25
ops v0.5.0 -> v0.7.0, render v0.7.0 -> v0.10.0, reader v0.4.1 -> v0.5.0,
pdffont -> v0.3.0 indirect. opentype comes along to v0.10.0 because render
asks for it.

What the workbench gains without asking:

  - A page stops drawing its own compressed bytes. A /CCITTFaxDecode image
    arrived still compressed and was painted through a stencil, so a scan
    came out as noise -- and noise looks like content. The first page of
    fr-cerfa/cerfa_11012.pdf was 86.9% covered in ink and is now 5.4%: a
    black rectangle with a band of static across the top, in place of a
    legible EU certificate of origin.
  - Annotations are drawn. A filled-in form used to appear empty, because
    what a person typed lives in the widget beside the content, not in it.
  - Soft masks, and a transparency group drawn as one thing.
  - A composite font's identifiers reach its glyphs through the charset, so
    a CIDFontType2 subset carrying no cmap no longer hides every glyph.
  - A clip is kept as the box it covers rather than as a value for every
    pixel of the page.
  - A form's field list and its structure tree survive a rotate, so the
    boxes keep their meaning and a screen reader keeps its reading order.
  - An inline image's dictionary is read in a settled order, so a page
    carrying both spellings of a key -- /W beside /Width -- no longer draws
    differently from one run to the next.

What it had to ask for is the time budget. render will stop drawing a page
when its time is up and hand back as far as it got, but only if the caller
says how long -- and a browser tab is exactly the caller that cannot wait:
of 59 432 corpus pages, 1 131 were still being drawn after twenty seconds
and one took two hundred and seventy-three. So renderPage sets five seconds
and shows the part that was drawn, instead of treating ErrTimedOut as a page
that cannot be drawn at all. Half a figure is worth more to somebody
scrolling than a sentence saying there was one.

Showing half a page without saying so would be the one thing worse than
showing nothing, so the status line says which it is. That is the only
place renderPage writes the note, and it overwrites what was there,
because what is on the screen now matters more than what happened before.
@tannevaled
tannevaled merged commit 20f8ee9 into main Aug 27, 2026
2 checks passed
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