Skip to main content

PDF page raster

A produced PDF can be turned into one PNG per page, so a page can be reviewed as an image: by a human outside a PDF viewer, or by a model with vision. The rasterizer is a copy of pdf.js shipped inside the binary and driven in the Chromium the renderer already requires — no native dependency, no cgo, and nothing added to the cross-compiled build.

The path a page takes

  1. Rasterizer.RasterizePDF puts the PDF bytes in a blob store keyed by an unguessable ID, exactly as a document to render is stored.
  2. It borrows one browser from the render pool — one borrow for the whole document, not one per page, because --render-pool-max-total defaults to 1.
  3. It navigates that page to the harness on the internal loopback server, which fetches the PDF back from the blob store by ID.
  4. Per page, the harness draws into a canvas at the document origin and the driver captures it with Page.captureScreenshot clipped to the canvas box, then hands that page to the caller before drawing the next.
  5. The browser goes back to the pool and the blob store entry is dropped.

Pages are handed over rather than returned as a slice, so a caller that writes each image out — into a bundle directory, or into a tar on an HTTP response — holds one PNG at a time instead of the whole document's worth. A caller that refuses a page stops the capture: there is no point drawing the rest into a destination that has stopped accepting them.

The bytes travel over the loopback listener rather than through Runtime.evaluate or a data: URL, both of which would push megabytes through the CDP connection (--factory-rpc-buffer-size is 8096 by default).

The harness is served by the internal server (see Internal callback server), not from the public static directory: the validate and debug paths never build the public HTTP server, so a harness served from /static/ would be missing exactly where it is needed most.

Scale and page size

The harness scales the pdf.js viewport by scale = dpi / 72, PDF user space being 72 dpi. The default is 96 dpi (scale 4/3), which is the browser's own 1:1 and renders an A4 page at roughly 794 by 1123 pixels.

Token cost of an image grows with the square of the scale while legibility for a model stops improving well before 150 dpi, so raising the scale is rarely worth it. Total pixels per page are capped; a page above the cap is rendered at a reduced scale, and the reduction is reported per page so a manifest can carry it. Truncation is never silent.

Raster scale is a rendering concern only. It is not the PrintToPDF scale, and changing it never changes how the document paginates.

The pinned rasterizer

The bundle lives in app/pdfrender/server/rasterassets/ and is embedded with go:embed; only harness.html is committed there. The minified modules are copied out of tester/node_modules/pdfjs-dist by make raster-assets, which every target that compiles the server depends on.

pdfjs-dist is pinned in tester/package.json, so the version is chosen in one place and pnpm-lock.yaml verifies its integrity — the tester's own copy and the server's harness cannot drift apart, and there is no second version file or checksum list to keep in step by hand. The Apache-2.0 notice travels inside the minified modules, so nothing separate has to be shipped or served.

This does make the Go build depend on pnpm installnot on the tester's bundler. Parcel is still never on the path to a binary, so the binary and the tester bundle stay independently cacheable, and CI pays nothing extra: the check job already installs tester/node_modules for the tester checks.

pdf.js and PDFium do not agree pixel for pixel, so the pinned version is the rasterizer of record. A page image is reproducible against that version and no other — worth knowing before anyone compares two page images byte for byte.