Skip to content

Render: a tile has the pixels of the page drawn at once - #223

Merged
timzifer merged 6 commits into
mainfrom
claude/issue-211
Oct 13, 2026
Merged

timzifer merged 6 commits into
mainfrom
claude/issue-211

Conversation

@timzifer

@timzifer timzifer commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Fixes #211 (bug, P3). Stacked on #221.

The bug

Page.Render into a tile did not draw what the page drawn at once has there: one or two levels in a few antialiased pixels. The same held for the bands several workers draw — the pixels of a page depended on how many workers drew it — and for the bands of RenderGray.

First pages of the pdf.js corpus (965 files) at 72 dpi, at once against bands of 100 rows and tiles of 256 pixels: 85 files differed, none does now.

The cause

Not the sampling of images, as the issue guessed (issue9418.pdf has no image): the rasterizer. Four things, all in stilus and described in timzifer/stilus#36:

  1. an edge was cut at the region and its ends rounded there, so its slope depended on the region;
  2. a run and coverage of the same value were blended with different rounding, and how a row is cut into the two depends on the region;
  3. curves culled outside the region changed which rows of a thin stroke are filled analytically;
  4. analytic stroke rows were counted from the region's first pixel.

timzifer/stilus#37 fixes them; this takes stilus v0.10.0, which has it.

Here

  • go.mod of the eight modules that have stilus: v0.10.0.
  • TestRenderTiles: a page that is all edges (strokes from hairlines to wide ones at every slope, dashes, curves, translucent fills, a frame under a clip off the pixels) at three scales — at once, in rows, columns and tiles, by two and five workers, and in grey — byte for byte the same. With stilus v0.9.0 it fails in every one of them (two workers: 124 pixels at 108 dpi).
  • The corpus step "grey" (Render to grey and CMYK #154), which draws large pages in bands, allowed two levels for this bug and allows none.
  • Docs: RenderOptions.Region, RenderGray, docs/usage.md say that a region gets the pixels of the page.

Checked

  • The suites of the root module (also with cera_nogyre), cmd/cera, fonts/cjk, fonts/system; lint.
  • The API corpus over the pdf.js files with the stricter step.
  • Speed: the first pages of the pdf.js corpus at 144 dpi with one worker take 1451 ms with stilus v0.9.0 and 1426 ms with the change — nothing that can be measured. (In stilus's own scenes that are all edges it costs 4 %.)

fonts/cjk supplies Noto Sans JP, SC and KR, variable fonts whose default
is Thin, and the face was made of the default: text of a font a
document names and does not embed was drawn in hairlines, whatever
weight it named. The stroke of 「一」 at 100 points was three pixels.

A program a FontProvider supplies that has a weight axis is used at the
weight the document asks for: /FontWeight of the descriptor, else 700
for a bold font and 400 for any other, within what the axis has. The
instance is baked once and the last eight are kept by program and
weight, so the documents of a process share them where the provider
hands out the same bytes. A font that cannot be instanced is used as it
is, and an embedded variable font too: its writer meant the default.

The stroke is eight pixels now, wider for a bold font and thinner for
one of weight 100.

Fixes #167.
A render with RenderOptions.Form draws a widget whose value is not the
file's from a list of its own, after the page and all its annotations.
An annotation that lies on the widget and follows it in /Annots (a line
across a field, a stamp on it) was above the field as long as it showed
the file's value and under it once the value changed.

The page's list is now cut at such a widget: it is drawn up to the
widget, then the widget, then the annotations that follow, up to the
next widget something lies on. The list knows where its annotations
begin and what each of them can touch, so that a pass after the first
replays the clip to the page and the annotations of its range, in the
part of the region they touch. A widget nothing lies on is an overlay
over a page drawn in one piece, as before.

The later passes draw onto the background, as the widgets do: a blend
mode of such an annotation sees the background where the page drew
nothing.

Fixes #164
A page drawn in bands or tiles differed from the page drawn at once by
one or two levels in a few antialiased pixels, and so did a page drawn
by several workers from one drawn by one: of the first pages of 965
files of the pdf.js corpus at 72 dpi, 85.

It was not the sampling of images, as the issue guessed, but the
rasterizer: edges cut at the region and rounded there, runs and
coverage blended with different rounding, analytic stroke rows counted
from the region's first pixel (timzifer/stilus#36). stilus draws a
pixel alike in every region now; this takes that version.

TestRenderTiles draws a page that is all edges at once, in rows, columns
and tiles, by two and five workers and in grey, and holds them against
one another byte for byte. The corpus step "grey", which draws large
pages in bands, allowed two levels and allows none now.

Fixes #211
The release that has the change (timzifer/stilus#37), in place of its
commit.
@timzifer
timzifer marked this pull request as ready for review October 13, 2026 11:12
@timzifer
timzifer merged commit 33af104 into main Oct 13, 2026
38 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.

Render: a tile is not bit for bit the page drawn whole

1 participant