Skip to content

feat(html): render the later pages of a pdf on demand - #1257

Merged
andiwand merged 4 commits into
mainfrom
feat/pdf-pages-on-demand
Oct 25, 2026
Merged

andiwand merged 4 commits into
mainfrom
feat/pdf-pages-on-demand

Conversation

@andiwand

@andiwand andiwand commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Refs #764.

Summary

This is level 3 for a pdf: option B of docs/design/lazy-loading.md decision 7 (#1256). With HtmlConfig::lazy_loading, the combined document renders its first page only. Each later page is an empty page box that loads pdf-pages/<n>.js, a render of that page alone.

  • What a script brings: a <style> with its page's rules, and the content of its page box. The placeholder already is the page box, with its size, page:, id="pN" and data-odr-page. So odr.chunks.fill works as it does for text, and an internal #pN link still lands.
  • Classes:
    • A page rendered alone suffixes its class, face and SVG def names with its number (StyleRegistry::set_suffix, SubstituteFontFaces): x0_12, odr-s1_12.
    • Every script states the .fvN rule of its fonts again, and later in the document. So in a document that loads its pages, the registry rules state their class three times (Rank::replaces_inline). Without that, a later page's .fvN{color:#000} turned an earlier page's blue link text black. A screenshot comparison caught this.
  • Fonts:
    • The dual layer re-encodes a font to the PUA range only, so one file serves every page.
    • The document lists the fonts that the pages and their forms state in their resources, before page 1 renders. It numbers each font for the whole service and links it as pdf-fonts/<n>.ttf, which bring_offline writes and the server serves.
    • A font that no resources state stays inline in its page.
  • Images: a script carries its images inline, because nobody writes the resources of a script.
  • The single layer keeps every page in the document, because it re-encodes a font for the runs it meets.
  • With the default config, the output does not change: a full regen differs from the pinned references only in the long-stale editing.js. So no pins move.

Measured on Core_v5.1.pdf (2,985 pages):

before at level 3
document, over HTTP 5.4 s to first byte 0.5 s
page script / font file, over HTTP 3–6 ms / <1 ms
document.html 127 MB 1.4 MB + 2,984 scripts + 84 fonts (1.8 MB)
core memory to write the document 761 MB 138 MB
first frame from file:// 1.8 s 92 ms

Screenshots at 0, 30%, 50% and 90% of the document match the full render to the pixel, from file:// and over HTTP.

Test

  • html.a_pdf_loads_its_later_pages covers opendocument-app-website.pdf (8 pages):
    • the placeholder of page 2 and the resource of page 8
    • fonts linked as files, and none inline
    • the script of page 2: its key, its suffixed names, and no font data
    • a font file served
    • the default config with no placeholder
  • All 2,535 tests pass.

Review fixes

  • clang-tidy 18 refused the loop that collects the page sizes and the empty class callback that took a string by value. The loop now reserves, and the callback takes a reference.
  • The CHANGELOG entry says that each page renders when the reader comes near it, and uses "document" for the html file.
  • The design doc lists the single layer at level 3 as an open question, with its two ways: fonts inline in each script, or one vote over the whole document.

🤖 Generated with Claude Code

andiwand and others added 4 commits October 25, 2026 00:10
This is level 3 for a pdf, option B of `lazy-loading.md` decision 7.
With `HtmlConfig::lazy_loading`, the document renders its first page.
Each later page is an empty page box that loads `pdf-pages/<n>.js`, a
render of that page alone: a `<style>` with its rules, and the content
of its page box.

- A page rendered alone suffixes its class, face and def names with its
  number. Its rules state their class three times, because the `.fvN`
  rule that every script states again comes later in the document.
- The fonts that the pages and their forms state are numbered for the
  whole service and linked as `pdf-fonts/<n>.ttf`. The dual layer
  re-encodes a font to the PUA range only, so one file serves every
  page. A script carries its images inline.
- The single layer keeps every page in the document, because it
  re-encodes a font for the runs it meets.

On `Core_v5.1.pdf` (2985 pages):

- The HTTP server answers the document after 0.5 s, not 5.4 s, and a
  page after 3 to 6 ms.
- `document.html` is 1.4 MB, not 127 MB.
- Writing it takes 138 MB, not 761 MB.
- Screenshots at the start, the middle and nine tenths of the document
  are the same to the pixel as before.

With the default config the output does not change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QtawyvLP2SCnoBtnVfFed
clang-tidy 18 in CI refused the loop that collects the page sizes
(performance-inefficient-vector-operation) and the empty class callback
that took a string by value (performance-unnecessary-value-param).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
"after the first one at a time" read as one page after the other, and
"every page in the page" used one word for two things.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHNzLmVv1mFWfPitHASucN
@andiwand
andiwand merged commit cc4bafa into main Oct 25, 2026
27 checks passed
@andiwand
andiwand deleted the feat/pdf-pages-on-demand branch October 25, 2026 07:20
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