@aseity/idocs-preview 0.1.0 → 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 +34 -0
- package/README.md +111 -14
- package/dist/core/display.d.ts +68 -6
- package/dist/core/engine.d.ts +1 -5
- package/dist/core/engine.js +12 -12
- package/dist/core/fonts.d.ts +10 -11
- package/dist/core/fonts.js +28 -14
- package/dist/core/index.d.ts +3 -8
- package/dist/core/index.js +41 -4
- package/dist/core/protocol.d.ts +37 -6
- package/dist/core/wire.d.ts +14 -0
- package/dist/core/wire.js +100 -0
- package/dist/view/find.js +1 -1
- package/dist/view/index.d.ts +7 -4
- package/dist/view/index.js +156 -25
- package/dist/view/selection.js +8 -7
- package/dist/view/shell.d.ts +2 -0
- package/dist/view/shell.js +53 -3
- package/package.json +3 -3
- 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 +4 -4
- package/wasm/idocs_wasm.js +15 -7
- package/wasm/idocs_wasm_bg.wasm +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @aseity/idocs-preview
|
|
2
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
|
+
|
|
3
37
|
## 0.1.0
|
|
4
38
|
|
|
5
39
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
8
|
|
|
9
9
|
The package ships ES modules, TypeScript declarations, the worker and the WASM
|
|
10
10
|
binary. It has no framework dependency and runs only in browsers.
|
|
@@ -13,6 +13,19 @@ binary. It has no framework dependency and runs only in browsers.
|
|
|
13
13
|
npm install @aseity/idocs-preview
|
|
14
14
|
```
|
|
15
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
|
+
|
|
16
29
|
## Quick start
|
|
17
30
|
|
|
18
31
|
```ts
|
|
@@ -57,9 +70,34 @@ so the caller can keep using them, for example to download the original.
|
|
|
57
70
|
(`docx-not-zip`, `docx-encrypted`, `docx-legacy-doc`, `docx-invalid-package`,
|
|
58
71
|
`docx-strict-ooxml`).
|
|
59
72
|
|
|
60
|
-
DOCX conversion is lossy:
|
|
61
|
-
floating
|
|
62
|
-
in `conversionLosses`.
|
|
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.
|
|
63
101
|
|
|
64
102
|
## View
|
|
65
103
|
|
|
@@ -91,19 +129,78 @@ addresses: `createPreview(el, { session: { workerUrl, wasmUrl } })`, where
|
|
|
91
129
|
`workerUrl` points to `dist/core/worker.js` and `wasmUrl` to
|
|
92
130
|
`wasm/idocs_wasm_bg.wasm`.
|
|
93
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
|
+
|
|
94
137
|
The worker is a same-origin module worker. Serve the WASM file as
|
|
95
138
|
`application/wasm`. A Content Security Policy needs `script-src
|
|
96
139
|
'wasm-unsafe-eval'` and `worker-src 'self'`.
|
|
97
140
|
|
|
98
141
|
## Fonts
|
|
99
142
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
107
204
|
|
|
108
205
|
## License
|
|
109
206
|
|
package/dist/core/display.d.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
|
|
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';
|
|
2
3
|
export interface Rect {
|
|
3
4
|
x: number;
|
|
4
5
|
y: number;
|
|
5
6
|
width: number;
|
|
6
7
|
height: number;
|
|
7
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
|
+
}
|
|
8
22
|
export interface Glyph {
|
|
23
|
+
shapes?: GlyphShape[];
|
|
24
|
+
/** Display-only discretionary mark; does not consume source text. */
|
|
25
|
+
generated?: 'automatic' | 'manual';
|
|
9
26
|
/** Left edge from the metric profile. */
|
|
10
27
|
x: number;
|
|
11
28
|
/** Width occupied in the line, including added spacing. */
|
|
@@ -31,13 +48,17 @@ export interface GlyphRun {
|
|
|
31
48
|
size: number;
|
|
32
49
|
bold: boolean;
|
|
33
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;
|
|
34
53
|
color: string;
|
|
35
54
|
baseline: number;
|
|
36
55
|
lineTop: number;
|
|
37
56
|
lineHeight: number;
|
|
38
|
-
|
|
57
|
+
/** Read-only in opened previews; indexed glyph objects are snapshots. */
|
|
58
|
+
glyphs: readonly Glyph[];
|
|
59
|
+
syntheticSlant?: number;
|
|
39
60
|
}
|
|
40
|
-
export type RectRole = 'areaFill' | 'border' | 'shading' | 'characterBorder' | 'underline' | 'strike' | 'footnoteSeparator' | 'mathRule';
|
|
61
|
+
export type RectRole = 'areaFill' | 'border' | 'shading' | 'characterBorder' | 'underline' | 'strike' | 'footnoteSeparator' | 'mathRule' | 'pageBorder';
|
|
41
62
|
export interface RectItem extends Rect {
|
|
42
63
|
kind: 'rect';
|
|
43
64
|
role: RectRole;
|
|
@@ -61,7 +82,8 @@ export interface ImageItem extends Rect {
|
|
|
61
82
|
}
|
|
62
83
|
export interface PathItem {
|
|
63
84
|
kind: 'path';
|
|
64
|
-
|
|
85
|
+
/** emphasisMark: an emphasis mark, `d` in em about its centre. */
|
|
86
|
+
role: 'mathGlyph' | 'mathPath' | 'emphasisMark';
|
|
65
87
|
d: string;
|
|
66
88
|
transform: [number, number, number, number, number, number];
|
|
67
89
|
color: string;
|
|
@@ -70,6 +92,20 @@ export interface PathItem {
|
|
|
70
92
|
file: string;
|
|
71
93
|
char: string;
|
|
72
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
|
+
};
|
|
73
109
|
};
|
|
74
110
|
}
|
|
75
111
|
export interface ClipItem {
|
|
@@ -83,7 +119,32 @@ export interface TurnItem {
|
|
|
83
119
|
y: number;
|
|
84
120
|
items: DisplayItem[];
|
|
85
121
|
}
|
|
86
|
-
|
|
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;
|
|
87
148
|
export interface LineGlyph {
|
|
88
149
|
x: number;
|
|
89
150
|
/** UTF-16 offset within the line's text. */
|
|
@@ -105,7 +166,8 @@ export interface TextLine {
|
|
|
105
166
|
/** UTF-16 offset of the line start in its story's text. */
|
|
106
167
|
offset: number;
|
|
107
168
|
text: string;
|
|
108
|
-
|
|
169
|
+
/** Read-only in opened previews; indexed glyph objects are snapshots. */
|
|
170
|
+
glyphs: readonly LineGlyph[];
|
|
109
171
|
turn?: [number, number];
|
|
110
172
|
}
|
|
111
173
|
export interface DisplayPage {
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -3,11 +3,7 @@ import type { Endpoint } from './protocol.js';
|
|
|
3
3
|
export interface ServeOptions {
|
|
4
4
|
/** The wasm module bytes or address; defaults to the file next to the glue. */
|
|
5
5
|
wasm?: BufferSource | URL | string;
|
|
6
|
-
/**
|
|
7
|
-
* Measures characters that no metric profile or stand-in font has. Defaults
|
|
8
|
-
* to OffscreenCanvas `measureText` along the painter's font chain; `null`
|
|
9
|
-
* (or no OffscreenCanvas) leaves them estimated and drawn as boxes.
|
|
10
|
-
*/
|
|
6
|
+
/** Browser font metrics; defaults to OffscreenCanvas. null disables measurement and causes open to fail. */
|
|
11
7
|
measure?: Measure | null;
|
|
12
8
|
}
|
|
13
9
|
export declare function serve(endpoint: Endpoint, options?: ServeOptions): void;
|
package/dist/core/engine.js
CHANGED
|
@@ -8,10 +8,8 @@ import { canvasMeasure } from './fonts.js';
|
|
|
8
8
|
export function serve(endpoint, options = {}) {
|
|
9
9
|
let ready;
|
|
10
10
|
const start = (wasm = options.wasm) => (ready ??= init(wasm === undefined ? undefined : { module_or_path: wasm }));
|
|
11
|
-
// Stand-in fonts do not depend on the document, so answers outlive an open.
|
|
12
|
-
const fonts = new Map();
|
|
13
|
-
const pending = new Map();
|
|
14
11
|
const measure = options.measure === undefined ? canvasMeasure() : (options.measure ?? undefined);
|
|
12
|
+
const pending = new Map();
|
|
15
13
|
const post = (message, transfer = []) => endpoint.postMessage(message, transfer);
|
|
16
14
|
const attempt = (id) => {
|
|
17
15
|
const job = pending.get(id);
|
|
@@ -19,7 +17,14 @@ export function serve(endpoint, options = {}) {
|
|
|
19
17
|
return;
|
|
20
18
|
let outcome;
|
|
21
19
|
try {
|
|
22
|
-
|
|
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);
|
|
23
28
|
}
|
|
24
29
|
catch (error) {
|
|
25
30
|
pending.delete(id);
|
|
@@ -35,12 +40,7 @@ export function serve(endpoint, options = {}) {
|
|
|
35
40
|
post({ type: 'failed', id, error: JSON.parse(outcome.json) });
|
|
36
41
|
return;
|
|
37
42
|
}
|
|
38
|
-
|
|
39
|
-
const document = {
|
|
40
|
-
...parsed,
|
|
41
|
-
images: parsed.images.map((image, i) => ({ ...image, bytes: outcome.images[i] })),
|
|
42
|
-
};
|
|
43
|
-
post({ type: 'opened', id, document }, outcome.images.map((bytes) => bytes.buffer));
|
|
43
|
+
post({ type: 'opened', id, json: outcome.json, images: outcome.images, data: outcome.data }, [outcome.data.buffer, ...outcome.images.map((bytes) => bytes.buffer)]);
|
|
44
44
|
};
|
|
45
45
|
endpoint.addEventListener('message', async (event) => {
|
|
46
46
|
const message = event.data;
|
|
@@ -61,6 +61,7 @@ export function serve(endpoint, options = {}) {
|
|
|
61
61
|
if (message.type === 'open') {
|
|
62
62
|
pending.set(message.id, {
|
|
63
63
|
kind: message.kind,
|
|
64
|
+
progressive: message.progressive ?? false,
|
|
64
65
|
name: message.name,
|
|
65
66
|
bytes: message.bytes,
|
|
66
67
|
assets: new Map(),
|
|
@@ -71,8 +72,7 @@ export function serve(endpoint, options = {}) {
|
|
|
71
72
|
if (!job)
|
|
72
73
|
return;
|
|
73
74
|
for (const { need, bytes } of message.answers) {
|
|
74
|
-
;
|
|
75
|
-
(need.kind === 'font' ? fonts : job.assets).set(need.name, bytes);
|
|
75
|
+
job.assets.set(need.name, bytes);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
attempt(message.id);
|
package/dist/core/fonts.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { GlyphRun } from './display.js';
|
|
2
|
+
export declare const CANVAS_TEXT_LANGUAGE = "und";
|
|
2
3
|
export type FontChainInput = Pick<GlyphRun, 'family' | 'declaredFamily' | 'role'>;
|
|
3
4
|
/**
|
|
4
5
|
* The CSS font families to draw `run` with, most preferred first; the last is
|
|
5
6
|
* always a generic family. When layout measured the declared family itself,
|
|
6
7
|
* it leads, then the look-alikes of the Word family it names. When the
|
|
7
|
-
*
|
|
8
|
+
* selected family differs from the declared family, layout measured a fallback face:
|
|
8
9
|
* that face and its look-alikes lead, so glyphs are drawn as wide as they
|
|
9
10
|
* were placed, and the declared family only covers characters they lack.
|
|
10
11
|
*/
|
|
@@ -15,14 +16,12 @@ export type CanvasFontInput = FontChainInput & Pick<GlyphRun, 'size' | 'bold' |
|
|
|
15
16
|
* one point: the run's size in points, its weight and slant, then the chain.
|
|
16
17
|
*/
|
|
17
18
|
export declare function canvasFont(run: CanvasFontInput): string;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* painter draws with; undefined where there is no OffscreenCanvas (Node).
|
|
27
|
-
*/
|
|
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. */
|
|
28
27
|
export declare function canvasMeasure(): Measure | undefined;
|
package/dist/core/fonts.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
export const CANVAS_TEXT_LANGUAGE = 'und';
|
|
1
2
|
const GENERIC = new Set(['serif', 'sans-serif', 'monospace']);
|
|
2
3
|
/**
|
|
3
4
|
* Word family → its look-alikes on Windows, macOS and Linux, ending in a
|
|
4
|
-
* generic family. Covers the default theme and
|
|
5
|
-
* (assets/font-metrics), plus the uncalibrated Word families the resolver
|
|
6
|
-
* knows (rust/idocs-core/src/fonts/resolver.rs `word_family`).
|
|
5
|
+
* generic family. Covers the default theme and common Word families.
|
|
7
6
|
*/
|
|
8
7
|
const SIMILAR = {
|
|
8
|
+
MiSans: ['MiSans', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', 'sans-serif'],
|
|
9
|
+
'Noto Sans CJK SC': ['Noto Sans CJK SC', 'Microsoft YaHei', 'PingFang SC', 'sans-serif'],
|
|
10
|
+
'Noto Serif CJK SC': ['Noto Serif CJK SC', 'SimSun', 'Songti SC', 'serif'],
|
|
9
11
|
// Simplified Chinese
|
|
10
12
|
SimSun: ['SimSun', 'Songti SC', 'Noto Serif CJK SC', 'serif'],
|
|
11
13
|
SimHei: ['SimHei', 'Heiti SC', 'STHeiti', 'Noto Sans CJK SC', 'sans-serif'],
|
|
@@ -17,6 +19,8 @@ const SIMILAR = {
|
|
|
17
19
|
PMingLiU: ['PMingLiU', 'Songti TC', 'Noto Serif CJK TC', 'serif'],
|
|
18
20
|
MingLiU: ['MingLiU', 'Songti TC', 'Noto Serif CJK TC', 'serif'],
|
|
19
21
|
'Microsoft JhengHei': ['Microsoft JhengHei', 'PingFang TC', 'Noto Sans CJK TC', 'sans-serif'],
|
|
22
|
+
// Japanese
|
|
23
|
+
'MS Gothic': ['MS Gothic', 'Hiragino Kaku Gothic ProN', 'Noto Sans CJK JP', 'Noto Sans CJK SC', 'sans-serif'],
|
|
20
24
|
// Latin
|
|
21
25
|
'Times New Roman': ['Times New Roman', 'Times', 'Liberation Serif', 'Tinos', 'serif'],
|
|
22
26
|
Arial: ['Arial', 'Helvetica', 'Liberation Sans', 'Arimo', 'sans-serif'],
|
|
@@ -39,6 +43,7 @@ const ALIASES = {
|
|
|
39
43
|
新細明體: 'PMingLiU',
|
|
40
44
|
微軟正黑體: 'Microsoft JhengHei',
|
|
41
45
|
細明體: 'MingLiU',
|
|
46
|
+
'MS ゴシック': 'MS Gothic',
|
|
42
47
|
};
|
|
43
48
|
const BY_LOWER = new Map(Object.keys(SIMILAR).map((name) => [name.toLowerCase(), name]));
|
|
44
49
|
/** The Word family a declared name stands for, matched as Word does (case-insensitive). */
|
|
@@ -56,7 +61,7 @@ function roleGeneric(role) {
|
|
|
56
61
|
* The CSS font families to draw `run` with, most preferred first; the last is
|
|
57
62
|
* always a generic family. When layout measured the declared family itself,
|
|
58
63
|
* it leads, then the look-alikes of the Word family it names. When the
|
|
59
|
-
*
|
|
64
|
+
* selected family differs from the declared family, layout measured a fallback face:
|
|
60
65
|
* that face and its look-alikes lead, so glyphs are drawn as wide as they
|
|
61
66
|
* were placed, and the declared family only covers characters they lack.
|
|
62
67
|
*/
|
|
@@ -101,21 +106,30 @@ export function canvasFont(run) {
|
|
|
101
106
|
}
|
|
102
107
|
return font;
|
|
103
108
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
* painter draws with; undefined where there is no OffscreenCanvas (Node).
|
|
109
|
-
*/
|
|
109
|
+
// Canvas rounds font box extents to pixels; retain sub-point precision
|
|
110
|
+
// when raised text sits near a line-grid boundary.
|
|
111
|
+
const MEASURE_SIZE = 1000;
|
|
112
|
+
/** Browser-local fonts only: this neither fetches files nor requests Local Font Access. */
|
|
110
113
|
export function canvasMeasure() {
|
|
111
114
|
if (typeof OffscreenCanvas === 'undefined')
|
|
112
115
|
return undefined;
|
|
113
116
|
const ctx = new OffscreenCanvas(1, 1).getContext('2d');
|
|
114
117
|
if (!ctx)
|
|
115
118
|
return undefined;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
Object.assign(ctx, { lang: CANVAS_TEXT_LANGUAGE });
|
|
120
|
+
const cache = new Map();
|
|
121
|
+
return (family, declaredFamily, bold, italic, text, kern = false) => {
|
|
122
|
+
const role = family === 'sans-serif' ? 'sans' : family === 'monospace' ? 'mono' : 'serif';
|
|
123
|
+
const font = canvasFont({ family, declaredFamily, role, size: MEASURE_SIZE, bold, italic });
|
|
124
|
+
const key = `${font}\0${kern}\0${text}`;
|
|
125
|
+
const known = cache.get(key);
|
|
126
|
+
if (known)
|
|
127
|
+
return known;
|
|
128
|
+
ctx.font = font;
|
|
129
|
+
ctx.fontKerning = kern ? 'normal' : 'none';
|
|
130
|
+
const m = ctx.measureText(text);
|
|
131
|
+
const result = { width: m.width / MEASURE_SIZE, ascent: (m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent) / MEASURE_SIZE, descent: (m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent) / MEASURE_SIZE };
|
|
132
|
+
cache.set(key, result);
|
|
133
|
+
return result;
|
|
120
134
|
};
|
|
121
135
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DocumentKind, Endpoint, PreviewDocument } from './protocol.js';
|
|
1
|
+
import type { DocumentKind, Endpoint, PreviewDocument, PreviewPage } from './protocol.js';
|
|
2
2
|
export type * from './display.js';
|
|
3
3
|
export type * from './protocol.js';
|
|
4
4
|
export interface SessionOptions {
|
|
@@ -8,17 +8,12 @@ export interface SessionOptions {
|
|
|
8
8
|
wasmUrl?: string | URL;
|
|
9
9
|
/** An already created worker (a MessagePort works too); `workerUrl` is then ignored. */
|
|
10
10
|
worker?: Endpoint;
|
|
11
|
-
/**
|
|
12
|
-
* Reads a stand-in font by basename (e.g. `LiberationSerif-Regular.ttf`).
|
|
13
|
-
* Layout uses stand-in metrics for characters the embedded profiles lack;
|
|
14
|
-
* without them those characters fall back, as the CLI does with no font
|
|
15
|
-
* directory. Fonts belong to the session: each file is read at most once.
|
|
16
|
-
*/
|
|
17
|
-
readFont?: Read;
|
|
18
11
|
}
|
|
19
12
|
type Bytes = Uint8Array | ArrayBuffer;
|
|
20
13
|
export type Read = (name: string) => Bytes | null | undefined | Promise<Bytes | null | undefined>;
|
|
21
14
|
export interface OpenOptions {
|
|
15
|
+
/** Receive provisional pages while the worker builds the remaining display list. */
|
|
16
|
+
onPage?: (page: PreviewPage) => void;
|
|
22
17
|
/** File name used in diagnostics. */
|
|
23
18
|
name?: string;
|
|
24
19
|
/**
|
package/dist/core/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeCompact } from './wire.js';
|
|
1
2
|
export class PreviewError extends Error {
|
|
2
3
|
code;
|
|
3
4
|
diagnostics;
|
|
@@ -51,10 +52,33 @@ export function createSession(options = {}) {
|
|
|
51
52
|
const job = jobs.get(message.id);
|
|
52
53
|
if (!job)
|
|
53
54
|
return;
|
|
55
|
+
if (message.type === 'pageReset') {
|
|
56
|
+
job.pages = [];
|
|
57
|
+
job.images = [];
|
|
58
|
+
job.previewImages = [];
|
|
59
|
+
job.outlines = [];
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (message.type === 'page') {
|
|
63
|
+
try {
|
|
64
|
+
job.images.push(...message.images);
|
|
65
|
+
const partial = decodeCompact(message.json, message.data, message.images);
|
|
66
|
+
job.previewImages.push(...partial.images);
|
|
67
|
+
job.outlines.push(...partial.glyphOutlines ?? []);
|
|
68
|
+
const page = partial.pages[0];
|
|
69
|
+
job.pages.push(page);
|
|
70
|
+
job.options.onPage?.({ page, images: job.previewImages, glyphOutlines: job.outlines, pageCount: partial.pageCount });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
jobs.delete(message.id);
|
|
74
|
+
job.reject(new PreviewError({ code: 'internal-error', message: String(error), diagnostics: [] }));
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
54
78
|
if (message.type === 'needs') {
|
|
55
79
|
const answers = await Promise.all(message.needs.map(async (need) => ({
|
|
56
80
|
need,
|
|
57
|
-
bytes: await answer(
|
|
81
|
+
bytes: await answer(job.options.readResource, need.name),
|
|
58
82
|
})));
|
|
59
83
|
if (disposed)
|
|
60
84
|
return;
|
|
@@ -62,8 +86,20 @@ export function createSession(options = {}) {
|
|
|
62
86
|
return;
|
|
63
87
|
}
|
|
64
88
|
jobs.delete(message.id);
|
|
65
|
-
if (message.type === 'opened')
|
|
66
|
-
|
|
89
|
+
if (message.type === 'opened') {
|
|
90
|
+
try {
|
|
91
|
+
const doc = decodeCompact(message.json, message.data, job.options.onPage ? job.images : message.images);
|
|
92
|
+
if (job.options.onPage) {
|
|
93
|
+
if (job.pages.length !== doc.pageCount)
|
|
94
|
+
throw new Error('Preview page count mismatch');
|
|
95
|
+
doc.pages = job.pages;
|
|
96
|
+
}
|
|
97
|
+
job.resolve(doc);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
job.reject(new PreviewError({ code: 'internal-error', message: String(error), diagnostics: [] }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
67
103
|
else
|
|
68
104
|
job.reject(new PreviewError(message.error));
|
|
69
105
|
});
|
|
@@ -96,9 +132,10 @@ export function createSession(options = {}) {
|
|
|
96
132
|
const bytes = copyBytes(input);
|
|
97
133
|
const id = next++;
|
|
98
134
|
return new Promise((resolve, reject) => {
|
|
99
|
-
jobs.set(id, { options: openOptions, resolve, reject });
|
|
135
|
+
jobs.set(id, { options: openOptions, resolve, reject, pages: [], images: [], previewImages: [], outlines: [] });
|
|
100
136
|
post({
|
|
101
137
|
type: 'open',
|
|
138
|
+
progressive: openOptions.onPage !== undefined,
|
|
102
139
|
id,
|
|
103
140
|
kind: openOptions.kind ?? (isWordFile(bytes, openOptions.name) ? 'docx' : 'idoc'),
|
|
104
141
|
name: openOptions.name ?? 'document.idoc',
|