@laminapdf/viewer 0.0.0-stage → 0.2.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -2
- package/dist/canvas/cache.d.ts +35 -0
- package/dist/canvas/cache.d.ts.map +1 -0
- package/dist/canvas/cache.js +89 -0
- package/dist/canvas/cache.js.map +1 -0
- package/dist/canvas/pool.d.ts +35 -0
- package/dist/canvas/pool.d.ts.map +1 -0
- package/dist/canvas/pool.js +65 -0
- package/dist/canvas/pool.js.map +1 -0
- package/dist/client.d.ts +114 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +240 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/page-layout.d.ts +58 -0
- package/dist/layout/page-layout.d.ts.map +1 -0
- package/dist/layout/page-layout.js +110 -0
- package/dist/layout/page-layout.js.map +1 -0
- package/dist/protocol.d.ts +107 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +36 -0
- package/dist/protocol.js.map +1 -0
- package/dist/render/tiles.d.ts +17 -0
- package/dist/render/tiles.d.ts.map +1 -0
- package/dist/render/tiles.js +29 -0
- package/dist/render/tiles.js.map +1 -0
- package/dist/source.d.ts +9 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +27 -0
- package/dist/source.js.map +1 -0
- package/dist/view/viewer.d.ts +135 -0
- package/dist/view/viewer.d.ts.map +1 -0
- package/dist/view/viewer.js +781 -0
- package/dist/view/viewer.js.map +1 -0
- package/dist/worker/document.d.ts +22 -0
- package/dist/worker/document.d.ts.map +1 -0
- package/dist/worker/document.js +122 -0
- package/dist/worker/document.js.map +1 -0
- package/dist/worker/entry.d.ts +2 -0
- package/dist/worker/entry.d.ts.map +1 -0
- package/dist/worker/entry.js +7 -0
- package/dist/worker/entry.js.map +1 -0
- package/dist/worker/methods.d.ts +4 -0
- package/dist/worker/methods.d.ts.map +1 -0
- package/dist/worker/methods.js +19 -0
- package/dist/worker/methods.js.map +1 -0
- package/dist/worker/render.d.ts +7 -0
- package/dist/worker/render.d.ts.map +1 -0
- package/dist/worker/render.js +22 -0
- package/dist/worker/render.js.map +1 -0
- package/dist/zoom/zoom.d.ts +44 -0
- package/dist/zoom/zoom.d.ts.map +1 -0
- package/dist/zoom/zoom.js +56 -0
- package/dist/zoom/zoom.js.map +1 -0
- package/package.json +55 -3
package/README.md
CHANGED
|
@@ -1,3 +1,177 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @laminapdf/viewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@laminapdf/viewer` shows a PDF in a web page: it scrolls a 1,000-page document smoothly, draws each page sharp, and zooms around the cursor. It has no UI framework inside, so a React toolbar or the desktop app drives it from outside. It has two parts:
|
|
4
|
+
|
|
5
|
+
- **The view** handles layout, virtualisation, canvases and zoom.
|
|
6
|
+
- **The document Web Worker**, one per open document, opens and renders the file away from the page's main thread.
|
|
7
|
+
|
|
8
|
+
The package is private and ESM-only.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import pdfiumWasm from "@embedpdf/pdfium/pdfium.wasm?url";
|
|
12
|
+
import { createViewer, ViewerClient } from "@laminapdf/viewer";
|
|
13
|
+
|
|
14
|
+
const client = await ViewerClient.create({ backend: { backend: "pdfium", wasm: pdfiumWasm } });
|
|
15
|
+
const viewer = createViewer({ client });
|
|
16
|
+
viewer.mount(container); // the container gives the viewer its size
|
|
17
|
+
viewer.open(await client.open(file)); // a File, a Blob or bytes
|
|
18
|
+
viewer.subscribe(() => toolbar.update(viewer.getState())); // page, zoom, rotation
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The view
|
|
22
|
+
|
|
23
|
+
The view keeps only the pages near the viewport in the DOM and paints each in two passes through the document Web Worker. Six parts do that work:
|
|
24
|
+
|
|
25
|
+
- **Layout.** Every page's size comes from its displayed size in points, the zoom and the view rotation. The pages stack in one column with a gap. The offsets are prefix sums, so finding the page at a scroll position is a binary search, and a zoom change costs one pass over the pages (`PageLayout`).
|
|
26
|
+
- **Virtualisation.** The viewer mounts a shell for each page in view, plus a margin of 2 pages or one viewport each side, whichever is larger. A shell that leaves the margin aborts its renders. If a result still arrives for that page, the viewer drops it and never draws it. Scroll handling reads the DOM first and writes it once per animation frame.
|
|
27
|
+
- **Canvases.** A shell borrows its canvas from a pool of 12, which caps the canvases' total size at 64 million device pixels (`CanvasPool`). Safari fails past an undocumented total canvas area, so the pool shrinks a canvas rather than exceed the cap. Rendered bitmaps stay in a 256 MB cache, which closes the bitmaps farthest from the current page first (`BitmapCache`).
|
|
28
|
+
- **Progressive rendering.** A page first paints a blurry whole-page pass, at most 256 pixels wide, at `visible` priority. Then it paints sharp tiles, 512 CSS px × the device pixel ratio on a side, over the part in view. Pages in the margin get only the blurry pass, at `prefetch` priority. A canvas is resized only when the new pixels are ready, so the page never flashes white.
|
|
29
|
+
- **Zoom.** The modes are fit width, fit page, actual size (96 CSS px per inch) and 10–800 %. Zoom comes from ctrl/cmd+wheel, trackpad pinch (ctrl+wheel, or Safari's gesture events), touch pinch and the `+`, `-` and `0` keys. The document point under the cursor stays where it is: always vertically, and horizontally while the pages are wider than the viewport. Narrower pages are centred, so no horizontal scroll can keep it there. During a gesture the canvases stretch, and the sharp re-render waits until the gesture pauses for 150 ms.
|
|
30
|
+
- **Tall documents.** A document taller than 16 million CSS px at the chosen zoom gets its zoom capped, because browsers stop scrolling near that height.
|
|
31
|
+
|
|
32
|
+
**API.**
|
|
33
|
+
|
|
34
|
+
| Member | What it does |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `createViewer({ client, margin?, gap?, pool?, cacheBytes?, settleMs?, zoom? })` | Makes a viewer over a `ViewerClient`. |
|
|
37
|
+
| `mount(container)`, `open(document)`, `destroy()` | Puts it on the page, shows a document, takes it down. |
|
|
38
|
+
| `goToPage(index, { anchor })`, `setZoom(zoom, { anchor })`, `rotate(delta)` | Navigation. `zoom` is a mode or a percentage. |
|
|
39
|
+
| `getState()`, `subscribe(listener)` | The view state (`currentPage`, `pageCount`, `zoom`, `percent`, `rotation`, `scrollTop`, `visibleRange`), for `useSyncExternalStore`. |
|
|
40
|
+
| `on("pagechange" \| "zoomchange" \| "renderstats", …)`, `stats()` | Events and counters. `renderstats` counts renders, for measuring performance. |
|
|
41
|
+
| `addLayer({ name, onMount, onUnmount, onZoom })` | Mounts a layer into each page. |
|
|
42
|
+
| `pointAt(x, y)`, `positionOf(point)` | Converts between viewport positions and document points. |
|
|
43
|
+
| `MIN_ZOOM`, `MAX_ZOOM` | The bounds of a percentage zoom, 10 and 800, for a zoom control's range. |
|
|
44
|
+
|
|
45
|
+
**Layers.** Each page shell stacks a canvas and three slots above it: `div.lamina-layer-text`, `div.lamina-layer-annot` and `div.lamina-layer-highlight`. The coming text layer, find highlights and links will mount there through `addLayer`. A layer with another name gets its own slot on top.
|
|
46
|
+
|
|
47
|
+
**Tuning.** The margin, pool size, pixel cap, cache budget and settle delay are measured by the perf harness ([Performance](#performance)); the first full run kept them.
|
|
48
|
+
|
|
49
|
+
## The document Web Worker
|
|
50
|
+
|
|
51
|
+
The client starts the Web Worker and talks to it; you call `ViewerClient`'s methods and listen to its events.
|
|
52
|
+
|
|
53
|
+
**Events:**
|
|
54
|
+
|
|
55
|
+
| Event | Source | Data | Meaning |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| `warning` | Web Worker | a reader warning | for example, a repaired xref |
|
|
58
|
+
| `render-start` | Web Worker | `{ page, scale }` | a render began, for measuring performance |
|
|
59
|
+
| `render-slow` | page | `{ id, page }` | the soft timeout passed, counted from `render-start`: waiting in the queue isn't slow |
|
|
60
|
+
| `render-stuck` | page | `{ id, page, needsPassword }` | an encrypted document's render passed the hard cap |
|
|
61
|
+
| `crashed` | page | `{ reason }` | the Web Worker crashed |
|
|
62
|
+
| `restarted` | page | `{ reason, reopened }` | a new Web Worker serves again |
|
|
63
|
+
| `password-required` | page | — | the new Web Worker can't reopen without the password |
|
|
64
|
+
| `restart-failed` | page | `{ reason, error }` | a restart failed (for example, the backend wouldn't load again) |
|
|
65
|
+
|
|
66
|
+
### How it behaves
|
|
67
|
+
|
|
68
|
+
- **Priorities and coalescing:** the Web Worker runs one call at a time, the highest priority first, FIFO within one.
|
|
69
|
+
- Before each call it reads every message already sent, so a burst of renders and cancels is ordered and pruned before anything runs.
|
|
70
|
+
- Renders with an equal normalised `RenderRequest` (`renderKey`: page, scale, clip, rotation, flags, background) run once, and each caller gets its own bitmap.
|
|
71
|
+
- **Cancellation:** `signal` rejects the call at once with a `DOMException` named `AbortError`.
|
|
72
|
+
- A queued render is dropped.
|
|
73
|
+
- A running one finishes, because synchronous WebAssembly can't be interrupted. Its late answer's bitmap is closed unpainted (`onDiscard`).
|
|
74
|
+
- (`@laminapdf/sdk/worker` itself rejects with a `cancelled` LaminaPdfError; the viewer maps it to `AbortError`.)
|
|
75
|
+
- **Timeouts:** the page keeps the timers, because a Web Worker inside a render can't post anything.
|
|
76
|
+
- Defaults are `soft` 10 s (`render-slow`) and `hard` 120 s.
|
|
77
|
+
- At the hard cap the client calls `stop("hard-timeout")`. For a document that needed a password, it emits `render-stuck` instead and waits for the user's `stop()`.
|
|
78
|
+
- **Crashes:** a crash is the Web Worker's `error` or `messageerror`, or a WebAssembly trap it reports, never silence.
|
|
79
|
+
- Pending calls reject with `worker-crashed`.
|
|
80
|
+
- A new Web Worker reopens the document from the same source, but nothing that was pending is re-sent. The UI offers Retry, so a page that crashes the engine can't loop.
|
|
81
|
+
- `stop()` takes the same path with `reason: "cancelled"`.
|
|
82
|
+
- **Secrets:** the password crosses to the Web Worker once, when the document opens, and is kept only there. The page keeps the source and the other options.
|
|
83
|
+
- After a crash of an encrypted document, `password-required` fires and the caller opens again.
|
|
84
|
+
- Lamina PDF's tests count every message that carries the password, so a change that sends it more often fails them.
|
|
85
|
+
- **Sources:**
|
|
86
|
+
- A `Blob`/`File` is sent by reference, and the Web Worker reads it in 4 MiB `slice()`s.
|
|
87
|
+
- Bytes are copied, and kept on the page for a reopen.
|
|
88
|
+
|
|
89
|
+
## For contributors
|
|
90
|
+
|
|
91
|
+
The viewer is the web client of [ADR 0009](../../docs/decisions/0009-web-client-architecture.md); it was built in [C3a](../../docs/todos/done/C3a-viewer-worker-rpc.md) (the document worker and its typed RPC) and [C3b](../../docs/todos/done/C3b-viewer-virtualiser-and-zoom.md) (the viewer). [C3c](../../docs/todos/done/C3c-viewer-perf-harness.md) measures the tuning values with `renderstats`. The layers are for [C5a](../../docs/todos/ready/C5a-text-layer-and-selection.md) (text), [C5b](../../docs/todos/ready/C5b-find-in-document.md) (find) and [C6b](../../docs/todos/ready/C6b-links-and-history.md) (links); C4c will read documents by range through `@laminapdf/sdk`'s `RangeSource`. Cancellation follows ADR 0009 §3.5, and warnings are [ADR 0003](../../docs/decisions/0003-reader-tolerance-and-limits.md)'s.
|
|
92
|
+
|
|
93
|
+
### Architecture
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
main thread document worker (one per open document)
|
|
97
|
+
┌────────────────────────────┐ postMessage ┌──────────────────────────────────────────┐
|
|
98
|
+
│ ViewerClient │ ─────────────▶ │ @laminapdf/sdk/worker/server serve({ … }) │
|
|
99
|
+
│ call(method, params, opts) │ call / cancel │ handshake · priority queue · coalescing │
|
|
100
|
+
│ render timers (slow/stuck) │ │ cancellation · ADR 0026 errors · traps │
|
|
101
|
+
│ restart + reopen on crash │ ◀───────────── │ ├─ viewer methods (src/worker/) │
|
|
102
|
+
│ AbortError on abort │ result / event │ │ openDocument → SDK Document + │
|
|
103
|
+
│ @laminapdf/sdk/worker client │ (transferred │ │ RenderDocument │
|
|
104
|
+
│ request() · onDiscard │ ImageBitmap) │ │ render → backend → ImageBitmap │
|
|
105
|
+
└────────────────────────────┘ │ └─ the SDK's own methods (open, …) │
|
|
106
|
+
└──────────────────────────────────────────┘
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
There is one protocol, not two: the viewer's worker is `@laminapdf/sdk/worker`'s server with the viewer's methods added (`serve({ methods })`), and the client calls them through `WorkerClient.request` (ADR 0009 §3, [L2a](../../docs/todos/done/L2a-worker-client.md)).
|
|
110
|
+
|
|
111
|
+
### Methods
|
|
112
|
+
|
|
113
|
+
| Method | Params | Result | Priority |
|
|
114
|
+
| --- | --- | --- | --- |
|
|
115
|
+
| `initBackend` | `{ backend: "pdfium" \| "hayro", wasm? }` | `{ name, version }` | document. Sent by `create()`, before a file is chosen, so the WebAssembly downloads in parallel with file selection. |
|
|
116
|
+
| `openDocument` | `source` (`Blob`, `ArrayBuffer`, `Uint8Array`), `{ password?, limits?, renderLimits? }` | `{ pageCount, pages, needsPassword, backend }` | document |
|
|
117
|
+
| `closeDocument` | — | — | document |
|
|
118
|
+
| `pageInfo` | `index` | `PageInfo` (displayed size in points, rotation, ID, label) | document |
|
|
119
|
+
| `render` | ADR 0004 `RenderRequest` | `{ bitmap: ImageBitmap, width, height }` (transferred) | `visible` > `prefetch` > `background` |
|
|
120
|
+
| `memory` | — | `WorkerMemory`: the bytes of the backend's and Lamina PDF's WebAssembly memories | document. `client.memory()`; the perf harness samples it. |
|
|
121
|
+
|
|
122
|
+
`client.call(method, params, { priority, signal })` is typed by the `Methods` map: an unknown method or wrong params fails `tsc` (`test/types.test.ts`).
|
|
123
|
+
|
|
124
|
+
### Tests
|
|
125
|
+
|
|
126
|
+
The browser suites run in Chromium, Firefox and WebKit over the built package: run `cargo xtask pdf-runtimes chromium|firefox|webkit`, or `pnpm exec vitest run -c vitest.browser.config.ts` after `cargo xtask wasm`.
|
|
127
|
+
|
|
128
|
+
- `test/browser/viewer.browser.test.ts` scrolls a 1,000-page document that the test builds with `@laminapdf/sdk`'s builder. It covers:
|
|
129
|
+
- scrolling one viewport per frame from page 1 to 1,000 with at most visible + 2 × margin shells, and no more canvases than the pool holds;
|
|
130
|
+
- a fast scroll past 200 pages that paints no cancelled render;
|
|
131
|
+
- every visible page reaching sharp tiles once scrolling stops, including the part of a tall zoomed page scrolled into later;
|
|
132
|
+
- ctrl+wheel zoom 100 % → 200 % → 50 % holding the point under the cursor within 1 CSS px;
|
|
133
|
+
- keyboard zoom, rotation and layers.
|
|
134
|
+
- `test/browser/rpc.browser.test.ts` covers the document worker, and counts every `postMessage` that carries the password:
|
|
135
|
+
- the corpus page counts and sizes against `fixtures/corpus/expected.json`;
|
|
136
|
+
- hayro;
|
|
137
|
+
- limits;
|
|
138
|
+
- 100 renders with 90 aborted, and a render aborted mid-run;
|
|
139
|
+
- priorities and coalescing;
|
|
140
|
+
- slow, stuck and stopped renders;
|
|
141
|
+
- crashes, including an encrypted file;
|
|
142
|
+
- the password count.
|
|
143
|
+
- Unit tests sit next to their modules (`src/**/*.test.ts`) and run in Node. They cover the layout's cost, counted in steps, and the fit modes for portrait, landscape and rotated pages. They also cover zoom anchoring, tiles, cache eviction and the pool's caps. `test/dependencies.test.ts` checks that `src/` imports no UI framework, and `test/types.test.ts` checks the RPC types.
|
|
144
|
+
- Two demo pages are manual checks: run `pnpm --filter @laminapdf/viewer exec vite examples`. `examples/viewer-demo.html` is the viewer, with zoom buttons; `examples/rpc-demo.html` draws page 1 through the RPC alone. The perf harness has its own page, `perf/page/` ([Performance](#performance)).
|
|
145
|
+
|
|
146
|
+
### Performance
|
|
147
|
+
|
|
148
|
+
A harness in `perf/` measures the viewer against the [web plan's targets](../../docs/plans/web-app.md#non-functional-targets-are-recommendations-measured-from-c3-onward) in Chromium, Firefox and WebKit:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
cargo xtask wasm # once: the package the harness runs
|
|
152
|
+
pnpm --filter @laminapdf/viewer perf # every browser; -- --browsers chromium --quick for a short run
|
|
153
|
+
cargo xtask viewer-perf [chromium|firefox|webkit] # the same, plus the job summary and the nightly history
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
It writes `target/viewer-perf/<browser>.json` and `summary.md`, and never fails on a missed target: no pull request lane asserts a duration ([ADR 0018](../../docs/decisions/0018-test-strategy.md#2-ci-lanes)).
|
|
157
|
+
|
|
158
|
+
**What it does.** It opens three seeded files, built with `@laminapdf/sdk` and cached in `perf/.fixtures/`: 10 pages of text with an image each (about 1 MB), 1,000 pages of text, and 300 pages of full-page images. A Vite build of `perf/page/` shows them, and `perf/serve.ts` serves that page gzipped, as Cloudflare serves the app. Playwright drives real input, so every run scrolls the same way:
|
|
159
|
+
|
|
160
|
+
| Scenario | Measures | Target |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| Open the 10-page file, warm (after a reload) | time to the first page's first paint | ≤ 700 ms |
|
|
163
|
+
| The same, cold, on Chrome DevTools' Fast 4G (Chromium only) | time from navigation to the first page | ≤ 3 s |
|
|
164
|
+
| Scroll the 1,000-page file end to end, twice, with wheel steps every 20 ms; stop at a quarter, a half and three quarters | the longest main-thread task, time to sharp after each stop, peak memory, memory after each pass, cancelled renders that painted | no task > 50 ms; sharp ≤ 300 ms; ≤ 600 MB; second pass ≤ first + 5 %; 0 |
|
|
165
|
+
| Fling across the 300 image pages, then stop | time to sharp | ≤ 300 ms |
|
|
166
|
+
| Three ctrl+wheel bursts in and out on the 10-page file | the longest frame, time to sharp after each burst | reported |
|
|
167
|
+
|
|
168
|
+
"Time to sharp" runs from the last scroll or zoom event to the moment every visible page turned sharp. Memory is the page's JS heap (Chromium only), the document Web Worker's WebAssembly memories (`client.memory()`), the bitmap cache and the canvases' pixels. Firefox and WebKit have no Long Tasks API, so their longest task is the longest gap between frames, which is an upper bound.
|
|
169
|
+
|
|
170
|
+
**History.** The nightly lane runs `cargo xtask viewer-perf --history … --record` on `main` and keeps each browser's figures in the benchmark history (`viewer/<browser>/<figure>`: nanoseconds, or bytes for memory). A figure more than 10 % worse than the median of its last five runs on the same runner opens an issue, as a benchmark does; `--dry-run` prints that issue. `--quick` covers 100 pages and is never recorded.
|
|
171
|
+
|
|
172
|
+
**The reference machine.** The official numbers are the nightly lane's, from the Mac that hosts the self-hosted runners, with nothing else running on it. Each report ends with the machine it ran on (CPU, cores, memory, OS). Numbers from other machines, CI containers included, are relative: compare them with each other, not with the targets.
|
|
173
|
+
|
|
174
|
+
**First measurements and tuning.** The first full run (Chromium 153 on a 4-core Linux container, 2026-11-05) came in under every scrolling and memory target, a first reading until the reference machine's numbers arrive. It had no long task while scrolling, pages were sharp 0–17 ms after a scroll stopped and 33 ms after a fling, memory peaked at 302 MB, the second pass ended 0.5 % above the first, and no cancelled render painted. So the defaults above stay: a margin of 2 pages or one viewport, a pool of 12 canvases and 64 million pixels, a 256 MB bitmap cache, 512 CSS px tiles and a 150 ms settle. It missed one target and found one slow spot, both for [C7c](../../docs/todos/ready/C7c-performance-gates-and-launch.md):
|
|
175
|
+
|
|
176
|
+
- **The cold first page took 4.2 s on Fast 4G (target 3 s).** The download alone takes about 3.7 s at Fast 4G's 1 MB/s: PDFium's WebAssembly (2.2 MB gzipped), Lamina PDF's base module (0.4 MB) and the 1 MB file.
|
|
177
|
+
- **Zooming in took about 1.2 s to sharp, with a 317 ms frame.** Zooming out took about 300 ms. The plan sets no zoom target.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** One cached bitmap: the page it belongs to and its decoded size in bytes. */
|
|
2
|
+
export interface CachedBitmap {
|
|
3
|
+
readonly page: number;
|
|
4
|
+
readonly bitmap: ImageBitmap;
|
|
5
|
+
readonly bytes: number;
|
|
6
|
+
}
|
|
7
|
+
/** Default budget: 256 MB of decoded pixels. */
|
|
8
|
+
export declare const DEFAULT_CACHE_BYTES: number;
|
|
9
|
+
/** Rendered bitmaps by key, within a byte budget, evicting the farthest from the current page. */
|
|
10
|
+
export declare class BitmapCache {
|
|
11
|
+
#private;
|
|
12
|
+
readonly budget: number;
|
|
13
|
+
constructor(budget?: number);
|
|
14
|
+
/** The decoded bytes the cached bitmaps hold. */
|
|
15
|
+
get bytes(): number;
|
|
16
|
+
/** How many bitmaps are cached. */
|
|
17
|
+
get size(): number;
|
|
18
|
+
/** The bitmap under `key`, if cached (it becomes the most recently used). */
|
|
19
|
+
get(key: string): ImageBitmap | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Stores `bitmap` for `page` under `key` (replacing and closing an older one), then evicts by
|
|
22
|
+
* distance from `focus` (the current page) until the cache fits its budget again.
|
|
23
|
+
*
|
|
24
|
+
* @returns whether the cache kept it. A bitmap larger than the whole budget isn't kept, nor
|
|
25
|
+
* closed: it stays the caller's, to draw and close.
|
|
26
|
+
*/
|
|
27
|
+
set(key: string, page: number, bitmap: ImageBitmap, focus: number): boolean;
|
|
28
|
+
/** Closes and forgets the bitmap under `key`, if any. */
|
|
29
|
+
delete(key: string): void;
|
|
30
|
+
/** Closes every bitmap of `page` whose key `keep` doesn't accept (an old zoom's tiles). */
|
|
31
|
+
prune(page: number, keep: (key: string) => boolean): void;
|
|
32
|
+
/** Closes everything. */
|
|
33
|
+
clear(): void;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/canvas/cache.ts"],"names":[],"mappings":"AAIA,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,gDAAgD;AAChD,eAAO,MAAM,mBAAmB,QAAoB,CAAC;AAErD,kGAAkG;AAClG,qBAAa,WAAW;;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAIxB,YAAY,MAAM,SAAsB,EAEvC;IAED,iDAAiD;IACjD,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,mCAAmC;IACnC,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,6EAA6E;IAC7E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAOxC;IAED;;;;;;OAMG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAQ1E;IAED,yDAAyD;IACzD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAMxB;IAED,2FAA2F;IAC3F,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAExD;IAoBD,yBAAyB;IACzB,KAAK,IAAI,IAAI,CAEZ;CACF"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The bitmap cache: rendered ImageBitmaps by page, scale and tile, within a byte budget
|
|
2
|
+
// (width × height × 4 each). Past the budget it evicts the bitmaps farthest from the viewport
|
|
3
|
+
// first, and closes every bitmap it evicts or replaces, so GPU memory is given back at once.
|
|
4
|
+
/** Default budget: 256 MB of decoded pixels. */
|
|
5
|
+
export const DEFAULT_CACHE_BYTES = 256 * 1024 * 1024;
|
|
6
|
+
/** Rendered bitmaps by key, within a byte budget, evicting the farthest from the current page. */
|
|
7
|
+
export class BitmapCache {
|
|
8
|
+
budget;
|
|
9
|
+
#entries = new Map();
|
|
10
|
+
#bytes = 0;
|
|
11
|
+
constructor(budget = DEFAULT_CACHE_BYTES) {
|
|
12
|
+
this.budget = budget;
|
|
13
|
+
}
|
|
14
|
+
/** The decoded bytes the cached bitmaps hold. */
|
|
15
|
+
get bytes() {
|
|
16
|
+
return this.#bytes;
|
|
17
|
+
}
|
|
18
|
+
/** How many bitmaps are cached. */
|
|
19
|
+
get size() {
|
|
20
|
+
return this.#entries.size;
|
|
21
|
+
}
|
|
22
|
+
/** The bitmap under `key`, if cached (it becomes the most recently used). */
|
|
23
|
+
get(key) {
|
|
24
|
+
const entry = this.#entries.get(key);
|
|
25
|
+
if (entry === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
// Most recently used last (the tie-breaker among pages at the same distance).
|
|
28
|
+
this.#entries.delete(key);
|
|
29
|
+
this.#entries.set(key, entry);
|
|
30
|
+
return entry.bitmap;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Stores `bitmap` for `page` under `key` (replacing and closing an older one), then evicts by
|
|
34
|
+
* distance from `focus` (the current page) until the cache fits its budget again.
|
|
35
|
+
*
|
|
36
|
+
* @returns whether the cache kept it. A bitmap larger than the whole budget isn't kept, nor
|
|
37
|
+
* closed: it stays the caller's, to draw and close.
|
|
38
|
+
*/
|
|
39
|
+
set(key, page, bitmap, focus) {
|
|
40
|
+
this.delete(key);
|
|
41
|
+
const bytes = bitmap.width * bitmap.height * 4;
|
|
42
|
+
if (bytes > this.budget)
|
|
43
|
+
return false;
|
|
44
|
+
this.#entries.set(key, { page, bitmap, bytes });
|
|
45
|
+
this.#bytes += bytes;
|
|
46
|
+
this.#evict(focus, key);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** Closes and forgets the bitmap under `key`, if any. */
|
|
50
|
+
delete(key) {
|
|
51
|
+
const entry = this.#entries.get(key);
|
|
52
|
+
if (entry === undefined)
|
|
53
|
+
return;
|
|
54
|
+
this.#entries.delete(key);
|
|
55
|
+
this.#bytes -= entry.bytes;
|
|
56
|
+
entry.bitmap.close();
|
|
57
|
+
}
|
|
58
|
+
/** Closes every bitmap of `page` whose key `keep` doesn't accept (an old zoom's tiles). */
|
|
59
|
+
prune(page, keep) {
|
|
60
|
+
for (const [key, entry] of [...this.#entries])
|
|
61
|
+
if (entry.page === page && !keep(key))
|
|
62
|
+
this.delete(key);
|
|
63
|
+
}
|
|
64
|
+
#evict(focus, protect) {
|
|
65
|
+
while (this.#bytes > this.budget) {
|
|
66
|
+
let worst;
|
|
67
|
+
let distance = -1;
|
|
68
|
+
// Insertion order is least recently used first, so among equals the oldest goes.
|
|
69
|
+
for (const [key, entry] of this.#entries) {
|
|
70
|
+
if (key === protect)
|
|
71
|
+
continue;
|
|
72
|
+
const d = Math.abs(entry.page - focus);
|
|
73
|
+
if (d > distance) {
|
|
74
|
+
distance = d;
|
|
75
|
+
worst = key;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (worst === undefined)
|
|
79
|
+
return;
|
|
80
|
+
this.delete(worst);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Closes everything. */
|
|
84
|
+
clear() {
|
|
85
|
+
for (const key of [...this.#entries.keys()])
|
|
86
|
+
this.delete(key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/canvas/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The pool's limits, and how it makes canvases. */
|
|
2
|
+
export interface CanvasPoolOptions {
|
|
3
|
+
/** Most canvases at once (default 12). */
|
|
4
|
+
readonly max?: number;
|
|
5
|
+
/** Most device pixels across every acquired canvas (default 64 MP). */
|
|
6
|
+
readonly maxPixels?: number;
|
|
7
|
+
/** Makes a canvas (default `document.createElement("canvas")`). */
|
|
8
|
+
readonly create?: () => HTMLCanvasElement;
|
|
9
|
+
}
|
|
10
|
+
/** A fixed set of canvases lent to mounted pages, within a total pixel cap. */
|
|
11
|
+
export declare class CanvasPool {
|
|
12
|
+
#private;
|
|
13
|
+
readonly max: number;
|
|
14
|
+
readonly maxPixels: number;
|
|
15
|
+
/** The most canvases ever acquired at once (reported in the viewer's render stats). */
|
|
16
|
+
peak: number;
|
|
17
|
+
constructor(options?: CanvasPoolOptions);
|
|
18
|
+
/** Canvases lent out now. */
|
|
19
|
+
get inUse(): number;
|
|
20
|
+
/** Device pixels the acquired canvases hold now. */
|
|
21
|
+
get pixels(): number;
|
|
22
|
+
/** A canvas, or undefined when `max` are in use (the page keeps its placeholder until one frees). */
|
|
23
|
+
acquire(): HTMLCanvasElement | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The size `canvas` may take, `width × height` device pixels scaled down (keeping the aspect)
|
|
26
|
+
* so that every acquired canvas together stays within `maxPixels`.
|
|
27
|
+
*/
|
|
28
|
+
fit(canvas: HTMLCanvasElement, width: number, height: number): {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
};
|
|
32
|
+
/** Returns `canvas`, reset (1×1, no pixels, default context state). */
|
|
33
|
+
release(canvas: HTMLCanvasElement): void;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../src/canvas/pool.ts"],"names":[],"mappings":"AAIA,oDAAoD;AACpD,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,iBAAiB,CAAC;CAC3C;AAKD,+EAA+E;AAC/E,qBAAa,UAAU;;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAI3B,uFAAuF;IACvF,IAAI,SAAK;IAET,YAAY,OAAO,GAAE,iBAAsB,EAI1C;IAED,6BAA6B;IAC7B,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,oDAAoD;IACpD,IAAI,MAAM,IAAI,MAAM,CAInB;IAED,qGAAqG;IACrG,OAAO,IAAI,iBAAiB,GAAG,SAAS,CAMvC;IAED;;;OAGG;IACH,GAAG,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAO/F;IAED,uEAAuE;IACvE,OAAO,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAOvC;CACF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The canvas pool: a fixed number of canvases, reused as pages mount and unmount, with a
|
|
2
|
+
// cap on the device pixels they hold together (browsers, Safari above all, fail or crash past an
|
|
3
|
+
// undocumented total canvas area). A canvas comes back reset: 1×1, so it holds no pixels.
|
|
4
|
+
/** The default limits: 12 canvases, 64 million device pixels in all. */
|
|
5
|
+
const DEFAULT_POOL = Object.freeze({ max: 12, maxPixels: 64 * 1024 * 1024 });
|
|
6
|
+
/** A fixed set of canvases lent to mounted pages, within a total pixel cap. */
|
|
7
|
+
export class CanvasPool {
|
|
8
|
+
max;
|
|
9
|
+
maxPixels;
|
|
10
|
+
#create;
|
|
11
|
+
#free = [];
|
|
12
|
+
#used = new Set();
|
|
13
|
+
/** The most canvases ever acquired at once (reported in the viewer's render stats). */
|
|
14
|
+
peak = 0;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.max = options.max ?? DEFAULT_POOL.max;
|
|
17
|
+
this.maxPixels = options.maxPixels ?? DEFAULT_POOL.maxPixels;
|
|
18
|
+
this.#create = options.create ?? (() => document.createElement("canvas"));
|
|
19
|
+
}
|
|
20
|
+
/** Canvases lent out now. */
|
|
21
|
+
get inUse() {
|
|
22
|
+
return this.#used.size;
|
|
23
|
+
}
|
|
24
|
+
/** Device pixels the acquired canvases hold now. */
|
|
25
|
+
get pixels() {
|
|
26
|
+
let total = 0;
|
|
27
|
+
for (const c of this.#used)
|
|
28
|
+
total += c.width * c.height;
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
/** A canvas, or undefined when `max` are in use (the page keeps its placeholder until one frees). */
|
|
32
|
+
acquire() {
|
|
33
|
+
if (this.#used.size >= this.max)
|
|
34
|
+
return undefined;
|
|
35
|
+
const canvas = this.#free.pop() ?? this.#create();
|
|
36
|
+
this.#used.add(canvas);
|
|
37
|
+
if (this.#used.size > this.peak)
|
|
38
|
+
this.peak = this.#used.size;
|
|
39
|
+
return canvas;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The size `canvas` may take, `width × height` device pixels scaled down (keeping the aspect)
|
|
43
|
+
* so that every acquired canvas together stays within `maxPixels`.
|
|
44
|
+
*/
|
|
45
|
+
fit(canvas, width, height) {
|
|
46
|
+
const others = this.pixels - canvas.width * canvas.height;
|
|
47
|
+
const room = Math.max(0, this.maxPixels - others);
|
|
48
|
+
const want = width * height;
|
|
49
|
+
if (want <= room)
|
|
50
|
+
return { width: Math.max(1, Math.round(width)), height: Math.max(1, Math.round(height)) };
|
|
51
|
+
const k = Math.sqrt(room / want);
|
|
52
|
+
return { width: Math.max(1, Math.floor(width * k)), height: Math.max(1, Math.floor(height * k)) };
|
|
53
|
+
}
|
|
54
|
+
/** Returns `canvas`, reset (1×1, no pixels, default context state). */
|
|
55
|
+
release(canvas) {
|
|
56
|
+
if (!this.#used.delete(canvas))
|
|
57
|
+
return;
|
|
58
|
+
// Setting the size clears the pixels and the context's state.
|
|
59
|
+
canvas.width = 1;
|
|
60
|
+
canvas.height = 1;
|
|
61
|
+
canvas.remove();
|
|
62
|
+
this.#free.push(canvas);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/canvas/pool.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,iGAAiG;AACjG,0FAA0F;AAY1F,wEAAwE;AACxE,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;AAE7E,+EAA+E;AAC/E,MAAM,OAAO,UAAU;IACZ,GAAG,CAAS;IACZ,SAAS,CAAS;IAClB,OAAO,CAA0B;IACjC,KAAK,GAAwB,EAAE,CAAC;IAChC,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC9C,uFAAuF;IACvF,IAAI,GAAG,CAAC,CAAC;IAET,YAAY,OAAO,GAAsB,EAAE;QACzC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,YAAY,CAAC,SAAS,CAAC;QAC7D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED,6BAA6B;IAC7B,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;IACzB,CAAC;IAED,oDAAoD;IACpD,IAAI,MAAM;QACR,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK;YAAE,KAAK,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC;QACxD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,qGAAqG;IACrG,OAAO;QACL,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAClD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvB,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;QAC7D,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,MAAyB,EAAE,KAAa,EAAE,MAAc;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;QAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC;QAClD,MAAM,IAAI,GAAG,KAAK,GAAG,MAAM,CAAC;QAC5B,IAAI,IAAI,IAAI,IAAI;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAC5G,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;QACjC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACpG,CAAC;IAED,uEAAuE;IACvE,OAAO,CAAC,MAAyB;QAC/B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,OAAO;QACvC,8DAA8D;QAC9D,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC;QACjB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QAClB,MAAM,CAAC,MAAM,EAAE,CAAC;QAChB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;CACF"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { RenderRequest } from "@laminapdf/render";
|
|
2
|
+
import { type WorkerInitOptions, type WorkerWasmSource } from "@laminapdf/sdk/worker";
|
|
3
|
+
import { type BackendInit, type MethodName, type OpenedDocument, type Params, type Priority, type RenderedBitmap, type Result, type SourceInit, type ViewerOpenOptions } from "./protocol.js";
|
|
4
|
+
/** `ViewerClient.create`'s options: the backend, Lamina PDF's WebAssembly, the timeouts, the worker and a signal to stop starting. */
|
|
5
|
+
export interface ViewerOptions {
|
|
6
|
+
/** The render engine; PDFium needs its `.wasm` URL from your bundler (`pdfium.wasm?url`). */
|
|
7
|
+
readonly backend: BackendInit;
|
|
8
|
+
/** Where the worker loads Lamina PDF's own WebAssembly from (`@laminapdf/sdk/worker`'s `init`). */
|
|
9
|
+
readonly lamina?: WorkerWasmSource | WorkerInitOptions;
|
|
10
|
+
/** Render timeouts in ms (`soft` emits `render-slow`; `hard` restarts or emits `render-stuck`). */
|
|
11
|
+
readonly timeouts?: {
|
|
12
|
+
readonly soft?: number;
|
|
13
|
+
readonly hard?: number;
|
|
14
|
+
};
|
|
15
|
+
/** Makes the worker (CSP, or your own hosting of `dist/worker/entry.js`). */
|
|
16
|
+
readonly worker?: () => Worker;
|
|
17
|
+
/** Aborts starting. */
|
|
18
|
+
readonly signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
/** Why the worker was restarted. */
|
|
21
|
+
export type RestartReason = "error" | "messageerror" | "trap" | "cancelled" | "hard-timeout";
|
|
22
|
+
/** The client's events: from the worker (`warning`) or raised on the page (the rest). */
|
|
23
|
+
export interface ViewerEvents {
|
|
24
|
+
/** An ADR 0003 warning about the open document (a repaired xref, …). */
|
|
25
|
+
warning: {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly message: string;
|
|
28
|
+
};
|
|
29
|
+
/** The worker started drawing a render (so a performance test can see what the worker is busy with). */
|
|
30
|
+
"render-start": {
|
|
31
|
+
readonly page: number;
|
|
32
|
+
readonly scale: number;
|
|
33
|
+
readonly key: string;
|
|
34
|
+
};
|
|
35
|
+
/** A render passed the soft timeout: offer Stop. */
|
|
36
|
+
"render-slow": {
|
|
37
|
+
readonly id: number;
|
|
38
|
+
readonly page: number;
|
|
39
|
+
};
|
|
40
|
+
/** An encrypted document's render passed the hard cap: `stop()` would ask for the password again. */
|
|
41
|
+
"render-stuck": {
|
|
42
|
+
readonly id: number;
|
|
43
|
+
readonly page: number;
|
|
44
|
+
readonly needsPassword: true;
|
|
45
|
+
};
|
|
46
|
+
/** The worker crashed (not on `stop()`). */
|
|
47
|
+
crashed: {
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
};
|
|
50
|
+
/** A new worker serves again (and has reopened the document, unless it needed a password). */
|
|
51
|
+
restarted: {
|
|
52
|
+
readonly reason: RestartReason;
|
|
53
|
+
readonly reopened: boolean;
|
|
54
|
+
};
|
|
55
|
+
/** The restarted worker can't reopen the document without its password: open it again. */
|
|
56
|
+
"password-required": Record<string, never>;
|
|
57
|
+
/** A restart failed (the backend couldn't load again, …): the client serves nothing until `stop()` works. */
|
|
58
|
+
"restart-failed": {
|
|
59
|
+
readonly reason: RestartReason;
|
|
60
|
+
readonly error: unknown;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The names of the client's events. */
|
|
64
|
+
export type ViewerEvent = keyof ViewerEvents;
|
|
65
|
+
/** Per-call options. */
|
|
66
|
+
export interface ViewerCallOptions {
|
|
67
|
+
readonly signal?: AbortSignal;
|
|
68
|
+
/** Default `visible` for renders; document calls run above every render. */
|
|
69
|
+
readonly priority?: Priority;
|
|
70
|
+
}
|
|
71
|
+
/** One document worker and the typed RPC to it. */
|
|
72
|
+
export declare class ViewerClient {
|
|
73
|
+
#private;
|
|
74
|
+
private constructor();
|
|
75
|
+
/**
|
|
76
|
+
* Starts the document worker and loads the backend at once, so the WebAssembly downloads while
|
|
77
|
+
* the user picks a file.
|
|
78
|
+
*/
|
|
79
|
+
static create(options: ViewerOptions): Promise<ViewerClient>;
|
|
80
|
+
/** Listens for `event`; returns a function that stops listening. */
|
|
81
|
+
on<E extends ViewerEvent>(event: E, listener: (data: ViewerEvents[E]) => void): () => void;
|
|
82
|
+
/** A typed call: `method` and `params` are checked against `Methods`. */
|
|
83
|
+
call<M extends MethodName>(method: M, params: Params<M>, options?: ViewerCallOptions & {
|
|
84
|
+
key?: string;
|
|
85
|
+
}): Promise<Result<M>>;
|
|
86
|
+
/**
|
|
87
|
+
* Opens `source` in the worker (Lamina PDF and the backend, from the same bytes). The password is
|
|
88
|
+
* sent once and kept only in the worker; a Blob is sent by reference, bytes are copied (and
|
|
89
|
+
* kept here, so a restarted worker can reopen them).
|
|
90
|
+
*/
|
|
91
|
+
open(source: SourceInit, options?: ViewerOpenOptions, call?: ViewerCallOptions): Promise<OpenedDocument>;
|
|
92
|
+
/** The open document, if any. */
|
|
93
|
+
get document(): OpenedDocument | undefined;
|
|
94
|
+
/** Closes the document in the worker (the worker stays, ready for another `open`). */
|
|
95
|
+
close(): Promise<void>;
|
|
96
|
+
/** The worker's WebAssembly memory in bytes (see `WorkerMemory`), for measuring. */
|
|
97
|
+
memory(options?: ViewerCallOptions): Promise<import("./protocol.js").WorkerMemory>;
|
|
98
|
+
/** Page `index`'s size, rotation, ID and label. */
|
|
99
|
+
pageInfo(index: number, options?: ViewerCallOptions): Promise<import("@laminapdf/wasm/text").PageInfoJs>;
|
|
100
|
+
/**
|
|
101
|
+
* Renders `request` as an ImageBitmap (close it when done). Identical requests in flight are
|
|
102
|
+
* drawn once. An aborted render rejects with `AbortError` and never paints.
|
|
103
|
+
*/
|
|
104
|
+
render(request: RenderRequest, options?: ViewerCallOptions): Promise<RenderedBitmap>;
|
|
105
|
+
/**
|
|
106
|
+
* Stops whatever the worker is doing by terminating it (the user's Stop, or the hard cap), then
|
|
107
|
+
* restarts it as after a crash: pending calls reject, the running one isn't re-sent, and an
|
|
108
|
+
* encrypted document asks for its password again.
|
|
109
|
+
*/
|
|
110
|
+
stop(reason?: "cancelled" | "hard-timeout"): Promise<void>;
|
|
111
|
+
/** Ends the worker for good. */
|
|
112
|
+
terminate(): void;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|