@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.
- package/CHANGELOG.md +50 -0
- package/LICENSE +21 -0
- package/README.md +206 -2
- package/dist/core/display.d.ts +180 -0
- package/dist/core/display.js +4 -0
- package/dist/core/engine.d.ts +9 -0
- package/dist/core/engine.js +81 -0
- package/dist/core/fonts.d.ts +27 -0
- package/dist/core/fonts.js +135 -0
- package/dist/core/index.d.ts +47 -0
- package/dist/core/index.js +156 -0
- package/dist/core/protocol.d.ts +137 -0
- package/dist/core/protocol.js +1 -0
- package/dist/core/wire.d.ts +14 -0
- package/dist/core/wire.js +100 -0
- package/dist/core/worker.d.ts +1 -0
- package/dist/core/worker.js +5 -0
- package/dist/view/find-bar.d.ts +26 -0
- package/dist/view/find-bar.js +117 -0
- package/dist/view/find.d.ts +46 -0
- package/dist/view/find.js +121 -0
- package/dist/view/index.d.ts +27 -0
- package/dist/view/index.js +274 -0
- package/dist/view/selection-layer.d.ts +27 -0
- package/dist/view/selection-layer.js +269 -0
- package/dist/view/selection.d.ts +131 -0
- package/dist/view/selection.js +350 -0
- package/dist/view/shell.d.ts +77 -0
- package/dist/view/shell.js +452 -0
- package/dist/view/viewport.d.ts +100 -0
- package/dist/view/viewport.js +145 -0
- package/package.json +49 -4
- package/wasm/hyphenation-licenses/LICENSE-APACHE +201 -0
- package/wasm/hyphenation-licenses/LICENSE-MIT +21 -0
- package/wasm/hyphenation-licenses/README.md +26 -0
- package/wasm/hyphenation-licenses/en-gb.tex +8615 -0
- package/wasm/hyphenation-licenses/en-us.tex +5018 -0
- package/wasm/hyphenation-licenses/ushyphex.tex +1454 -0
- package/wasm/idocs_wasm.d.ts +45 -0
- package/wasm/idocs_wasm.js +405 -0
- package/wasm/idocs_wasm_bg.wasm +0 -0
- package/wasm/idocs_wasm_bg.wasm.d.ts +10 -0
- 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
|
-
#
|
|
1
|
+
# iDocs preview
|
|
2
2
|
|
|
3
|
-
|
|
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,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;
|