@aseity/idocs-preview 0.0.0-stage → 0.2.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -2
  4. package/dist/core/display.d.ts +180 -0
  5. package/dist/core/display.js +4 -0
  6. package/dist/core/engine.d.ts +9 -0
  7. package/dist/core/engine.js +81 -0
  8. package/dist/core/fonts.d.ts +27 -0
  9. package/dist/core/fonts.js +135 -0
  10. package/dist/core/index.d.ts +47 -0
  11. package/dist/core/index.js +156 -0
  12. package/dist/core/protocol.d.ts +137 -0
  13. package/dist/core/protocol.js +1 -0
  14. package/dist/core/wire.d.ts +14 -0
  15. package/dist/core/wire.js +100 -0
  16. package/dist/core/worker.d.ts +1 -0
  17. package/dist/core/worker.js +5 -0
  18. package/dist/view/find-bar.d.ts +26 -0
  19. package/dist/view/find-bar.js +117 -0
  20. package/dist/view/find.d.ts +46 -0
  21. package/dist/view/find.js +121 -0
  22. package/dist/view/index.d.ts +27 -0
  23. package/dist/view/index.js +274 -0
  24. package/dist/view/selection-layer.d.ts +27 -0
  25. package/dist/view/selection-layer.js +269 -0
  26. package/dist/view/selection.d.ts +131 -0
  27. package/dist/view/selection.js +350 -0
  28. package/dist/view/shell.d.ts +77 -0
  29. package/dist/view/shell.js +452 -0
  30. package/dist/view/viewport.d.ts +100 -0
  31. package/dist/view/viewport.js +145 -0
  32. package/package.json +49 -4
  33. package/wasm/hyphenation-licenses/LICENSE-APACHE +201 -0
  34. package/wasm/hyphenation-licenses/LICENSE-MIT +21 -0
  35. package/wasm/hyphenation-licenses/README.md +26 -0
  36. package/wasm/hyphenation-licenses/en-gb.tex +8615 -0
  37. package/wasm/hyphenation-licenses/en-us.tex +5018 -0
  38. package/wasm/hyphenation-licenses/ushyphex.tex +1454 -0
  39. package/wasm/idocs_wasm.d.ts +45 -0
  40. package/wasm/idocs_wasm.js +405 -0
  41. package/wasm/idocs_wasm_bg.wasm +0 -0
  42. package/wasm/idocs_wasm_bg.wasm.d.ts +10 -0
  43. package/wasm/package.json +1 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # @aseity/idocs-preview
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Use browser-local fonts for text measurement and Canvas drawing without downloading font files. Remove the font-file mode and the `readFont` / `readFamily` callbacks from the web API, and exclude the native shaping engine from the WASM bundle to reduce its download size. Mathematical symbols retain their embedded fonts. Different installed fonts can change line breaks and pagination.
8
+ - Floating objects (`floats` on a paragraph) now show in the preview: pictures, shapes, text boxes
9
+ and groups, over or behind the text, turned and flipped about their centres. The display list
10
+ gains two item kinds, `shape` (SVG path data, filled and/or stroked, with dashes) and `transform`
11
+ (an SVG matrix around rotated or flipped objects), and the `textbox` story for a text box's glyph
12
+ runs, whose lines are not in the hit-testing table.
13
+
14
+ ### Patch Changes
15
+
16
+ - Lines between bordered paragraphs (Word's "between" paragraph border, `w:pBdr/w:between`)
17
+ now show in the preview: a line between each two adjacent paragraphs that share it, with
18
+ its spacing above and below, and none above the first or below the last.
19
+ - Numbered and bulleted lists in table cells now show in the preview as lists: their labels
20
+ count on with the lists outside the table in document order (row by row, cell by cell),
21
+ and their indents are measured from the cell's text area, as Word does. Converted Word
22
+ documents keep the numbering of list paragraphs in table cells instead of dropping it.
23
+ - Emphasis marks (Word's `w:em`: dot, comma, circle, under dot) now show in the preview, one
24
+ per character except spaces. Path items in the display list may carry the role
25
+ `emphasisMark`.
26
+ - Text set to a fixed width in Word (Fit Text, `w:fitText`) now keeps that width in the
27
+ preview: wider spacing when the text is shorter, glyphs compressed horizontally when it
28
+ is longer. Glyph runs in the display list carry `scaleX` when compressed.
29
+ - Tables in table cells now show in the preview as tables: each sits in its cell's text area
30
+ the way a table sits in the page's text area, the outer row grows to hold it, and a row
31
+ that runs onto the next page splits the table in it between its rows, as Word does.
32
+ Converted Word documents keep a table in a cell as a table instead of flattening it into
33
+ the cell's paragraphs.
34
+ - Character scale and baseline shift (Word's `w:w` and `w:position`) now show in the preview:
35
+ `scaleX` on a glyph run also carries the character scale, and `baseline` the shift.
36
+
37
+ ## 0.1.0
38
+
39
+ ### Minor Changes
40
+
41
+ - First public release of the read-only browser preview for DOCX and iDocs source
42
+ documents.
43
+
44
+ - `@aseity/idocs-preview/core`: a headless session that converts and lays out documents
45
+ with Word-calibrated metrics in a Web Worker and returns each page's display list,
46
+ diagnostics and conversion losses.
47
+ - `@aseity/idocs-preview/view`: a Canvas page view with virtualized pages, text
48
+ selection and copying, a find bar, zoom with fit-width support, Ctrl+wheel and
49
+ pinch zoom around the pointer, and page navigation with optional smooth
50
+ scrolling.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iDocs contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,207 @@
1
- # Temporary Holding Version
1
+ # iDocs preview
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Read-only browser preview for Word documents (`.docx`) and iDocs source documents
4
+ (`.idoc`). A Web Worker measures browser-local fonts with Canvas and lays out
5
+ the document in WASM. Canvas draws text using the same font settings. No text
6
+ font downloads or Local Font Access permissions are required. Different installed
7
+ fonts can change line breaks and page counts.
8
+
9
+ The package ships ES modules, TypeScript declarations, the worker and the WASM
10
+ binary. It has no framework dependency and runs only in browsers.
11
+
12
+ ```sh
13
+ npm install @aseity/idocs-preview
14
+ ```
15
+
16
+ ## Local development
17
+
18
+ ```sh
19
+ npm ci
20
+ npm run dev
21
+ ```
22
+
23
+ The build prepends `$CARGO_HOME/bin` (default `~/.cargo/bin`) to its own PATH
24
+ so rustup selects `rust/rust-toolchain.toml`, even when Homebrew Rust comes
25
+ first in your shell. The toolchain file includes the WASM target. No global
26
+ PATH changes or `.env` file are needed. Install rustup and wasm-bindgen CLI
27
+ 0.2.128 before building.
28
+
29
+ ## Quick start
30
+
31
+ ```ts
32
+ const { createPreview } = await import("@aseity/idocs-preview/view");
33
+
34
+ const preview = createPreview(container, {
35
+ onPageChange: (index, doc) => console.log(`page ${index + 1} of ${doc.pageCount}`),
36
+ onZoomChange: (zoom) => console.log(`zoomed to ${Math.round(zoom * 100)}%`),
37
+ });
38
+ const doc = await preview.open(bytes, { name: "report.docx" });
39
+ preview.setZoom(preview.view?.fitWidthZoom ?? 1);
40
+ preview.goToPage(2, { behavior: "smooth" });
41
+ preview.destroy();
42
+ ```
43
+
44
+ `container` must have a height; the view fills it and scrolls inside it. Load the
45
+ package lazily on the client (a dynamic `import()` or a client-only component), not
46
+ during server rendering.
47
+
48
+ ## Subpaths
49
+
50
+ - `@aseity/idocs-preview/core`: the headless session. `createSession()` starts the
51
+ worker; `session.open(bytes, options)` resolves to a `PreviewDocument` with the
52
+ pages' display lists, diagnostics and conversion losses.
53
+ - `@aseity/idocs-preview/view`: the reference view. `createPreview(container)`
54
+ owns a session and a view; `mountPreview(container, doc)` shows a document you
55
+ opened yourself. Both re-export the core types.
56
+
57
+ ## Opening documents
58
+
59
+ `open(bytes, { name?, kind?, readResource? })` takes a `Uint8Array` or
60
+ `ArrayBuffer`. The package copies the bytes before transferring them to the worker,
61
+ so the caller can keep using them, for example to download the original.
62
+
63
+ - `kind` defaults to `docx` for ZIP or OLE bytes or a Word file name, and to `idoc`
64
+ otherwise.
65
+ - `readResource(path)` resolves images referenced by an `.idoc` file, relative to
66
+ that file. DOCX images are read from the file itself.
67
+ - A failed open rejects with `PreviewError`, whose `code` is one of
68
+ `invalid-source`, `layout-failed`, `unsupported-format`, `wasm-unavailable`,
69
+ `worker-unavailable`, `internal-error`, or a DOCX conversion code
70
+ (`docx-not-zip`, `docx-encrypted`, `docx-legacy-doc`, `docx-invalid-package`,
71
+ `docx-strict-ooxml`).
72
+
73
+ DOCX conversion is lossy: supported floating images, shapes, text boxes and groups
74
+ are preserved as floating objects. Tracked changes, comments and object properties
75
+ outside the model are downgraded or omitted and listed in `conversionLosses`.
76
+
77
+ ### Preview transport
78
+
79
+ Every open transfers glyph coordinates and text indices in binary buffers.
80
+ Coordinates retain double precision. The painter and hit testing read those
81
+ buffers directly. Page, line, and drawing metadata still use JSON.
82
+
83
+ Opened previews expose read-only glyph arrays. Indexed glyph objects are
84
+ snapshots; changing one does not change the drawing buffer. Array traversal,
85
+ selection, copying, and JSON serialization remain available.
86
+
87
+ By default, the view displays the document after every page is ready.
88
+ `createPreview(container, { progressive: true })` shows a provisional first-page
89
+ canvas while later display pages are generated. Full pagination completes before
90
+ any page is sent. Selection and find become available when `open` resolves.
91
+ A failure removes the provisional page.
92
+
93
+ Headless sessions can receive provisional pages through `open(bytes, { onPage })`.
94
+ Each packet contains a page, the total page count, and cumulative image and glyph
95
+ outline resources. Resource arrays grow as later pages arrive. Resource retries
96
+ can restart delivery from page one. The final promise resolves to the complete
97
+ document. A callback exception rejects its open.
98
+
99
+ The development page enables progressive delivery with `?progressive=1`.
100
+ There is no `compact` option or legacy JSON-only transport.
101
+
102
+ ## View
103
+
104
+ - Zoom 1 is 100% (one point is 4/3 CSS pixels), clamped to 0.1–10. `setZoom` keeps
105
+ the spot at the top of the viewport in place. `view.fitWidthZoom` is the zoom at
106
+ which the widest page fills the viewport width; the view does not refit by
107
+ itself.
108
+ - Ctrl+wheel and trackpad pinch zoom around the pointer and report the new zoom
109
+ through `onZoomChange`. `setZoom` does not call it.
110
+ - `goToPage(index, { behavior })` scrolls a page (0-based) to the top. Pass
111
+ `"smooth"` to animate, and `"instant"` when the reader prefers reduced motion.
112
+ - Pages are virtualized: only the visible pages and one on either side keep a
113
+ canvas.
114
+ - Text can be selected and copied. Ctrl+F (Cmd+F on macOS) opens the built-in
115
+ find bar while the preview has focus. Escape that closes the find bar is marked
116
+ with `preventDefault()`, so hosts that close their panel on Escape should skip
117
+ events whose `defaultPrevented` is set.
118
+
119
+ ## Bundlers and hosting
120
+
121
+ The session creates its worker with `new Worker(new URL("./worker.js",
122
+ import.meta.url), { type: "module" })`, and the WASM glue loads
123
+ `new URL("idocs_wasm_bg.wasm", import.meta.url)`. Vite, webpack and Turbopack
124
+ (Next.js 16) recognize both forms and emit the worker chunk and the WASM asset
125
+ without configuration.
126
+
127
+ Without a bundler, host the package files at their relative positions and pass the
128
+ addresses: `createPreview(el, { session: { workerUrl, wasmUrl } })`, where
129
+ `workerUrl` points to `dist/core/worker.js` and `wasmUrl` to
130
+ `wasm/idocs_wasm_bg.wasm`.
131
+
132
+ Use the same package version for the session and its worker. The successful worker
133
+ response carries metadata JSON, a binary glyph buffer, and separate image buffers. Progressive opens send resource deltas and page packets
134
+ before the final response. Custom workers must implement the corresponding
135
+ message variants in `src/core/protocol.ts`.
136
+
137
+ The worker is a same-origin module worker. Serve the WASM file as
138
+ `application/wasm`. A Content Security Policy needs `script-src
139
+ 'wasm-unsafe-eval'` and `worker-src 'self'`.
140
+
141
+ ## Fonts
142
+
143
+ The web preview uses installed browser fonts. The Worker uses
144
+ OffscreenCanvas `measureText()` for widths and font extents; the view uses the same
145
+ CSS family chain, weight, style and kerning policy with `fillText()`. Missing families
146
+ fall back through the browser's installed fonts. Source text and UTF-16 offsets
147
+ remain available for selection, copying and search. Math continues to use embedded
148
+ KaTeX resources and does not download fonts.
149
+
150
+ Native snapshots use actual font files. Their line breaks and page counts may
151
+ therefore differ from a browser with different installed fonts.
152
+
153
+ The WASM build excludes the native font-file feature and HarfRust. There is no
154
+ font-file mode, `readFont` or `readFamily` API. Font file registrations in a document
155
+ do not become network requests. Native builds retain the complete font-file
156
+ resolver, shaping and outline engine. The browser retains the embedded KaTeX
157
+ resources and the parser needed for mathematical outlines.
158
+
159
+ ## Profile core document opening
160
+
161
+ Build separate timing and allocation modules from the repository root. Run Cargo
162
+ inside `rust/` so rustup selects the pinned toolchain. Use wasm-bindgen CLI
163
+ 0.2.128 for both modules.
164
+
165
+ ```sh
166
+ (cd rust && cargo build --profile wasm -j 4 -p idocs-wasm --target wasm32-unknown-unknown --features perf-profile)
167
+ wasm-bindgen rust/target/wasm32-unknown-unknown/wasm/idocs_wasm.wasm --target web --out-dir .audit/core-profile/timing --out-name idocs_wasm
168
+ (cd rust && cargo build --profile wasm -j 4 -p idocs-wasm --target wasm32-unknown-unknown --features perf-alloc)
169
+ wasm-bindgen rust/target/wasm32-unknown-unknown/wasm/idocs_wasm.wasm --target web --out-dir .audit/core-profile/alloc --out-name idocs_wasm
170
+ ```
171
+
172
+ Start Vite in another terminal with `cd web && npx vite --host 127.0.0.1 --port
173
+ 5174 --strictPort`. Install Playwright and its Chromium browser in your tooling
174
+ environment. If this script cannot resolve `playwright`, set `PLAYWRIGHT_MODULE`
175
+ to its entry module path.
176
+
177
+ ```sh
178
+ node web/scripts/profile-core.mjs path/to/sample.docx path/to/source.idoc
179
+ ```
180
+
181
+ The script uses a blank page without Vite HMR and a fresh, isolated Worker for
182
+ each run. Each Worker uses the real browser Canvas measurements and enables
183
+ `configureProfile`. Runs are sequential and alternate mode order across at least
184
+ five rounds. Stop other builds and benchmarks before measuring.
185
+
186
+ Input paths and `OUT` resolve relative to the repository root. Configure
187
+ `BASE_URL`, `ROUNDS`, `TIMEOUT_MS`, and `OUT` through environment variables.
188
+ `PROFILE_DIRS` is a JSON object mapping mode names to module directories, such as
189
+ `{"before":".audit/before","after":".audit/after"}`. Vite must allow access to
190
+ these files and inputs through `/@fs`. The default output is
191
+ `.audit/core-profile/results.json`. Each row includes the complete profile,
192
+ elapsed milliseconds, a combined SHA-256 of metadata, glyph buffers, and images,
193
+ page and conversion loss counts,
194
+ diagnostic counts, output bytes, and WASM memory capacity.
195
+
196
+ The elapsed timer covers `openDocument`, including Rust output serialization.
197
+ It excludes module initialization, input fetch, profile retrieval, JavaScript
198
+ JSON parsing, and hashing. Allocation instrumentation adds overhead. Compare
199
+ timings with `perf-profile` builds and allocation counts with `perf-alloc` builds.
200
+ Live allocation bytes measure requested bytes. WASM memory capacity measures
201
+ linear memory reserved by the instance. Neither measures physical memory.
202
+ The allocation counters require a single-threaded, isolated Worker. Normal
203
+ builds leave both profiling features off.
204
+
205
+ ## License
206
+
207
+ MIT
@@ -0,0 +1,180 @@
1
+ /** `textbox`: a floating text box's text (#552), whose lines are not in the hit-testing table. */
2
+ export type Story = 'body' | 'header' | 'footer' | 'footnotes' | 'textbox' | 'lineNumber';
3
+ export interface Rect {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export interface GlyphOutline {
10
+ faceId: string;
11
+ family: string;
12
+ glyphId: number;
13
+ d: string;
14
+ }
15
+ export interface GlyphShape {
16
+ outline: number;
17
+ advance: number;
18
+ xOffset: number;
19
+ yOffset: number;
20
+ cluster: number;
21
+ }
22
+ export interface Glyph {
23
+ shapes?: GlyphShape[];
24
+ /** Display-only discretionary mark; does not consume source text. */
25
+ generated?: 'automatic' | 'manual';
26
+ /** Left edge from the metric profile. */
27
+ x: number;
28
+ /** Width occupied in the line, including added spacing. */
29
+ width: number;
30
+ text: string;
31
+ /** UTF-16 offset of the source character within its run's text. */
32
+ offset: number;
33
+ /**
34
+ * No metric profile, stand-in font or local font measured this character:
35
+ * its width is an estimate, and the painter draws a placeholder box.
36
+ */
37
+ unmeasured?: true;
38
+ }
39
+ export interface GlyphRun {
40
+ kind: 'glyphs';
41
+ /** `leader` for tab leader characters, which are not part of the line text. */
42
+ runKind: 'text' | 'leader';
43
+ story: Story;
44
+ path: string;
45
+ role: string;
46
+ family: string;
47
+ declaredFamily: string;
48
+ size: number;
49
+ bold: boolean;
50
+ italic: boolean;
51
+ /** Horizontal scale of each glyph about its left edge: the character scale times a compressing fit-width group's; absent when 1. */
52
+ scaleX?: number;
53
+ color: string;
54
+ baseline: number;
55
+ lineTop: number;
56
+ lineHeight: number;
57
+ /** Read-only in opened previews; indexed glyph objects are snapshots. */
58
+ glyphs: readonly Glyph[];
59
+ syntheticSlant?: number;
60
+ }
61
+ export type RectRole = 'areaFill' | 'border' | 'shading' | 'characterBorder' | 'underline' | 'strike' | 'footnoteSeparator' | 'mathRule' | 'pageBorder';
62
+ export interface RectItem extends Rect {
63
+ kind: 'rect';
64
+ role: RectRole;
65
+ color: string;
66
+ }
67
+ export interface LineItem {
68
+ kind: 'line';
69
+ role: 'diagonal';
70
+ x1: number;
71
+ y1: number;
72
+ x2: number;
73
+ y2: number;
74
+ width: number;
75
+ color: string;
76
+ }
77
+ export interface ImageItem extends Rect {
78
+ kind: 'image';
79
+ /** Index into PreviewDocument.images. */
80
+ image: number;
81
+ path: string;
82
+ }
83
+ export interface PathItem {
84
+ kind: 'path';
85
+ /** emphasisMark: an emphasis mark, `d` in em about its centre. */
86
+ role: 'mathGlyph' | 'mathPath' | 'emphasisMark';
87
+ d: string;
88
+ transform: [number, number, number, number, number, number];
89
+ color: string;
90
+ clip?: Rect;
91
+ glyph?: {
92
+ file: string;
93
+ char: string;
94
+ size: number;
95
+ text?: {
96
+ browser?: {
97
+ family: string;
98
+ declaredFamily: string;
99
+ bold: boolean;
100
+ italic: boolean;
101
+ };
102
+ standIn: {
103
+ strokeEm?: number;
104
+ };
105
+ width: number;
106
+ height: number;
107
+ depth: number;
108
+ };
109
+ };
110
+ }
111
+ export interface ClipItem {
112
+ kind: 'clip';
113
+ rect: Rect;
114
+ items: DisplayItem[];
115
+ }
116
+ export interface TurnItem {
117
+ kind: 'turn';
118
+ x: number;
119
+ y: number;
120
+ items: DisplayItem[];
121
+ }
122
+ /** A floating object's geometry (#552): SVG path data in page coordinates, filled, stroked or both. */
123
+ export interface ShapeItem {
124
+ kind: 'shape';
125
+ path: string;
126
+ d: string;
127
+ fill?: string | {
128
+ width: number;
129
+ height: number;
130
+ foreground: string;
131
+ background: string;
132
+ rects: [number, number, number, number][];
133
+ };
134
+ /** Centred on the path; `dash` lengths in points, absent for a solid line. */
135
+ stroke?: {
136
+ width: number;
137
+ color: string;
138
+ dash?: number[];
139
+ };
140
+ }
141
+ /** Items mapped by `matrix` [a, b, c, d, e, f] (SVG `matrix`): a floating object's rotation and flips (#552). */
142
+ export interface TransformItem {
143
+ kind: 'transform';
144
+ matrix: [number, number, number, number, number, number];
145
+ items: DisplayItem[];
146
+ }
147
+ export type DisplayItem = GlyphRun | RectItem | LineItem | ImageItem | PathItem | ShapeItem | ClipItem | TurnItem | TransformItem;
148
+ export interface LineGlyph {
149
+ x: number;
150
+ /** UTF-16 offset within the line's text. */
151
+ offset: number;
152
+ }
153
+ /** One laid-out line's glyph table, for hit testing. */
154
+ export interface TextLine {
155
+ story: Story;
156
+ path: string;
157
+ index: number;
158
+ x: number;
159
+ width: number;
160
+ top: number;
161
+ height: number;
162
+ baseline: number;
163
+ /** Extent of the text above and below `baseline` (the line box less its leading). */
164
+ ascent: number;
165
+ descent: number;
166
+ /** UTF-16 offset of the line start in its story's text. */
167
+ offset: number;
168
+ text: string;
169
+ /** Read-only in opened previews; indexed glyph objects are snapshots. */
170
+ glyphs: readonly LineGlyph[];
171
+ turn?: [number, number];
172
+ }
173
+ export interface DisplayPage {
174
+ number: number;
175
+ label?: string;
176
+ width: number;
177
+ height: number;
178
+ items: DisplayItem[];
179
+ lines: TextLine[];
180
+ }
@@ -0,0 +1,4 @@
1
+ // Display list types (#442). The Rust definitions in
2
+ // rust/idocs-core/src/display/mod.rs are authoritative; coordinates are points,
3
+ // origin at the page's top-left corner, y down. Item order is drawing order.
4
+ export {};
@@ -0,0 +1,9 @@
1
+ import { type Measure } from './fonts.js';
2
+ import type { Endpoint } from './protocol.js';
3
+ export interface ServeOptions {
4
+ /** The wasm module bytes or address; defaults to the file next to the glue. */
5
+ wasm?: BufferSource | URL | string;
6
+ /** Browser font metrics; defaults to OffscreenCanvas. null disables measurement and causes open to fail. */
7
+ measure?: Measure | null;
8
+ }
9
+ export declare function serve(endpoint: Endpoint, options?: ServeOptions): void;
@@ -0,0 +1,81 @@
1
+ // The Worker side: owns the wasm instance and answers the protocol in
2
+ // protocol.ts. `serve` takes any message endpoint, so Node tests run the same
3
+ // code behind a MessageChannel. Layout is synchronous inside one wasm call;
4
+ // resources the host has not answered yet come back as `needs`, and the open
5
+ // is retried once they arrive.
6
+ import init, { openDocument } from '../../wasm/idocs_wasm.js';
7
+ import { canvasMeasure } from './fonts.js';
8
+ export function serve(endpoint, options = {}) {
9
+ let ready;
10
+ const start = (wasm = options.wasm) => (ready ??= init(wasm === undefined ? undefined : { module_or_path: wasm }));
11
+ const measure = options.measure === undefined ? canvasMeasure() : (options.measure ?? undefined);
12
+ const pending = new Map();
13
+ const post = (message, transfer = []) => endpoint.postMessage(message, transfer);
14
+ const attempt = (id) => {
15
+ const job = pending.get(id);
16
+ if (!job)
17
+ return;
18
+ let outcome;
19
+ try {
20
+ if (!measure)
21
+ throw new Error('Browser font measurement requires OffscreenCanvas 2D');
22
+ if (job.progressive)
23
+ post({ type: 'pageReset', id });
24
+ const onPage = job.progressive ? (packet) => {
25
+ post({ type: 'page', id, ...packet }, [packet.data.buffer, ...packet.images.map(bytes => bytes.buffer)]);
26
+ } : undefined;
27
+ outcome = openDocument(job.kind, job.bytes, job.name, job.assets, measure, onPage);
28
+ }
29
+ catch (error) {
30
+ pending.delete(id);
31
+ post({ type: 'failed', id, error: { code: 'internal-error', message: String(error), diagnostics: [] } });
32
+ return;
33
+ }
34
+ if (outcome.status === 'needs') {
35
+ post({ type: 'needs', id, needs: JSON.parse(outcome.json) });
36
+ return;
37
+ }
38
+ pending.delete(id);
39
+ if (outcome.status === 'failed') {
40
+ post({ type: 'failed', id, error: JSON.parse(outcome.json) });
41
+ return;
42
+ }
43
+ post({ type: 'opened', id, json: outcome.json, images: outcome.images, data: outcome.data }, [outcome.data.buffer, ...outcome.images.map((bytes) => bytes.buffer)]);
44
+ };
45
+ endpoint.addEventListener('message', async (event) => {
46
+ const message = event.data;
47
+ if (message.type === 'init') {
48
+ // Start fetching early; a failure is reported to the first open.
49
+ start(message.wasmUrl).catch(() => { });
50
+ return;
51
+ }
52
+ try {
53
+ await start();
54
+ }
55
+ catch (error) {
56
+ const id = 'id' in message ? message.id : -1;
57
+ pending.delete(id);
58
+ post({ type: 'failed', id, error: { code: 'wasm-unavailable', message: String(error), diagnostics: [] } });
59
+ return;
60
+ }
61
+ if (message.type === 'open') {
62
+ pending.set(message.id, {
63
+ kind: message.kind,
64
+ progressive: message.progressive ?? false,
65
+ name: message.name,
66
+ bytes: message.bytes,
67
+ assets: new Map(),
68
+ });
69
+ }
70
+ else if (message.type === 'resources') {
71
+ const job = pending.get(message.id);
72
+ if (!job)
73
+ return;
74
+ for (const { need, bytes } of message.answers) {
75
+ job.assets.set(need.name, bytes);
76
+ }
77
+ }
78
+ attempt(message.id);
79
+ });
80
+ endpoint.start?.();
81
+ }
@@ -0,0 +1,27 @@
1
+ import type { GlyphRun } from './display.js';
2
+ export declare const CANVAS_TEXT_LANGUAGE = "und";
3
+ export type FontChainInput = Pick<GlyphRun, 'family' | 'declaredFamily' | 'role'>;
4
+ /**
5
+ * The CSS font families to draw `run` with, most preferred first; the last is
6
+ * always a generic family. When layout measured the declared family itself,
7
+ * it leads, then the look-alikes of the Word family it names. When the
8
+ * selected family differs from the declared family, layout measured a fallback face:
9
+ * that face and its look-alikes lead, so glyphs are drawn as wide as they
10
+ * were placed, and the declared family only covers characters they lack.
11
+ */
12
+ export declare function fontChain(run: FontChainInput): string[];
13
+ export type CanvasFontInput = FontChainInput & Pick<GlyphRun, 'size' | 'bold' | 'italic'>;
14
+ /**
15
+ * The `CanvasRenderingContext2D.font` for `run`, in a context whose unit is
16
+ * one point: the run's size in points, its weight and slant, then the chain.
17
+ */
18
+ export declare function canvasFont(run: CanvasFontInput): string;
19
+ /** Measurements use the same CSS chain and kerning policy as the Canvas painter. */
20
+ export interface FontMeasurement {
21
+ width: number;
22
+ ascent: number;
23
+ descent: number;
24
+ }
25
+ export type Measure = (family: string, declaredFamily: string, bold: boolean, italic: boolean, text: string, kern?: boolean) => number | FontMeasurement;
26
+ /** Browser-local fonts only: this neither fetches files nor requests Local Font Access. */
27
+ export declare function canvasMeasure(): Measure | undefined;