Skip to main content

developing-pdf-viewer

Use when changing Trilium's built-in PDF viewer — anything under `packages/pdfjs-viewer` (the code injected into Mozilla's PDF.js viewer: `bootstrap.ts`, `annotations.ts`, `pages.ts`, `toc.ts`, `layers.ts`, `attachments.ts`, `persistence.ts`, `editing.ts`), the client side that talks to it (`apps/client/src/widgets/type_widgets/file/Pdf.tsx`, `PdfViewer.tsx`, the sidebar panels in `widgets/sidebar/pdf/`), the postMessage protocol between them, PDF links (`?page=` / `?annotation=`), or a bug where the viewer "does not pick up" a change. Covers the mandatory rebuild after every viewer change, the message protocol, the hidden-but-mounted viewer, the pdf.js timing traps, and how to test at each level.

Quellinformationen

Repository
TriliumNext/Trilium
Letzte Quellaktivität
4. Oktober 2026 um 20:19
Erkannte Sprache von SKILL.md
Englisch
Sterne
38.343
Forks
2.580

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
developing-pdf-viewer
description
Use when changing Trilium's built-in PDF viewer — anything under `packages/pdfjs-viewer` (the code injected into Mozilla's PDF.js viewer: `bootstrap.ts`, `annotations.ts`, `pages.ts`, `toc.ts`, `layers.ts`, `attachments.ts`, `persistence.ts`, `editing.ts`), the client side that talks to it (`apps/client/src/widgets/type_widgets/file/Pdf.tsx`, `PdfViewer.tsx`, the sidebar panels in `widgets/sidebar/pdf/`), the postMessage protocol between them, PDF links (`?page=` / `?annotation=`), or a bug where the viewer "does not pick up" a change. Covers the mandatory rebuild after every viewer change, the message protocol, the hidden-but-mounted viewer, the pdf.js timing traps, and how to test at each level.
# Developing the PDF viewer A PDF note renders in an iframe running Mozilla's PDF.js viewer, vendored in `packages/pdfjs-viewer/viewer/` (`viewer.mjs`, `viewer.html`, refreshed by `pnpm --filter @triliumnext/pdfjs-viewer update-viewer`) plus Trilium's own code in `packages/pdfjs-viewer/src/`, bundled into `web/custom.mjs`. The client never imports that code: `Pdf.tsx` and the viewer talk only through `postMessage`. ## Rebuild the viewer after every change to it **After any edit under `packages/pdfjs-viewer/` (`src/`, `viewer/`, `scripts/`), run the build before testing anything in an app:** ```bash pnpm --filter @triliumnext/pdfjs-viewer build ``` Every app serves the **built** `packages/pdfjs-viewer/dist`, never the sources: the server in development (`getPdfjsAssetDir()` in `apps/server/src/routes/assets.ts`), desktop, and standalone (`apps/standalone/vite.config.mts`). `dist` is built only by the root `prepare` script, i.e. on `pnpm install`; `pnpm server:start` and the other `*:start` scripts do not rebuild it and do not watch it. Vite hot-reloads the client half of a change while the viewer half stays at whatever was built last, so a stale `dist` looks exactly like a bug in the new code — or like a fix that works, when the old viewer happens to behave. Check the age when in doubt: ```bash ls -l --time-style=+%H:%M packages/pdfjs-viewer/dist/web/custom.mjs ``` - For a longer session, `pnpm --filter @triliumnext/pdfjs-viewer watch` rebuilds `custom.mjs` on save. Then reload the app page: the iframe loads the viewer afresh with the note. - The viewer's own Playwright suite (`pnpm --filter @triliumnext/pdfjs-viewer e2e`) builds before serving, but `reuseExistingServer` keeps a harness server that is already running — and its stale bundle — outside CI. - The unit specs (`pnpm --filter @triliumnext/pdfjs-viewer test`) import the sources directly and need no build. Green specs therefore say nothing about what an app serves. - When reporting a viewer change done, say that `dist` was rebuilt, so the user's running instance picks it up on reload. ## The message protocol - **Viewer → client**: `pdfjs-viewer-*` messages, typed in `apps/client/src/types-pdfjs.d.ts` (`PdfMessageEvent`). Every message carries `noteId` and `ntxId` (from the iframe URL, set in `bootstrap.ts`), and `Pdf.tsx` drops any message not addressed to its own note and context, because every viewer posts to the same parent window. - **Client → viewer**: `trilium-*` messages, posted to `iframeRef.current.contentWindow`; the viewer checks `event.origin`. They are not typed on the viewer side. - `Pdf.tsx` turns the viewer's data into note-context data (`toc`, `pdfPages`, `pdfAnnotations`, `pdfAttachments`, `pdfLayers`), which the right sidebar panels read with `useGetContextData()`. Write it through `publish()`, not `noteContext.setContextData()` — see below. - Most setup runs on pdf.js `documentloaded` and only when the note is editable (`bootstrap.ts`); `setupPdfAnnotations()` runs for read-only PDFs too. The first `pdfjs-viewer-annotations` message is the client's signal that the viewer can scroll. ## The viewer outlives the note it shows `NoteDetailWrapper` keeps each type widget mounted, hidden, while the pane shows a note of another type, so a PDF → text note → same PDF round trip reuses the iframe with its document loaded. Two consequences: - `NoteContext.setNote()` clears the context data on the way out, and the viewer never sends it again. `Pdf.tsx` keeps what it last received per key and republishes it when `isVisible` turns true; anything new published while hidden is recorded, not written. - Anything that scrolls must wait for `isVisible`: a hidden viewer throws "offsetParent is not set -- cannot scroll" and the scroll is lost. `noteSwitched` reaches the hidden viewer before it is shown. ## pdf.js timing traps - **The initial view undoes an early scroll.** pdf.js restores the last-read position at `documentinit` and, for pages of unequal size, again after `pagesPromise`. `trackInitialView()` in `annotations.ts` makes `scrollToAnnotation()` wait for both. - **A resize scrolls back to the recorded position.** `onResize()` reapplies an `auto`, `page-fit` or `page-width` zoom and `#setScaleUpdatePages()` scrolls to `_location`, which pdf.js records in `pdfViewer.update()` on the next frame's scroll event. A viewer shown again resizes, often in the same frame as a jump. After a programmatic scroll, call `pdfViewer.update()` and scroll instantly, never smoothly. - **An annotation's element is not a reliable target.** Pages render only near the viewport, and while an annotation tool is active pdf.js hides each editable annotation's element behind an editor. Position from the rectangle (`page.getAnnotations()`, or `annotationStorage.serializable` for what was edited in this session) through `pageView.viewport.convertToViewportPoint()`; pdf.js 6 has no `convertToViewportRectangle()`. - **Ids drawn this session are temporary.** A stored annotation's id is its object reference (`12R`); one drawn since the document loaded has an editor id (`pdfjs_internal_editor_N`) that the next load does not reuse. ## Testing | Level | What | Command | |---|---|---| | Viewer unit | `src/*.spec.ts` against real pdf.js and a thin `PDFViewerApplication` (`src/test/viewer_app.ts`, fixture PDF in `src/test/fixture_pdf.ts`) | `pnpm --filter @triliumnext/pdfjs-viewer test --run` | | Client unit | `Pdf.spec.tsx` (the protocol, with `PdfViewer` mocked), sidebar panel specs | `pnpm --filter client test src/widgets/type_widgets/file/Pdf.spec.tsx src/widgets/sidebar/pdf/` | | Viewer e2e | built bundle + a stub parent (`e2e/harness`) | `pnpm --filter @triliumnext/pdfjs-viewer e2e` | | App | in-memory e2e fixture with `Dacia Logan.pdf` (`oUfFD9lugwiQ`) and `Layers test.pdf` (`n7gU3aBDPKd9`) | see `building-client-ui` → `references/inspecting-the-running-app.md` — **rebuild `dist` first** | A timing bug (a jump undone, a panel empty after navigation) usually passes in a fast headless run. Instrument the iframe (`contentWindow` `resize` listener, `PDFViewerApplication.pdfViewer` `_location` / `currentScale`, a wrapped `postMessage`) to read the order of events, then force the losing order in the repro — for example resize the iframe right after the client posts — before trusting a fix.
Auf GitHub ansehen