@paged-media/idml-viewer 0.1.0-canary.0

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 ADDED
@@ -0,0 +1,52 @@
1
+ # @paged-media/idml-viewer
2
+
3
+ Standalone IDML viewer for the browser — camera, pages, input lanes and
4
+ events over the `@paged-media/sdk` WebGPU `ViewerSession`. Rendering is
5
+ WebGPU-only (the session rejects without `navigator.gpu`).
6
+
7
+ ```ts
8
+ import { createViewer, createSessionFromBundledWasm } from "@paged-media/idml-viewer";
9
+
10
+ const viewer = await createViewer({
11
+ canvas: document.querySelector("canvas")!,
12
+ session: createSessionFromBundledWasm, // wasm ships inside this package
13
+ });
14
+
15
+ viewer.on("pageChanged", ({ page }) => console.log("page", page + 1));
16
+ await viewer.load("/files/brochure.idml"); // URL | ArrayBuffer | Uint8Array | Blob
17
+ ```
18
+
19
+ ## Surface
20
+
21
+ - **Load** — `load(source)`, repeatable; failures throw a `ViewerError`
22
+ with `code: "PARSE_ERROR" | "UNSUPPORTED" | "GPU_UNAVAILABLE"` and the
23
+ engine's structured diagnostics attached.
24
+ - **Camera** — `zoom`, `setZoom(z, { anchor })`, `zoomIn/zoomOut`,
25
+ `fit("page" | "width")`, `minZoom/maxZoom`, `scroll`, `scrollTo`,
26
+ `scrollBy`. Zooming about an anchor keeps the document point under it
27
+ fixed.
28
+ - **Pages** — `pageCount`, `currentPage`, `goToPage(n)`,
29
+ `layoutMode("single" | "continuous")`,
30
+ `renderPageThumbnail(n, { width })` (RGBA8 readback).
31
+ - **Events** — `on("loaded" | "pageChanged" | "zoomChanged" |
32
+ "scrollChanged" | "error", cb)` → unsubscribe function.
33
+ - **Input** — wheel/pinch zoom-to-cursor, drag-pan, double-click zoom,
34
+ keyboard (`+`/`-`/`0`, arrows, PgUp/PgDn, Home/End); each lane
35
+ disableable via `input: {...}`.
36
+ - **Teardown** — `dispose()` detaches all listeners and frees the wasm
37
+ session.
38
+
39
+ Fonts: register faces on the session before `load`
40
+ (`session.register_font(family, style, bytes)`) — same contract as
41
+ `paged-inspect --font-family`.
42
+
43
+ ## Embedding your own session
44
+
45
+ `createViewer` takes any object satisfying `ViewerSessionLike` (the
46
+ typed contract of the wasm session), so you can init the wasm yourself
47
+ — e.g. from a custom URL or a worker — and hand it in. Tests do exactly
48
+ this with a fake.
49
+
50
+ ## License
51
+
52
+ MPL-2.0 OR LicenseRef-PMEL — see the repository `LICENSE.md`.
@@ -0,0 +1,11 @@
1
+ import type { ViewerSessionLike } from "./session.js";
2
+ /**
3
+ * Create a `ViewerSession` from the wasm bundled with the PUBLISHED
4
+ * package (`wasm/paged_sdk.js` + `wasm/paged_sdk_bg.wasm`, placed by
5
+ * the publish workflow — absent in a source checkout, where embedders
6
+ * inject a session built via `viewer/build-wasm.sh` instead).
7
+ *
8
+ * `wasmUrl` overrides the `.wasm` location for bundlers that move
9
+ * assets; default resolves next to the glue file.
10
+ */
11
+ export declare function createSessionFromBundledWasm(wasmUrl?: string): Promise<ViewerSessionLike>;
@@ -0,0 +1,30 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ *
6
+ * This file is part of paged (https://paged.media) and is additionally
7
+ * available under the Paged Media Enterprise License (PMEL). Full
8
+ * copyright and license information is available in LICENSE.md which is
9
+ * distributed with this source code.
10
+ *
11
+ * @copyright Copyright (c) And The Next GmbH
12
+ * @license MPL-2.0 OR Paged Media Enterprise License (PMEL)
13
+ */
14
+ /**
15
+ * Create a `ViewerSession` from the wasm bundled with the PUBLISHED
16
+ * package (`wasm/paged_sdk.js` + `wasm/paged_sdk_bg.wasm`, placed by
17
+ * the publish workflow — absent in a source checkout, where embedders
18
+ * inject a session built via `viewer/build-wasm.sh` instead).
19
+ *
20
+ * `wasmUrl` overrides the `.wasm` location for bundlers that move
21
+ * assets; default resolves next to the glue file.
22
+ */
23
+ export async function createSessionFromBundledWasm(wasmUrl) {
24
+ const glueUrl = new URL("../wasm/paged_sdk.js", import.meta.url).href;
25
+ // Dynamic, computed import — left untouched by TS and bundlers so
26
+ // the glue resolves at runtime inside the published package.
27
+ const mod = (await import(/* @vite-ignore */ glueUrl));
28
+ await mod.default(wasmUrl ? { module_or_path: wasmUrl } : undefined);
29
+ return mod.ViewerSession["new"]();
30
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pure camera math over the continuous page stack. Doc space is pt
3
+ * (pages stacked vertically per `page_layout()`); the camera maps a
4
+ * doc point `p` to viewport CSS px as `scroll + p * zoom`. Everything
5
+ * here is side-effect-free so the invariants (zoom-to-anchor, fit,
6
+ * clamps, current-page) are unit-testable without a canvas.
7
+ */
8
+ import type { SessionPagesLayout } from "./session.js";
9
+ export interface Camera {
10
+ /** CSS px per pt. */
11
+ zoom: number;
12
+ /** Doc origin in viewport CSS px. */
13
+ x: number;
14
+ y: number;
15
+ }
16
+ export interface Viewport {
17
+ /** CSS px. */
18
+ width: number;
19
+ height: number;
20
+ }
21
+ export interface ZoomLimits {
22
+ min: number;
23
+ max: number;
24
+ }
25
+ /** Default page-fit margin — matches the wasm fit-page present. */
26
+ export declare const FIT_MARGIN = 0.95;
27
+ export declare function clampZoom(zoom: number, limits: ZoomLimits): number;
28
+ /** Total doc-space extents of the continuous stack (pt). */
29
+ export declare function contentExtent(layout: SessionPagesLayout): {
30
+ width: number;
31
+ height: number;
32
+ };
33
+ /**
34
+ * Zoom about an anchor point (viewport CSS px): the doc point under
35
+ * the anchor stays under it. With no anchor, zoom about the viewport
36
+ * centre.
37
+ */
38
+ export declare function zoomAt(camera: Camera, nextZoom: number, limits: ZoomLimits, viewport: Viewport, anchor?: {
39
+ x: number;
40
+ y: number;
41
+ }): Camera;
42
+ /**
43
+ * Keep content on screen: per axis, content smaller than the viewport
44
+ * centres; larger content clamps so no blank gap opens at either edge.
45
+ */
46
+ export declare function clampScroll(camera: Camera, layout: SessionPagesLayout, viewport: Viewport): Camera;
47
+ /**
48
+ * Fit a page: `"page"` fits both dimensions, `"width"` fits the page
49
+ * width (top-aligned to the page). Returns the camera centred on the
50
+ * page in continuous coordinates; single-page mode passes a layout
51
+ * containing just that page at `yPt: 0`.
52
+ */
53
+ export declare function fitPage(layout: SessionPagesLayout, pageIndex: number, mode: "page" | "width", viewport: Viewport, limits: ZoomLimits): Camera;
54
+ /**
55
+ * The page whose band contains the viewport-centre line (continuous
56
+ * mode). Falls back to the nearest band when the centre sits in a gap.
57
+ */
58
+ export declare function currentPageAt(camera: Camera, layout: SessionPagesLayout, viewport: Viewport): number;
59
+ /** Scroll that brings `pageIndex`'s top into view (continuous mode). */
60
+ export declare function scrollToPage(camera: Camera, layout: SessionPagesLayout, pageIndex: number, viewport: Viewport): Camera;
package/dist/camera.js ADDED
@@ -0,0 +1,113 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ *
6
+ * This file is part of paged (https://paged.media) and is additionally
7
+ * available under the Paged Media Enterprise License (PMEL). Full
8
+ * copyright and license information is available in LICENSE.md which is
9
+ * distributed with this source code.
10
+ *
11
+ * @copyright Copyright (c) And The Next GmbH
12
+ * @license MPL-2.0 OR Paged Media Enterprise License (PMEL)
13
+ */
14
+ /** Default page-fit margin — matches the wasm fit-page present. */
15
+ export const FIT_MARGIN = 0.95;
16
+ export function clampZoom(zoom, limits) {
17
+ return Math.min(limits.max, Math.max(limits.min, zoom));
18
+ }
19
+ /** Total doc-space extents of the continuous stack (pt). */
20
+ export function contentExtent(layout) {
21
+ let width = 0;
22
+ let height = 0;
23
+ for (const p of layout.pages) {
24
+ width = Math.max(width, p.widthPt);
25
+ height = Math.max(height, p.yPt + p.heightPt);
26
+ }
27
+ return { width, height };
28
+ }
29
+ /**
30
+ * Zoom about an anchor point (viewport CSS px): the doc point under
31
+ * the anchor stays under it. With no anchor, zoom about the viewport
32
+ * centre.
33
+ */
34
+ export function zoomAt(camera, nextZoom, limits, viewport, anchor) {
35
+ const z = clampZoom(nextZoom, limits);
36
+ const ax = anchor?.x ?? viewport.width / 2;
37
+ const ay = anchor?.y ?? viewport.height / 2;
38
+ const docX = (ax - camera.x) / camera.zoom;
39
+ const docY = (ay - camera.y) / camera.zoom;
40
+ return { zoom: z, x: ax - docX * z, y: ay - docY * z };
41
+ }
42
+ /**
43
+ * Keep content on screen: per axis, content smaller than the viewport
44
+ * centres; larger content clamps so no blank gap opens at either edge.
45
+ */
46
+ export function clampScroll(camera, layout, viewport) {
47
+ const { width, height } = contentExtent(layout);
48
+ const w = width * camera.zoom;
49
+ const h = height * camera.zoom;
50
+ const clampAxis = (pos, content, view) => content <= view
51
+ ? (view - content) / 2
52
+ : Math.min(0, Math.max(view - content, pos));
53
+ return {
54
+ zoom: camera.zoom,
55
+ x: clampAxis(camera.x, w, viewport.width),
56
+ y: clampAxis(camera.y, h, viewport.height),
57
+ };
58
+ }
59
+ /**
60
+ * Fit a page: `"page"` fits both dimensions, `"width"` fits the page
61
+ * width (top-aligned to the page). Returns the camera centred on the
62
+ * page in continuous coordinates; single-page mode passes a layout
63
+ * containing just that page at `yPt: 0`.
64
+ */
65
+ export function fitPage(layout, pageIndex, mode, viewport, limits) {
66
+ const page = layout.pages[pageIndex] ?? layout.pages[0];
67
+ if (!page)
68
+ return { zoom: 1, x: 0, y: 0 };
69
+ const pw = Math.max(page.widthPt, 1);
70
+ const ph = Math.max(page.heightPt, 1);
71
+ const zoom = clampZoom(mode === "page"
72
+ ? Math.min(viewport.width / pw, viewport.height / ph) * FIT_MARGIN
73
+ : (viewport.width / pw) * FIT_MARGIN, limits);
74
+ const x = (viewport.width - pw * zoom) / 2;
75
+ const y = mode === "page"
76
+ ? (viewport.height - ph * zoom) / 2 - page.yPt * zoom
77
+ : (viewport.height - ph * zoom) / 2 -
78
+ page.yPt * zoom +
79
+ Math.max(0, (ph * zoom - viewport.height) / 2);
80
+ return { zoom, x, y };
81
+ }
82
+ /**
83
+ * The page whose band contains the viewport-centre line (continuous
84
+ * mode). Falls back to the nearest band when the centre sits in a gap.
85
+ */
86
+ export function currentPageAt(camera, layout, viewport) {
87
+ if (layout.pages.length === 0)
88
+ return 0;
89
+ const centreDocY = (viewport.height / 2 - camera.y) / camera.zoom;
90
+ let best = 0;
91
+ let bestDist = Number.POSITIVE_INFINITY;
92
+ for (const p of layout.pages) {
93
+ if (centreDocY >= p.yPt && centreDocY <= p.yPt + p.heightPt)
94
+ return p.index;
95
+ const mid = p.yPt + p.heightPt / 2;
96
+ const dist = Math.abs(mid - centreDocY);
97
+ if (dist < bestDist) {
98
+ bestDist = dist;
99
+ best = p.index;
100
+ }
101
+ }
102
+ return best;
103
+ }
104
+ /** Scroll that brings `pageIndex`'s top into view (continuous mode). */
105
+ export function scrollToPage(camera, layout, pageIndex, viewport) {
106
+ const page = layout.pages[pageIndex];
107
+ if (!page)
108
+ return camera;
109
+ const x = (viewport.width - page.widthPt * camera.zoom) / 2;
110
+ // Small breathing room above the page top, mirroring the gap.
111
+ const y = -page.yPt * camera.zoom + Math.min(12, layout.gapPt * camera.zoom);
112
+ return { zoom: camera.zoom, x, y };
113
+ }
@@ -0,0 +1,7 @@
1
+ /** Minimal typed emitter — no wildcard, no once, no bubbling. */
2
+ export declare class Emitter<Events extends Record<string, unknown>> {
3
+ private listeners;
4
+ on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): () => void;
5
+ emit<K extends keyof Events>(event: K, payload: Events[K]): void;
6
+ clear(): void;
7
+ }
package/dist/events.js ADDED
@@ -0,0 +1,37 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ *
6
+ * This file is part of paged (https://paged.media) and is additionally
7
+ * available under the Paged Media Enterprise License (PMEL). Full
8
+ * copyright and license information is available in LICENSE.md which is
9
+ * distributed with this source code.
10
+ *
11
+ * @copyright Copyright (c) And The Next GmbH
12
+ * @license MPL-2.0 OR Paged Media Enterprise License (PMEL)
13
+ */
14
+ /** Minimal typed emitter — no wildcard, no once, no bubbling. */
15
+ export class Emitter {
16
+ listeners = new Map();
17
+ on(event, listener) {
18
+ let set = this.listeners.get(event);
19
+ if (!set) {
20
+ set = new Set();
21
+ this.listeners.set(event, set);
22
+ }
23
+ set.add(listener);
24
+ return () => set?.delete(listener);
25
+ }
26
+ emit(event, payload) {
27
+ const set = this.listeners.get(event);
28
+ if (!set)
29
+ return;
30
+ for (const listener of [...set]) {
31
+ listener(payload);
32
+ }
33
+ }
34
+ clear() {
35
+ this.listeners.clear();
36
+ }
37
+ }
@@ -0,0 +1,7 @@
1
+ export { createViewer, ViewerError } from "./viewer.js";
2
+ export { createSessionFromBundledWasm } from "./bootstrap.js";
3
+ export type { CreateViewerOptions, LayoutMode, Viewer, ViewerErrorCode, ViewerEvents, } from "./viewer.js";
4
+ export type { InputOptions } from "./input.js";
5
+ export type { SessionDiagnostic, SessionDiagnostics, SessionPageRect, SessionPagesLayout, SessionRaster, ViewerSessionLike, } from "./session.js";
6
+ export { clampScroll, clampZoom, contentExtent, currentPageAt, fitPage, scrollToPage, zoomAt, } from "./camera.js";
7
+ export type { Camera, Viewport, ZoomLimits } from "./camera.js";
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ *
6
+ * This file is part of paged (https://paged.media) and is additionally
7
+ * available under the Paged Media Enterprise License (PMEL). Full
8
+ * copyright and license information is available in LICENSE.md which is
9
+ * distributed with this source code.
10
+ *
11
+ * @copyright Copyright (c) And The Next GmbH
12
+ * @license MPL-2.0 OR Paged Media Enterprise License (PMEL)
13
+ */
14
+ export { createViewer, ViewerError } from "./viewer.js";
15
+ export { createSessionFromBundledWasm } from "./bootstrap.js";
16
+ export { clampScroll, clampZoom, contentExtent, currentPageAt, fitPage, scrollToPage, zoomAt, } from "./camera.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Input lanes for the viewer — each independently disableable:
3
+ * - wheel: Ctrl/⌘-wheel and pinch (`ctrlKey` wheel) zoom to cursor,
4
+ * plain wheel pans.
5
+ * - drag: pointer-drag pans.
6
+ * - double-click: zoom step at the cursor.
7
+ * - keyboard: `+`/`-`/`0`, arrows, PgUp/PgDn, Home/End.
8
+ */
9
+ export interface InputOptions {
10
+ wheelZoom?: boolean;
11
+ dragPan?: boolean;
12
+ doubleClickZoom?: boolean;
13
+ keyboard?: boolean;
14
+ }
15
+ export interface InputSink {
16
+ zoomAt(factor: number, anchor: {
17
+ x: number;
18
+ y: number;
19
+ }): void;
20
+ panBy(dx: number, dy: number): void;
21
+ zoomStep(direction: 1 | -1, anchor?: {
22
+ x: number;
23
+ y: number;
24
+ }): void;
25
+ fit(): void;
26
+ pageStep(direction: 1 | -1): void;
27
+ home(): void;
28
+ end(): void;
29
+ }
30
+ /** Attach listeners; returns the detach function. */
31
+ export declare function attachInput(canvas: HTMLCanvasElement, options: InputOptions, sink: InputSink): () => void;
package/dist/input.js ADDED
@@ -0,0 +1,122 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ *
6
+ * This file is part of paged (https://paged.media) and is additionally
7
+ * available under the Paged Media Enterprise License (PMEL). Full
8
+ * copyright and license information is available in LICENSE.md which is
9
+ * distributed with this source code.
10
+ *
11
+ * @copyright Copyright (c) And The Next GmbH
12
+ * @license MPL-2.0 OR Paged Media Enterprise License (PMEL)
13
+ */
14
+ const ARROW_PAN_PX = 48;
15
+ /** Attach listeners; returns the detach function. */
16
+ export function attachInput(canvas, options, sink) {
17
+ const teardown = [];
18
+ const listen = (type, handler, opts) => {
19
+ canvas.addEventListener(type, handler, opts);
20
+ teardown.push(() => canvas.removeEventListener(type, handler, opts));
21
+ };
22
+ const anchorOf = (e) => {
23
+ const rect = canvas.getBoundingClientRect();
24
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
25
+ };
26
+ if (options.wheelZoom !== false) {
27
+ listen("wheel", (e) => {
28
+ e.preventDefault();
29
+ if (e.ctrlKey || e.metaKey) {
30
+ // Trackpad pinch arrives as ctrlKey wheel; exponential map
31
+ // keeps pinch and wheel-notch speeds comparable.
32
+ sink.zoomAt(Math.exp(-e.deltaY * 0.0024), anchorOf(e));
33
+ }
34
+ else {
35
+ sink.panBy(-e.deltaX, -e.deltaY);
36
+ }
37
+ }, { passive: false });
38
+ }
39
+ if (options.dragPan !== false) {
40
+ let dragging = false;
41
+ let lastX = 0;
42
+ let lastY = 0;
43
+ listen("pointerdown", (e) => {
44
+ if (e.button !== 0)
45
+ return;
46
+ dragging = true;
47
+ lastX = e.clientX;
48
+ lastY = e.clientY;
49
+ canvas.setPointerCapture?.(e.pointerId);
50
+ });
51
+ listen("pointermove", (e) => {
52
+ if (!dragging)
53
+ return;
54
+ sink.panBy(e.clientX - lastX, e.clientY - lastY);
55
+ lastX = e.clientX;
56
+ lastY = e.clientY;
57
+ });
58
+ const stop = (e) => {
59
+ dragging = false;
60
+ canvas.releasePointerCapture?.(e.pointerId);
61
+ };
62
+ listen("pointerup", stop);
63
+ listen("pointercancel", stop);
64
+ }
65
+ if (options.doubleClickZoom !== false) {
66
+ listen("dblclick", (e) => {
67
+ e.preventDefault();
68
+ sink.zoomStep(e.shiftKey ? -1 : 1, anchorOf(e));
69
+ });
70
+ }
71
+ if (options.keyboard !== false) {
72
+ // The canvas needs focusability for keydown.
73
+ if (!canvas.hasAttribute("tabindex"))
74
+ canvas.setAttribute("tabindex", "0");
75
+ listen("keydown", (e) => {
76
+ switch (e.key) {
77
+ case "+":
78
+ case "=":
79
+ sink.zoomStep(1);
80
+ break;
81
+ case "-":
82
+ case "_":
83
+ sink.zoomStep(-1);
84
+ break;
85
+ case "0":
86
+ sink.fit();
87
+ break;
88
+ case "ArrowLeft":
89
+ sink.panBy(ARROW_PAN_PX, 0);
90
+ break;
91
+ case "ArrowRight":
92
+ sink.panBy(-ARROW_PAN_PX, 0);
93
+ break;
94
+ case "ArrowUp":
95
+ sink.panBy(0, ARROW_PAN_PX);
96
+ break;
97
+ case "ArrowDown":
98
+ sink.panBy(0, -ARROW_PAN_PX);
99
+ break;
100
+ case "PageUp":
101
+ sink.pageStep(-1);
102
+ break;
103
+ case "PageDown":
104
+ sink.pageStep(1);
105
+ break;
106
+ case "Home":
107
+ sink.home();
108
+ break;
109
+ case "End":
110
+ sink.end();
111
+ break;
112
+ default:
113
+ return;
114
+ }
115
+ e.preventDefault();
116
+ });
117
+ }
118
+ return () => {
119
+ for (const detach of teardown)
120
+ detach();
121
+ };
122
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The typed contract of the wasm `ViewerSession`
3
+ * (`core/crates/paged-sdk/src/lib.rs`). The viewer programs against
4
+ * this interface so tests can inject a fake session and embedders can
5
+ * wrap their own init flow. Method names mirror the wasm-bindgen
6
+ * exports (snake_case); the viewer surface re-exposes everything in
7
+ * idiomatic camelCase.
8
+ */
9
+ /** One diagnostic from `load` / `render_*`. */
10
+ export interface SessionDiagnostic {
11
+ /** `"error" | "warning" | "info"`. */
12
+ severity: string;
13
+ /** Short machine code, e.g. `"open"`, `"build"`, `"no_gpu"`. */
14
+ code: string;
15
+ message: string;
16
+ part?: string | null;
17
+ line?: number | null;
18
+ }
19
+ export interface SessionDiagnostics {
20
+ ok: boolean;
21
+ messages: SessionDiagnostic[];
22
+ }
23
+ /** Continuous-layout geometry from `page_layout()`. */
24
+ export interface SessionPageRect {
25
+ index: number;
26
+ yPt: number;
27
+ widthPt: number;
28
+ heightPt: number;
29
+ }
30
+ export interface SessionPagesLayout {
31
+ gapPt: number;
32
+ pages: SessionPageRect[];
33
+ }
34
+ export interface SessionRaster {
35
+ width: number;
36
+ height: number;
37
+ rgba: Uint8Array;
38
+ }
39
+ export interface ViewerSessionLike {
40
+ load(idml: Uint8Array, font?: Uint8Array): SessionDiagnostics;
41
+ register_font(family: string, style: string | undefined, bytes: Uint8Array): void;
42
+ page_count(): number;
43
+ set_page(index: number): void;
44
+ page_layout(): SessionPagesLayout;
45
+ /**
46
+ * Camera-transformed present of the continuous page stack. `zoom`
47
+ * is CSS px per pt; `scrollX`/`scrollY` place the doc origin in CSS
48
+ * px; `onlyPage` restricts to one page laid out at y = 0.
49
+ */
50
+ present(zoom: number, scrollX: number, scrollY: number, dpr: number, onlyPage?: number | null): SessionDiagnostics;
51
+ /** Binds the presenter to `canvas` on first call (fit-page paint). */
52
+ render_to_canvas_main(canvas: HTMLCanvasElement): Promise<SessionDiagnostics>;
53
+ render_page_to_bytes(index: number, targetWidthPx: number): Promise<SessionRaster>;
54
+ resize(width: number, height: number, devicePixelRatio: number): void;
55
+ /** wasm-bindgen finalizer. */
56
+ free?(): void;
57
+ }
@@ -0,0 +1,14 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ *
6
+ * This file is part of paged (https://paged.media) and is additionally
7
+ * available under the Paged Media Enterprise License (PMEL). Full
8
+ * copyright and license information is available in LICENSE.md which is
9
+ * distributed with this source code.
10
+ *
11
+ * @copyright Copyright (c) And The Next GmbH
12
+ * @license MPL-2.0 OR Paged Media Enterprise License (PMEL)
13
+ */
14
+ export {};
@@ -0,0 +1,80 @@
1
+ import { type ZoomLimits } from "./camera.js";
2
+ import { type InputOptions } from "./input.js";
3
+ import type { SessionDiagnostics, SessionRaster, ViewerSessionLike } from "./session.js";
4
+ /** Structured load/init failure. */
5
+ export type ViewerErrorCode = "PARSE_ERROR" | "UNSUPPORTED" | "GPU_UNAVAILABLE";
6
+ export declare class ViewerError extends Error {
7
+ readonly code: ViewerErrorCode;
8
+ readonly diagnostics: SessionDiagnostics | undefined;
9
+ constructor(code: ViewerErrorCode, message: string, diagnostics?: SessionDiagnostics);
10
+ }
11
+ export type LayoutMode = "single" | "continuous";
12
+ export interface ViewerEvents extends Record<string, unknown> {
13
+ loaded: {
14
+ pageCount: number;
15
+ };
16
+ pageChanged: {
17
+ page: number;
18
+ };
19
+ zoomChanged: {
20
+ zoom: number;
21
+ };
22
+ scrollChanged: {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ error: ViewerError;
27
+ }
28
+ export interface CreateViewerOptions {
29
+ canvas: HTMLCanvasElement;
30
+ /**
31
+ * The wasm session (or a factory). Embedders typically pass
32
+ * `await ViewerSession.new()` from `@paged-media/sdk` after
33
+ * `init(wasmUrl)`; tests pass a fake.
34
+ */
35
+ session: ViewerSessionLike | (() => Promise<ViewerSessionLike>);
36
+ /** Zoom clamps in CSS px per pt. Default `{ min: 0.1, max: 8 }`. */
37
+ zoomLimits?: Partial<ZoomLimits>;
38
+ /** Input lanes; each defaults to enabled. */
39
+ input?: InputOptions;
40
+ /** Initial layout mode. Default `"continuous"`. */
41
+ layoutMode?: LayoutMode;
42
+ /**
43
+ * Frame scheduler — injectable for tests. Defaults to
44
+ * `requestAnimationFrame` (microtask fallback off-DOM).
45
+ */
46
+ schedule?: (frame: () => void) => void;
47
+ /** Device pixel ratio override (defaults to `window.devicePixelRatio`). */
48
+ devicePixelRatio?: number;
49
+ }
50
+ export interface Viewer {
51
+ load(source: ArrayBuffer | Uint8Array | Blob | string): Promise<void>;
52
+ readonly zoom: number;
53
+ setZoom(zoom: number, opts?: {
54
+ anchor?: {
55
+ x: number;
56
+ y: number;
57
+ };
58
+ }): void;
59
+ zoomIn(): void;
60
+ zoomOut(): void;
61
+ fit(mode: "page" | "width"): void;
62
+ readonly minZoom: number;
63
+ readonly maxZoom: number;
64
+ readonly scroll: {
65
+ x: number;
66
+ y: number;
67
+ };
68
+ scrollTo(x: number, y: number): void;
69
+ scrollBy(dx: number, dy: number): void;
70
+ readonly pageCount: number;
71
+ readonly currentPage: number;
72
+ goToPage(index: number): void;
73
+ layoutMode(mode?: LayoutMode): LayoutMode;
74
+ renderPageThumbnail(index: number, opts?: {
75
+ width?: number;
76
+ }): Promise<SessionRaster>;
77
+ on<K extends keyof ViewerEvents>(event: K, listener: (payload: ViewerEvents[K]) => void): () => void;
78
+ dispose(): void;
79
+ }
80
+ export declare function createViewer(options: CreateViewerOptions): Promise<Viewer>;