Skip to content

UN-4503 [DEV] Render v1 PDF pages in a windowed, scrolling list - #2405

Open
muhammad-ali-e wants to merge 13 commits into
UN-4502-v1-pdf-loaderfrom
UN-4503-v1-pdf-pages
Open

muhammad-ali-e wants to merge 13 commits into
UN-4502-v1-pdf-loaderfrom
UN-4503-v1-pdf-pages

Conversation

@muhammad-ali-e

@muhammad-ali-e muhammad-ali-e commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

What

New files in the v1 DocumentViewer (P1-T21, UN-4390):

  • components/patterns/document-viewer/PdfPageList.tsx — shows every page of a document in one scrolling column.
  • components/patterns/document-viewer/PdfPage.tsx — one page: a placeholder at the page's real size, plus a canvas.
  • hooks/usePageWindow.ts — tracks which pages are in view.
  • lib/pdf/windowing.ts — the arithmetic: which pages to render, the pixel ratio, and page size at a zoom.

Why

The same viewer has to open 200-page documents. Rendering every page uses far too much memory, so only the pages near the viewport get a canvas.

How

  • Page sizes and scrollbar: each page is laid out at its own size, using the sizes usePdfDocument reads at load (UN-4502). The scrollbar is therefore right before anything renders.

  • Render window: one IntersectionObserver on the scroll container tracks which pages are in view. Those pages plus 2 on each side get a canvas. When nothing is in view yet, nothing renders.

  • Fast scrolling: a page starts rendering one frame after it enters the window. The UN-4501 spike flagged fast scrolling at 2× zoom as the main risk, and with this delay a fast scroll draws nothing for the pages it passes.

  • Cancelling and freeing: a page leaving the window, or a zoom change, does four things:

    • cancels the RenderTask in flight
    • calls page.cleanup()
    • sets the canvas width and height to 0
    • removes the canvas

    Zeroing the canvas first matters because Safari keeps a canvas's memory until garbage collection, and caps the total.

  • Pixel ratio: the screen's ratio, capped at 2. It is lowered further when a canvas would exceed 16,777,216 px, the largest canvas Safari allocates.

  • Zoom: zoom 1 means 100%, i.e. the printed size (96/72 CSS px per PDF point).

  • Sizing without inline style: v1 bans the style attribute (S1), so page sizes are set as --page-width / --page-height custom properties through a ref, and ui:w-(--page-width) / ui:h-(--page-height) read them. I've asked Praveen in #engg-unstract-frontend-v1-migration whether they'd prefer an explicit S1 escape for this kind of computed geometry.

  • Layout: pages are centred while they fit, and scroll both ways once a zoomed page is wider than the viewer.

  • Overlays: renderPageOverlay(index) draws over each page, at the page's size. The highlight layer in UN-4504 uses it.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. This adds new files under src/v1/ only, and nothing imports them yet. There are no package, config or v0 changes.

Database Migrations

  • None

Env Config

  • None

Relevant Docs

  • The P1-T21 implementation plan, plus the UN-4501 spike results (200-page prototype on the same pdf.js 3.11.174).

Related Issues or PRs

Dependencies Versions

  • None

Notes on Testing

  • bun run typecheck:v1, lint:v1 and test:v1 pass: 79 unit tests and 56 boundary fixtures.
  • The new tests use a controllable IntersectionObserver and a mocked pdf.js. They cover:
    • all pages laid out at their own size, with no canvases before layout
    • a scripted scroll through 200 pages: at most 6 canvases ever exist, and at the end only pages 196–199 are live
    • DPR 3 rendering at a ratio of 2, with the canvas sized to match
    • a zoom change cancelling the render in flight
    • leaving the window zeroing the canvas and cleaning up the page
    • a page that leaves before its frame fires never being drawn
  • A probe Vite build checked two more things:
    • the ui:w-(--page-width) / ui:h-(--page-height) utilities are generated
    • the worker is emitted as a hashed asset from the ?url import
  • The build warns about eval in pdf.js. That is the code path isEvalSupported: false turns off.
  • Real-browser checks (the 200-page fixture in Playwright, and highlight rectangles) come in UN-4507.

Screenshots

  • None yet. The gallery comes in UN-4507.

Checklist

I have read and understood the Contribution Guidelines.


Generated by Claude Code

- PdfPageList lays every page out at its own size from the page sizes
  read at load, so the scrollbar is right from the start, and gives a
  canvas only to the pages in view plus two on each side.
- usePageWindow tracks pages in view with one IntersectionObserver on
  the scroll container; lib/pdf/windowing holds the window, pixel ratio
  and CSS size arithmetic.
- PdfPage renders a frame after it enters the window, so a fast scroll
  draws nothing for pages it passes. Leaving the window or a zoom change
  cancels the render in flight, cleans up the page and zeroes the
  canvas. The pixel ratio is capped at 2 and lowered further for pages
  that would exceed the largest canvas Safari allocates.
- Page geometry is set as custom properties read by ui: classes, since
  v1 bans the style attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm

Copy link
Copy Markdown
Contributor Author

pre-commit.ci - pr is red because of the base branch, not this PR.

The failing hook is check-shebang-scripts-are-executable, on frontend/scripts/check-v1-styles.mjs. That file has a #! line but no executable bit. It comes from the tokens PR #2399 (UN-4299-design-tokens-themes); this PR doesn't touch it. The hook doesn't fail on #2399 itself only because pre-commit.ci doesn't run on that PR. Running pre-commit run --from-ref $(git merge-base origin/main HEAD) locally reproduces it, and this PR's own files pass every hook.

There's no fix to port yet. The fix is a one-line change on #2399: git add --chmod=+x frontend/scripts/check-v1-styles.mjs (or drop the shebang). Once #2399 has it, this check should go green here without any change to this PR.


Generated by Claude Code

muhammad-ali-e and others added 7 commits October 29, 2026 08:43
S1 now allows a style attribute whose keys are all custom properties, so
the page size is set directly instead of through a ref and layout effect.
React's CSSProperties gains a --* index signature so this typechecks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
# Conflicts:
#	frontend/src/v1/lib/css-custom-properties.d.ts
…ive tech

- A page whose getPage resolves after it left the render window is now
  cleaned up; the effect cleanup had already run with no page to free.
- Pages are figures instead of role="img", whose children are
  presentational and would hide the highlight buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
Covers a render task that rejects with a real error and a canvas with no
2D context, so a regression that leaves the page in rendering fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
@muhammad-ali-e
muhammad-ali-e marked this pull request as ready for review November 2, 2026 07:00
@greptile-apps

greptile-apps Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Low impact] The PR appears safe to merge as part of the reviewed stack, including PR #2408's document-switch protection.

Summary

Adds a scrolling PDF page list that keeps canvases near the visible pages.

  • PDF pages scroll in one column while only nearby pages render.
  • PDF pages redraw off-screen and release their canvases when they leave.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Page sizes and zoom] --> B[Place every page at its own size]
  B --> C[Observe pages in view]
  C --> D[Add two nearby pages on each side]
  D --> E[Draw into a scratch canvas]
  E --> F[Copy the finished picture]
  D --> G[Page leaves the window]
  G --> H[Cancel work and free canvases]
Loading

Reviews (4) · Last reviewed commit: "Merge branch 'UN-4502-v1-pdf-loader' int..." · Reviewed by Greptile

muhammad-ali-e and others added 2 commits November 2, 2026 08:03
A page whose render failed was left blank, so it could not be told from
an empty page. It now says it didn't load, with a Retry for that page.
Raised by Greptile on #2405.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 2, 2026

@praveen-formido praveen-formido left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windowing and the page component. Two visible problems, both in PdfPage.tsx/PdfPageList.tsx: the canvas is blanked before the replacement render starts, so every page in the window flashes on each zoom step, and the overlay children defeat the memo on every window change. The rest are smaller.

Comment thread frontend/src/v1/hooks/usePageWindow.ts Resolved
muhammad-ali-e and others added 2 commits November 3, 2026 09:54
…let pages stay memoised

From review on #2405:
- A page draws off screen and copies the result in one step, so a zoom
  change keeps the old picture up instead of blanking every page; the
  canvas is still zeroed when the page leaves the window or unmounts.
- useDevicePixelRatio follows a move to another screen or a browser zoom,
  and a change redraws the pages.
- PdfPage calls renderOverlay itself, so a window change re-renders only
  the pages that entered or left it.
- A page is 'rendering' from the moment it asks for the page, and a failed
  render is logged with its page number.
- usePageWindow skips an element without data-page-index instead of
  reading it as page 0.
- isRenderCancelled and pdfErrorName live in lib/pdf/pdfjs.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7trgcAzu6jxDnit3eeozm
@greptile-apps
greptile-apps Bot dismissed their stale review November 3, 2026 09:57

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 3, 2026
# Conflicts:
#	frontend/src/v1/lib/pdf/pdfjs.test.ts
@greptile-apps
greptile-apps Bot dismissed their stale review November 3, 2026 11:20

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

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.

2 participants