@onodocs/canvas 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 (50) hide show
  1. package/LICENSE +10 -0
  2. package/LICENSING.md +33 -0
  3. package/README.md +229 -0
  4. package/SDK.md +229 -0
  5. package/index.js +2428 -0
  6. package/package.json +44 -0
  7. package/types/canvas/canvas-axis-aligned-transform.d.ts +12 -0
  8. package/types/canvas/canvas-effect-surface.d.ts +9 -0
  9. package/types/canvas/canvas-materializer.d.ts +9 -0
  10. package/types/canvas/canvas-native-text.d.ts +6 -0
  11. package/types/canvas/canvas-physical-clips.d.ts +4 -0
  12. package/types/canvas/canvas-positioned-text-replay.d.ts +10 -0
  13. package/types/canvas/canvas-raster-replay.d.ts +7 -0
  14. package/types/canvas/canvas-shadow-replay.d.ts +19 -0
  15. package/types/canvas/canvas-soft-edge-replay.d.ts +9 -0
  16. package/types/canvas/canvas-table-border-replay.d.ts +7 -0
  17. package/types/canvas/canvas-target-pixel-alignment.d.ts +9 -0
  18. package/types/canvas/canvas-vector-shape-replay.d.ts +4 -0
  19. package/types/canvas/evaluation-watermark.d.ts +3 -0
  20. package/types/canvas/index.d.ts +30 -0
  21. package/types/canvas/presentation.d.ts +54 -0
  22. package/types/canvas/text-selection.d.ts +7 -0
  23. package/types/canvas/transport.d.ts +5 -0
  24. package/types/foundation/bounded-number-representation-cjk.d.ts +17 -0
  25. package/types/foundation/bounded-number-representation-decimal-roman-latin.d.ts +6 -0
  26. package/types/foundation/bounded-number-representation-hebrew.d.ts +5 -0
  27. package/types/foundation/bounded-number-representation-korean.d.ts +15 -0
  28. package/types/foundation/bounded-number-representation-mechanics.d.ts +17 -0
  29. package/types/foundation/bounded-number-representation-writer-alphabets.d.ts +22 -0
  30. package/types/foundation/bounded-number-representation-writer-tables.d.ts +13 -0
  31. package/types/foundation/bounded-number-representation.d.ts +18 -0
  32. package/types/foundation/browser-text.d.ts +23 -0
  33. package/types/foundation/chart-number-format.d.ts +3 -0
  34. package/types/foundation/device-rounding.d.ts +3 -0
  35. package/types/foundation/font-resources.d.ts +14 -0
  36. package/types/foundation/geometry-path.d.ts +27 -0
  37. package/types/foundation/host-task.d.ts +3 -0
  38. package/types/foundation/image-resources.d.ts +39 -0
  39. package/types/foundation/index.d.ts +17 -0
  40. package/types/foundation/page-interaction.d.ts +90 -0
  41. package/types/foundation/path-bounds.d.ts +10 -0
  42. package/types/foundation/raster-color-effect.d.ts +55 -0
  43. package/types/foundation/raster-source-rectangle.d.ts +8 -0
  44. package/types/foundation/vector-shape-fill.d.ts +34 -0
  45. package/types/foundation/vector-shape-shadow.d.ts +17 -0
  46. package/types/foundation/vector-shape.d.ts +56 -0
  47. package/types/foundation/writer-unmapped-ooxml-number-format.d.ts +6 -0
  48. package/types/render-api/index.d.ts +4 -0
  49. package/types/render-api/page-display-list.d.ts +508 -0
  50. package/types/render-api/page-source.d.ts +23 -0
package/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ OnoDocs SDK
2
+ All rights reserved, except for the permissions below and separately licensed third-party components.
3
+
4
+ Free evaluation permits installation, development, testing, and internal demonstrations for the purpose of evaluating OnoDocs. It has no time limit. It does not permit production use, including internal production workflows or services supplied to others. Preserve evaluation notices and watermarks. A signed trial license permits watermark-free evaluation for 30 days from issuance; it does not grant production rights.
5
+
6
+ Production use requires a commercial license issued by OnoDocs. The named licensee may use and deploy the SDK within the scope recorded in that license and the associated purchase agreement. Covered SDK releases may be used perpetually. The updates-through date includes releases published on that UTC date; it is not a runtime expiry. Renewal grants access to subsequent releases and support under the purchase agreement. A license to an application does not grant rights to redistribute the SDK as a competing standalone product.
7
+
8
+ Do not use another customer's key or remove licensing checks or evaluation notices to obtain unlicensed production use. The same terms apply to low-level stage APIs, custom adapters, browser applications, and server rendering. A publicly downloadable package does not grant production rights.
9
+
10
+ The SDK is provided as is, without warranties, except as expressly agreed in a commercial agreement. Third-party components retain their respective licenses.
package/LICENSING.md ADDED
@@ -0,0 +1,33 @@
1
+ # Offline licensing
2
+
3
+ The OnoDocs SDK and Canvas packages support evaluation, trial and commercial use under the same terms. Packages can be installed from an npm registry or supplied archives and copied into an offline environment. Runtime licensing makes no network requests.
4
+
5
+ | Mode | Behavior |
6
+ | --- | --- |
7
+ | Evaluation | No key or deadline. Full features, evaluation watermark and developer notice. Non-production use only. |
8
+ | Trial | Signed key removes the watermark for 30 days. Evaluation only; expiry returns to evaluation mode. |
9
+ | Commercial | Signed key permits the purchased scope and removes evaluation notices. Covered releases work indefinitely. |
10
+ | Demo | OnoDocs website demonstrations only. Valid for at most 24 hours at one exact browser origin; not valid for backend use. |
11
+
12
+ Pass the issued token as `licenseKey` when opening a document:
13
+
14
+ ```ts
15
+ import { openDocument } from "@onodocs/sdk/browser";
16
+ import { createDocument } from "@onodocs/canvas";
17
+
18
+ const document = await openDocument(bytes, { licenseKey });
19
+ console.log(document.license.mode);
20
+ createDocument(document, { container });
21
+ ```
22
+
23
+ The default Node analysis entry point accepts the same option. With `@onodocs/sdk/server`, use `renderer.openDocument(bytes, { licenseKey })` and `await document.licenseStatus()`. Different documents may use different keys. Keys are visible in a browser application; they are not secrets or API credentials. Keep the signing private key private.
24
+
25
+ Missing or invalid keys preserve document functionality and return evaluation mode with a `reason`: `missing-key`, `invalid-key`, `verification-unavailable`, `not-yet-valid`, `trial-expired`, or `release-not-covered`. Trial expiry is checked on opening, status access and rendering. Already produced images remain unchanged. Browsers need HTTPS or a secure local context for native signature verification; no internet connection is needed. The headless renderer supplies its own isolated secure context.
26
+
27
+ Commercial keys contain an inclusive `updatesThrough` date. A key covering 2027-10-04 permits releases published on or before that UTC date forever. Renew to use later releases and receive further support. `sdkReleaseDate`, exported from each entry point, identifies the running release date. Scope is recorded in the key and purchase agreement; it is not inferred from a hostname or tracked online.
28
+
29
+ Canvas, mounted views, PNG, JPEG and PDF receive the same evaluation watermark. Queries, source models, layouts and display lists remain complete. Low-level stage APIs and custom adapters are covered by the same terms; custom evaluation presentations must preserve an evaluation notice. Offline JavaScript checks can be bypassed by changing code or the clock; they do not provide immediate revocation or seat counting.
30
+
31
+ Demo licenses also report `demo-expired` or `origin-not-covered`. The website handles early renewal; the SDK continues to verify locally without contacting a license server. They do not grant customer production rights.
32
+
33
+ The included `LICENSE` states evaluation and commercial usage permissions.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # OnoDocs SDK
2
+
3
+ `@onodocs/sdk` processes Word documents. Its default entry performs analysis in Node or a browser; `/browser` adds layout and immutable page publication using browser font services; `/server` runs that processing in headless Chromium. `@onodocs/canvas` paints prepared pages and provides the viewer, selection and DOM attachments. The Canvas package runs independently of the parser, semantic engine and layout engine.
4
+
5
+ Supported Word inputs are DOCX, DOCM, DOTX and DOTM. Macro-enabled files are rendered without executing VBA. Older binary DOC/DOT files and RTF are not supported.
6
+
7
+ Install with `npm install @onodocs/sdk @onodocs/canvas`. Use matching versions of both packages. A backend can install only the SDK; a frontend receiving prepared pages can install only Canvas. Both packages include TypeScript declarations and this guide. Archives are also available from the [public releases](https://github.com/onodocs/onodocs/releases). Server rendering requires an installed Chromium-family browser. The server adapter includes the Canvas runtime it uses for image and PDF export.
8
+
9
+ Without a key, the SDK runs in free, non-production evaluation mode with full features and a watermark on rendered output. Pass `{ licenseKey }` to `openDocument` for a 30-day trial or commercial entitlement; this works in all three entry points. Verification is entirely offline. Commercial keys permit covered releases indefinitely, with renewal for later releases and support. Browser verification requires HTTPS or a secure local context. See the included `LICENSING.md` and `LICENSE` for details.
10
+
11
+ ## Inspect and query
12
+
13
+ ```ts
14
+ import { openDocument } from "@onodocs/sdk";
15
+
16
+ const doc = await openDocument(bytes);
17
+ const customer = doc.query.contentControls().where({ tag: "customer-name" }).one();
18
+ const cells = doc.query.tables().cells().all();
19
+ const matches = doc.query.findText("Invoice total").all();
20
+ const headers = doc.query.stories().where({ story: "header" }).paragraphs().all();
21
+ ```
22
+
23
+ TypeScript completion shows valid paths and filters. Select a kind, narrow with `where`, then choose how many results you expect:
24
+
25
+ | Method | Result |
26
+ | --- | --- |
27
+ | `one()` | One match; throws for zero or multiple matches |
28
+ | `optional()` | One match or undefined; throws for multiple matches |
29
+ | `first()` / `at(index)` | An explicitly chosen match or undefined |
30
+ | `all()` / iteration | Every match in document traversal order |
31
+ | `count()` | Number of matches |
32
+ | `map(project)` | Project matches into application data |
33
+
34
+ Selectors (`paragraphs`, `runs`, `tables`, `contentControls`, `bookmarks`, `stories`) search descendants. `rows` selects a table's own rows; `cells` selects a table or row's own cells. Nested tables are selected separately with `tables()`. Typed element collections also allow `table.rows[0].cells[1]`. `children()` and `descendants()` provide generic traversal. Scope an existing element with `doc.query.within(element)`. Nested or overlapping selections never duplicate the same element. A story represents an authored story; a reused header remains one semantic story with several physical occurrences.
35
+
36
+ `where({ text: "Total" })` matches exactly and case sensitively. Use `where({ text: { contains: "Total" } })`, `startsWith`, or `endsWith` for other matches. Multiple fields and chained filters combine with AND. Paragraphs support `style`; content controls support `tag` and `title`; bookmarks support `name`; stories support `story` (`body`, `header`, `footer`, `footnote`, `endnote`, `textbox`). Unknown keys fail instead of silently returning unexpected content. Use `.filter(element => ...)` for application predicates.
37
+
38
+ `doc.query.get(id)` resolves an element in the current snapshot. Every element has a `parent`; `doc.query.within(element).closest("cell")` finds its containing cell, including the input itself if it is a cell. Negative query indices count from the end.
39
+
40
+ `findText("...")` or `findText(/pattern/i)` searches contiguous text in selected paragraphs or runs, including text split across runs. It returns a paragraph and a half-open `[start, end)` range in JavaScript UTF-16 offsets. It does not join separate paragraphs. Regular expressions search each selected contiguous interval, preserve the caller's lastIndex, and omit zero-length matches. Queries preserve the selected revision view. Semantic text includes authored content; generated fields and physical appearances can differ after layout. Rasterized picture/chart text has no text query model.
41
+
42
+ Bookmarks expose their published start target through `bookmark.run`; they do not represent an enclosing bookmark text range. Symbols without a Unicode text representation use the object replacement character in query text.
43
+
44
+ ## Replace text and fill templates
45
+
46
+ ```ts
47
+ const customer = doc.query.contentControls().where({ tag: "customer" }).one();
48
+ await doc.update({ target: customer, text: "Willow Design" });
49
+ ```
50
+
51
+ `update` accepts one update or a batch. Targets are runs, paragraphs, cells, content controls, their IDs, or search ranges. Element targets must contain ordinary text in one paragraph. Replacement text inherits the first selected run’s formatting; other selected runs become empty. Tabs and newlines are supported. Batches reject overlapping or missing targets, generated fields, and multi-paragraph replacements without changing the document.
52
+
53
+ Use search ranges for partial replacements while keeping surrounding text and formatting:
54
+
55
+ ```ts
56
+ await doc.update(doc.query.findText("{{customer}}").map(target => ({ target, text: "Willow Design" })));
57
+ ```
58
+
59
+ Batch range offsets refer to the original paragraph text. Empty ranges insert text; offsets must not split an emoji or other surrogate pair. Unchanged updates preserve existing formatting, snapshots, and attachments.
60
+
61
+ Browser updates rebuild layout and refresh mounted views. Query, semantic, layout, page and geometry values obtained earlier remain snapshots; read them again after updating. Application attachments are removed during refresh and can be reattached using fresh geometry. Updates run in submission order and accept `{ signal }` for cancellation. The original `source` is unchanged.
62
+
63
+ Node inspection exposes the same update method. For server rendering, inspect the input with the default entry point to select elements or ranges, then pass those updates to the server document before `renderPage` or `pdf`. Exports reflect the updated text. Saving an updated DOCX, structural editing, and field recalculation are not supported.
64
+
65
+ ## Render and attach ordinary DOM
66
+
67
+ Mounted views include read-only text selection and plain-text copying. Drag or Shift-click to select text, extend with Shift and the arrow/Home/End keys, select all with Ctrl/Command+A, and copy with Ctrl/Command+C or the Copy context menu. Selection spans pages and survives scrolling and zoom, including pages whose canvases have been released. Document updates clear the previous selection. Attached inputs retain their normal editing and clipboard behavior.
68
+
69
+ ```ts
70
+ import { openDocument } from "@onodocs/sdk/browser";
71
+ import { createDocument } from "@onodocs/canvas";
72
+
73
+ const doc = await openDocument(file);
74
+ const canvasDocument = createDocument(doc);
75
+ const view = canvasDocument.mount(container);
76
+ const field = doc.query.contentControls().where({ tag: "customer-name" }).one();
77
+ const input = document.createElement("input");
78
+ input.name = "customerName";
79
+ const attachment = view.attach(input, { anchor: doc.geometry.fragments(field)[0] });
80
+ ```
81
+
82
+ The example requires a tagged control that produces one visible fragment. Call `doc.geometry.fragments(field)` for content spanning lines, pages, or repeated stories, and attach to an explicitly chosen fragment. Fragments retain transforms and clipping. Table/cell anchors use their final physical rectangles. `offset` and `size` optionally adjust attachment placement in page units. Values, validation, submission, focus, and accessibility labels belong to the application. Attaching an element moves it into the view. Detachment removes it and restores its original inline style. Reattaching the same HTML element disposes its previous attachment; old handles become inert. An attachment exposes its disposed state.
83
+
84
+ Pages are anchors too. Placement avoids manual rectangle arithmetic:
85
+
86
+ ```js
87
+ view.attach(checkbox, { anchor: doc.geometry.fragments(doc.query.findText("Client approval:").one())[0], placement: "outside-right", gap: 80, size: { width: 420, height: 420 } });
88
+ view.attach(submitButton, { anchor: doc.pages[0], placement: "inside-bottom-right", inset: 480, size: { width: 2400, height: 600 } });
89
+ ```
90
+
91
+ Inside placements use `inside-{top|center|bottom}-{left|center|right}` with optional `inset`. Outside placements use `outside-{top|right|bottom|left}` with optional `gap` and center the element along the selected edge. The default is `overlay`. All sizes, spacing and offsets use twips (1/1440 inch). Without a size, the element fills the anchor, reduced by the inset for inside placement. Offsets apply after placement. Content anchors retain their transforms and clips; page anchors use the full page. Attachments do not reflow text, so reserve room in the document.
92
+
93
+ Adjacent styled runs on one line share a fragment. Fragment bounds enclose the unclipped geometry; the supplied clips determine which portions are visible.
94
+
95
+ `view.setZoom("fit-width")` tracks container width. Numeric zoom uses 96 CSS pixels per inch at `1`. `view.toClient(pageIndex, point)` and `view.fromClient({ x: event.clientX, y: event.clientY })` translate coordinates. `view.hitTest(clientPoint)` returns `{ elementId, fragment, caret? }`. Resolve `elementId` with `doc.query.get(elementId)` when the processing SDK is available. Text hits resolve to a run or paragraph; table whitespace resolves to a cell or table. `caret` contains `{ paragraphId, offset, point }`, with the same UTF-16 offset convention as search ranges. Generated field text omits a caret when no source position can be represented. Raster drawings without a semantic hit identity and blank page space return no element.
96
+
97
+ `view.scrollTo(anchor, { block: "center" })` reveals a page or a physical fragment. Resolve elements and text ranges through `doc.geometry.fragments(anchor)` first, then choose the occurrence to reveal.
98
+
99
+ A run's optional `link` is `{ kind: "external", target, tooltip }` or `{ kind: "bookmark", target }`. Use bookmark queries and scrollTo for internal links. The host application decides whether and how to open external destinations.
100
+
101
+ All page and query indices are **zero based**. Page geometry uses **twips: 1440 units per inch**. `await canvasDocument.pages[index].render(canvas, { dpi: 144 })` prepares the page resources and paints the canvas; `await doc.pages[index].load(signal)` returns the immutable page commands and their resources. Containers should have a usable width. Multiple views can share one document.
102
+
103
+ Call `attachment.dispose()`, `view.dispose()`, `canvasDocument.dispose()`, or `doc.dispose()` when done. Canvas disposal removes its views and releases its font registrations. Processing disposal also disposes subscribed Canvas documents and releases processing resources. Models remain inspectable; further painting is rejected. Pass `{ signal }` when opening. Opening copies byte inputs; a File/Blob is read once by either the browser or analysis entry point. Fetch URLs in host code with the authentication policy your application needs, then pass bytes.
104
+
105
+ ## Show pages while processing
106
+
107
+ Progress events expose a document source that the Canvas package can observe. Create the view once. Its subscription follows provisional page replacements and the completed layout.
108
+
109
+ ```ts
110
+ import { openDocument } from "@onodocs/sdk/browser";
111
+ import { createDocument, type CanvasDocument } from "@onodocs/canvas";
112
+
113
+ let canvasDocument: CanvasDocument | undefined;
114
+ const doc = await openDocument(bytes, {
115
+ signal,
116
+ onProgress(progress) {
117
+ canvasDocument ??= createDocument(progress.document, { container });
118
+ status.textContent = progress.stage;
119
+ },
120
+ });
121
+ ```
122
+
123
+ Cancelling or failing processing disposes the source and its subscribed view. After success, dispose `doc` when the application closes the document. A Canvas document can be disposed earlier without closing the processor.
124
+
125
+ ## Render on the server
126
+
127
+ ```ts
128
+ import { readFile, writeFile } from "node:fs/promises";
129
+ import { createRenderer } from "@onodocs/sdk/server";
130
+
131
+ const renderer = await createRenderer({ channel: "chrome" });
132
+ try {
133
+ const doc = await renderer.openDocument(await readFile("invoice.docx"));
134
+ try {
135
+ await writeFile("invoice.png", await doc.renderPage(0, { dpi: 144 }));
136
+ await writeFile("invoice.pdf", await doc.pdf({ dpi: 150 }));
137
+ } finally { await doc.dispose(); }
138
+ } finally { await renderer.dispose(); }
139
+ ```
140
+
141
+ Install Chromium/Chrome separately; use `executablePath` for a deployment-managed binary or `channel: "chrome"`/`"msedge"` for an installed browser. The renderer reuses one browser, with a separate context per document. PNG and JPEG output use the browser engine's Canvas path. PDF contains rasterized pages at their document sizes, including mixed sizes. It does not provide searchable PDF text or tagged-PDF accessibility.
142
+
143
+ Contexts make no outbound network requests. Supply fonts as data URLs through `fonts`, or install them on the rendering host. Browser consumers can also supply font URLs. Embedded document fonts use the engine's existing resource path. Use identical browser versions and fonts when reproducible output matters.
144
+
145
+ Opening and output methods accept an AbortSignal. Cancelling an active operation closes that document context; open a new document to retry. Cancelling an operation still waiting in the queue leaves the active operation and document intact. Both server documents and renderers expose `disposed`. Disposing the renderer closes all remaining documents. A renderer can open multiple documents concurrently; output requests on one document are serialized. Applications control scheduling and process isolation.
146
+
147
+ ## Process on the backend and render in the frontend
148
+
149
+ The server renderer publishes a manifest with page sizes and interaction data. Its `page(index)` method returns the same `PageDisplayList` used by local Canvas rendering. Expose these results through authenticated routes in your application. The package does not install an HTTP server or choose an authentication policy.
150
+
151
+ ```ts
152
+ import { createRenderer } from "@onodocs/sdk/server";
153
+ import { encode } from "@onodocs/canvas";
154
+
155
+ const renderer = await createRenderer({ channel: "chrome" });
156
+ const doc = await renderer.openDocument(bytes, { fonts });
157
+
158
+ const manifestResponse = encode(await doc.manifest());
159
+ const firstPageResponse = encode(await doc.page(0));
160
+ ```
161
+
162
+ Use `encode` and `decode` for HTTP bodies because page geometry contains bigint coordinates and resources contain byte arrays. They encode the canonical contracts directly. The transport has no separate document model or schema version. Both packages should come from the same release.
163
+
164
+ The frontend imports only the Canvas package. This example expects `/document/manifest` and `/document/pages/:index` routes supplied by the application:
165
+
166
+ ```ts
167
+ import { createDocument, decode, type DocumentManifest, type PageDisplayList } from "@onodocs/canvas";
168
+
169
+ const response = await fetch("/document/manifest");
170
+ if (!response.ok) throw new Error("Document is unavailable.");
171
+ const manifest = decode<DocumentManifest>(await response.text());
172
+ const canvasDocument = createDocument({
173
+ pages: manifest.pages.map(page => ({
174
+ ...page,
175
+ async load(signal) {
176
+ const response = await fetch(`/document/pages/${page.index}`, { signal });
177
+ if (!response.ok) throw new Error("Page is unavailable.");
178
+ return decode<PageDisplayList>(await response.text());
179
+ },
180
+ })),
181
+ }, { container });
182
+ ```
183
+
184
+ Selection, copying, hit testing, zoom and attachments work from the published geometry. Semantic queries and updates remain on the backend. After an update, publish a fresh manifest and page set; do not mix pages from different document snapshots. Applications can cache immutable page responses or share resource transfers to suit their transport. Dispose the Canvas document when replacing its source, and dispose server documents and the renderer when they are no longer needed.
185
+
186
+ Published pages contain visible text and resources. Keeping the source DOCX on the backend does not hide the displayed content from the frontend. Supplied and embedded font bytes travel with the pages. Fonts resolved from the server's installed fonts remain explicit local font declarations and require the same fonts on the client; supply font files for deployment across different hosts. Backend image/PDF export remains available when a client cannot reproduce the required font environment.
187
+
188
+ ## Errors and application data
189
+
190
+ Opening, updating, and exporting can reject. Handle errors at your application boundary and dispose browser/server resources in finally blocks. The analysis and browser entries export `PackageOperationError`, `SemanticOperationError`, and `QueryCardinalityError`; cancellation uses the signal's reason.
191
+
192
+ Query elements are readonly in-memory models with parent references, source metadata, and bigint values. Project the fields your application needs instead of serializing the entire model:
193
+
194
+ ```ts
195
+ const paragraphs = doc.query.stories().where({ story: "body" }).paragraphs()
196
+ .map(({ id, text, style }) => ({ id, text, style }));
197
+ const json = JSON.stringify(paragraphs);
198
+ ```
199
+
200
+ The same projection supports AI context, search indexing, and backend messages without coupling your data format to engine internals.
201
+
202
+ ## Low-level stages
203
+
204
+ `doc.source` is the typed source model and `doc.semantic` contains completed document meaning. Element `source` locations refer into `doc.source.sourceFacts` and provenance. These identifiers belong to that loaded document, not arbitrary later revisions. The models describe supported DOCX content; they do not preserve every XML detail for round-trip writing.
205
+
206
+ The main SDK also exports the core stage APIs: `parseDocumentSource`, `createSemanticDocument`, `parseDocumentForRendering`, font preparation, `createLayoutDocument`, and `createPageDisplayList`. Low-level `materializePageDisplayListToCanvas` is exported by `@onodocs/canvas`. Advanced consumers can stop at a stage or replay paint through their own adapter. Use canonical readonly contracts; do not mutate stage outputs. Layout remains the authority for all physical geometry.
207
+
208
+ ### Extract a table as text
209
+
210
+ ```ts
211
+ const data = doc.query.tables()
212
+ .where({ headers: ["Deliverable", "Owner", "Due"] })
213
+ .one().textRows;
214
+ ```
215
+
216
+ `textRows` includes the first row and keeps cells grouped by their own table rows. The header filter matches the first row exactly, in order and case sensitively; it does not infer header formatting. Nested tables remain text inside their containing cells. Merged cells retain authored topology; the result is not padded into a rectangular grid. Use `data.slice(1)` to omit the header. The readonly matrix is a snapshot; query again after an update.
217
+
218
+ ## Large documents
219
+
220
+ Use the progress subscription shown above to display early pages while layout continues. Early pages are provisional: page counts, fields, notes and placement can change as formatting settles. Opening resolves with the complete document. Abort the loading signal to dispose the source and its subscribed views.
221
+
222
+ Mounted views paint the visible neighborhood and release distant canvas pixels. Zoom and scrolling render pages on demand. Decoded images are acquired only for a page render and released afterward. Parsing and document layout still require memory proportional to document content; virtualization does not make the document model constant-memory. Auto-fit tables and document-wide fields can require later content before their output settles.
223
+
224
+ Await page renders before reading or exporting the canvas:
225
+
226
+ ```js
227
+ await canvasDocument.pages[0].render(canvas, { dpi: 144, signal: controller.signal });
228
+ const png = canvas.toDataURL("image/png");
229
+ ```
package/SDK.md ADDED
@@ -0,0 +1,229 @@
1
+ # OnoDocs SDK
2
+
3
+ `@onodocs/sdk` processes Word documents. Its default entry performs analysis in Node or a browser; `/browser` adds layout and immutable page publication using browser font services; `/server` runs that processing in headless Chromium. `@onodocs/canvas` paints prepared pages and provides the viewer, selection and DOM attachments. The Canvas package runs independently of the parser, semantic engine and layout engine.
4
+
5
+ Supported Word inputs are DOCX, DOCM, DOTX and DOTM. Macro-enabled files are rendered without executing VBA. Older binary DOC/DOT files and RTF are not supported.
6
+
7
+ Install with `npm install @onodocs/sdk @onodocs/canvas`. Use matching versions of both packages. A backend can install only the SDK; a frontend receiving prepared pages can install only Canvas. Both packages include TypeScript declarations and this guide. Archives are also available from the [public releases](https://github.com/onodocs/onodocs/releases). Server rendering requires an installed Chromium-family browser. The server adapter includes the Canvas runtime it uses for image and PDF export.
8
+
9
+ Without a key, the SDK runs in free, non-production evaluation mode with full features and a watermark on rendered output. Pass `{ licenseKey }` to `openDocument` for a 30-day trial or commercial entitlement; this works in all three entry points. Verification is entirely offline. Commercial keys permit covered releases indefinitely, with renewal for later releases and support. Browser verification requires HTTPS or a secure local context. See the included `LICENSING.md` and `LICENSE` for details.
10
+
11
+ ## Inspect and query
12
+
13
+ ```ts
14
+ import { openDocument } from "@onodocs/sdk";
15
+
16
+ const doc = await openDocument(bytes);
17
+ const customer = doc.query.contentControls().where({ tag: "customer-name" }).one();
18
+ const cells = doc.query.tables().cells().all();
19
+ const matches = doc.query.findText("Invoice total").all();
20
+ const headers = doc.query.stories().where({ story: "header" }).paragraphs().all();
21
+ ```
22
+
23
+ TypeScript completion shows valid paths and filters. Select a kind, narrow with `where`, then choose how many results you expect:
24
+
25
+ | Method | Result |
26
+ | --- | --- |
27
+ | `one()` | One match; throws for zero or multiple matches |
28
+ | `optional()` | One match or undefined; throws for multiple matches |
29
+ | `first()` / `at(index)` | An explicitly chosen match or undefined |
30
+ | `all()` / iteration | Every match in document traversal order |
31
+ | `count()` | Number of matches |
32
+ | `map(project)` | Project matches into application data |
33
+
34
+ Selectors (`paragraphs`, `runs`, `tables`, `contentControls`, `bookmarks`, `stories`) search descendants. `rows` selects a table's own rows; `cells` selects a table or row's own cells. Nested tables are selected separately with `tables()`. Typed element collections also allow `table.rows[0].cells[1]`. `children()` and `descendants()` provide generic traversal. Scope an existing element with `doc.query.within(element)`. Nested or overlapping selections never duplicate the same element. A story represents an authored story; a reused header remains one semantic story with several physical occurrences.
35
+
36
+ `where({ text: "Total" })` matches exactly and case sensitively. Use `where({ text: { contains: "Total" } })`, `startsWith`, or `endsWith` for other matches. Multiple fields and chained filters combine with AND. Paragraphs support `style`; content controls support `tag` and `title`; bookmarks support `name`; stories support `story` (`body`, `header`, `footer`, `footnote`, `endnote`, `textbox`). Unknown keys fail instead of silently returning unexpected content. Use `.filter(element => ...)` for application predicates.
37
+
38
+ `doc.query.get(id)` resolves an element in the current snapshot. Every element has a `parent`; `doc.query.within(element).closest("cell")` finds its containing cell, including the input itself if it is a cell. Negative query indices count from the end.
39
+
40
+ `findText("...")` or `findText(/pattern/i)` searches contiguous text in selected paragraphs or runs, including text split across runs. It returns a paragraph and a half-open `[start, end)` range in JavaScript UTF-16 offsets. It does not join separate paragraphs. Regular expressions search each selected contiguous interval, preserve the caller's lastIndex, and omit zero-length matches. Queries preserve the selected revision view. Semantic text includes authored content; generated fields and physical appearances can differ after layout. Rasterized picture/chart text has no text query model.
41
+
42
+ Bookmarks expose their published start target through `bookmark.run`; they do not represent an enclosing bookmark text range. Symbols without a Unicode text representation use the object replacement character in query text.
43
+
44
+ ## Replace text and fill templates
45
+
46
+ ```ts
47
+ const customer = doc.query.contentControls().where({ tag: "customer" }).one();
48
+ await doc.update({ target: customer, text: "Willow Design" });
49
+ ```
50
+
51
+ `update` accepts one update or a batch. Targets are runs, paragraphs, cells, content controls, their IDs, or search ranges. Element targets must contain ordinary text in one paragraph. Replacement text inherits the first selected run’s formatting; other selected runs become empty. Tabs and newlines are supported. Batches reject overlapping or missing targets, generated fields, and multi-paragraph replacements without changing the document.
52
+
53
+ Use search ranges for partial replacements while keeping surrounding text and formatting:
54
+
55
+ ```ts
56
+ await doc.update(doc.query.findText("{{customer}}").map(target => ({ target, text: "Willow Design" })));
57
+ ```
58
+
59
+ Batch range offsets refer to the original paragraph text. Empty ranges insert text; offsets must not split an emoji or other surrogate pair. Unchanged updates preserve existing formatting, snapshots, and attachments.
60
+
61
+ Browser updates rebuild layout and refresh mounted views. Query, semantic, layout, page and geometry values obtained earlier remain snapshots; read them again after updating. Application attachments are removed during refresh and can be reattached using fresh geometry. Updates run in submission order and accept `{ signal }` for cancellation. The original `source` is unchanged.
62
+
63
+ Node inspection exposes the same update method. For server rendering, inspect the input with the default entry point to select elements or ranges, then pass those updates to the server document before `renderPage` or `pdf`. Exports reflect the updated text. Saving an updated DOCX, structural editing, and field recalculation are not supported.
64
+
65
+ ## Render and attach ordinary DOM
66
+
67
+ Mounted views include read-only text selection and plain-text copying. Drag or Shift-click to select text, extend with Shift and the arrow/Home/End keys, select all with Ctrl/Command+A, and copy with Ctrl/Command+C or the Copy context menu. Selection spans pages and survives scrolling and zoom, including pages whose canvases have been released. Document updates clear the previous selection. Attached inputs retain their normal editing and clipboard behavior.
68
+
69
+ ```ts
70
+ import { openDocument } from "@onodocs/sdk/browser";
71
+ import { createDocument } from "@onodocs/canvas";
72
+
73
+ const doc = await openDocument(file);
74
+ const canvasDocument = createDocument(doc);
75
+ const view = canvasDocument.mount(container);
76
+ const field = doc.query.contentControls().where({ tag: "customer-name" }).one();
77
+ const input = document.createElement("input");
78
+ input.name = "customerName";
79
+ const attachment = view.attach(input, { anchor: doc.geometry.fragments(field)[0] });
80
+ ```
81
+
82
+ The example requires a tagged control that produces one visible fragment. Call `doc.geometry.fragments(field)` for content spanning lines, pages, or repeated stories, and attach to an explicitly chosen fragment. Fragments retain transforms and clipping. Table/cell anchors use their final physical rectangles. `offset` and `size` optionally adjust attachment placement in page units. Values, validation, submission, focus, and accessibility labels belong to the application. Attaching an element moves it into the view. Detachment removes it and restores its original inline style. Reattaching the same HTML element disposes its previous attachment; old handles become inert. An attachment exposes its disposed state.
83
+
84
+ Pages are anchors too. Placement avoids manual rectangle arithmetic:
85
+
86
+ ```js
87
+ view.attach(checkbox, { anchor: doc.geometry.fragments(doc.query.findText("Client approval:").one())[0], placement: "outside-right", gap: 80, size: { width: 420, height: 420 } });
88
+ view.attach(submitButton, { anchor: doc.pages[0], placement: "inside-bottom-right", inset: 480, size: { width: 2400, height: 600 } });
89
+ ```
90
+
91
+ Inside placements use `inside-{top|center|bottom}-{left|center|right}` with optional `inset`. Outside placements use `outside-{top|right|bottom|left}` with optional `gap` and center the element along the selected edge. The default is `overlay`. All sizes, spacing and offsets use twips (1/1440 inch). Without a size, the element fills the anchor, reduced by the inset for inside placement. Offsets apply after placement. Content anchors retain their transforms and clips; page anchors use the full page. Attachments do not reflow text, so reserve room in the document.
92
+
93
+ Adjacent styled runs on one line share a fragment. Fragment bounds enclose the unclipped geometry; the supplied clips determine which portions are visible.
94
+
95
+ `view.setZoom("fit-width")` tracks container width. Numeric zoom uses 96 CSS pixels per inch at `1`. `view.toClient(pageIndex, point)` and `view.fromClient({ x: event.clientX, y: event.clientY })` translate coordinates. `view.hitTest(clientPoint)` returns `{ elementId, fragment, caret? }`. Resolve `elementId` with `doc.query.get(elementId)` when the processing SDK is available. Text hits resolve to a run or paragraph; table whitespace resolves to a cell or table. `caret` contains `{ paragraphId, offset, point }`, with the same UTF-16 offset convention as search ranges. Generated field text omits a caret when no source position can be represented. Raster drawings without a semantic hit identity and blank page space return no element.
96
+
97
+ `view.scrollTo(anchor, { block: "center" })` reveals a page or a physical fragment. Resolve elements and text ranges through `doc.geometry.fragments(anchor)` first, then choose the occurrence to reveal.
98
+
99
+ A run's optional `link` is `{ kind: "external", target, tooltip }` or `{ kind: "bookmark", target }`. Use bookmark queries and scrollTo for internal links. The host application decides whether and how to open external destinations.
100
+
101
+ All page and query indices are **zero based**. Page geometry uses **twips: 1440 units per inch**. `await canvasDocument.pages[index].render(canvas, { dpi: 144 })` prepares the page resources and paints the canvas; `await doc.pages[index].load(signal)` returns the immutable page commands and their resources. Containers should have a usable width. Multiple views can share one document.
102
+
103
+ Call `attachment.dispose()`, `view.dispose()`, `canvasDocument.dispose()`, or `doc.dispose()` when done. Canvas disposal removes its views and releases its font registrations. Processing disposal also disposes subscribed Canvas documents and releases processing resources. Models remain inspectable; further painting is rejected. Pass `{ signal }` when opening. Opening copies byte inputs; a File/Blob is read once by either the browser or analysis entry point. Fetch URLs in host code with the authentication policy your application needs, then pass bytes.
104
+
105
+ ## Show pages while processing
106
+
107
+ Progress events expose a document source that the Canvas package can observe. Create the view once. Its subscription follows provisional page replacements and the completed layout.
108
+
109
+ ```ts
110
+ import { openDocument } from "@onodocs/sdk/browser";
111
+ import { createDocument, type CanvasDocument } from "@onodocs/canvas";
112
+
113
+ let canvasDocument: CanvasDocument | undefined;
114
+ const doc = await openDocument(bytes, {
115
+ signal,
116
+ onProgress(progress) {
117
+ canvasDocument ??= createDocument(progress.document, { container });
118
+ status.textContent = progress.stage;
119
+ },
120
+ });
121
+ ```
122
+
123
+ Cancelling or failing processing disposes the source and its subscribed view. After success, dispose `doc` when the application closes the document. A Canvas document can be disposed earlier without closing the processor.
124
+
125
+ ## Render on the server
126
+
127
+ ```ts
128
+ import { readFile, writeFile } from "node:fs/promises";
129
+ import { createRenderer } from "@onodocs/sdk/server";
130
+
131
+ const renderer = await createRenderer({ channel: "chrome" });
132
+ try {
133
+ const doc = await renderer.openDocument(await readFile("invoice.docx"));
134
+ try {
135
+ await writeFile("invoice.png", await doc.renderPage(0, { dpi: 144 }));
136
+ await writeFile("invoice.pdf", await doc.pdf({ dpi: 150 }));
137
+ } finally { await doc.dispose(); }
138
+ } finally { await renderer.dispose(); }
139
+ ```
140
+
141
+ Install Chromium/Chrome separately; use `executablePath` for a deployment-managed binary or `channel: "chrome"`/`"msedge"` for an installed browser. The renderer reuses one browser, with a separate context per document. PNG and JPEG output use the browser engine's Canvas path. PDF contains rasterized pages at their document sizes, including mixed sizes. It does not provide searchable PDF text or tagged-PDF accessibility.
142
+
143
+ Contexts make no outbound network requests. Supply fonts as data URLs through `fonts`, or install them on the rendering host. Browser consumers can also supply font URLs. Embedded document fonts use the engine's existing resource path. Use identical browser versions and fonts when reproducible output matters.
144
+
145
+ Opening and output methods accept an AbortSignal. Cancelling an active operation closes that document context; open a new document to retry. Cancelling an operation still waiting in the queue leaves the active operation and document intact. Both server documents and renderers expose `disposed`. Disposing the renderer closes all remaining documents. A renderer can open multiple documents concurrently; output requests on one document are serialized. Applications control scheduling and process isolation.
146
+
147
+ ## Process on the backend and render in the frontend
148
+
149
+ The server renderer publishes a manifest with page sizes and interaction data. Its `page(index)` method returns the same `PageDisplayList` used by local Canvas rendering. Expose these results through authenticated routes in your application. The package does not install an HTTP server or choose an authentication policy.
150
+
151
+ ```ts
152
+ import { createRenderer } from "@onodocs/sdk/server";
153
+ import { encode } from "@onodocs/canvas";
154
+
155
+ const renderer = await createRenderer({ channel: "chrome" });
156
+ const doc = await renderer.openDocument(bytes, { fonts });
157
+
158
+ const manifestResponse = encode(await doc.manifest());
159
+ const firstPageResponse = encode(await doc.page(0));
160
+ ```
161
+
162
+ Use `encode` and `decode` for HTTP bodies because page geometry contains bigint coordinates and resources contain byte arrays. They encode the canonical contracts directly. The transport has no separate document model or schema version. Both packages should come from the same release.
163
+
164
+ The frontend imports only the Canvas package. This example expects `/document/manifest` and `/document/pages/:index` routes supplied by the application:
165
+
166
+ ```ts
167
+ import { createDocument, decode, type DocumentManifest, type PageDisplayList } from "@onodocs/canvas";
168
+
169
+ const response = await fetch("/document/manifest");
170
+ if (!response.ok) throw new Error("Document is unavailable.");
171
+ const manifest = decode<DocumentManifest>(await response.text());
172
+ const canvasDocument = createDocument({
173
+ pages: manifest.pages.map(page => ({
174
+ ...page,
175
+ async load(signal) {
176
+ const response = await fetch(`/document/pages/${page.index}`, { signal });
177
+ if (!response.ok) throw new Error("Page is unavailable.");
178
+ return decode<PageDisplayList>(await response.text());
179
+ },
180
+ })),
181
+ }, { container });
182
+ ```
183
+
184
+ Selection, copying, hit testing, zoom and attachments work from the published geometry. Semantic queries and updates remain on the backend. After an update, publish a fresh manifest and page set; do not mix pages from different document snapshots. Applications can cache immutable page responses or share resource transfers to suit their transport. Dispose the Canvas document when replacing its source, and dispose server documents and the renderer when they are no longer needed.
185
+
186
+ Published pages contain visible text and resources. Keeping the source DOCX on the backend does not hide the displayed content from the frontend. Supplied and embedded font bytes travel with the pages. Fonts resolved from the server's installed fonts remain explicit local font declarations and require the same fonts on the client; supply font files for deployment across different hosts. Backend image/PDF export remains available when a client cannot reproduce the required font environment.
187
+
188
+ ## Errors and application data
189
+
190
+ Opening, updating, and exporting can reject. Handle errors at your application boundary and dispose browser/server resources in finally blocks. The analysis and browser entries export `PackageOperationError`, `SemanticOperationError`, and `QueryCardinalityError`; cancellation uses the signal's reason.
191
+
192
+ Query elements are readonly in-memory models with parent references, source metadata, and bigint values. Project the fields your application needs instead of serializing the entire model:
193
+
194
+ ```ts
195
+ const paragraphs = doc.query.stories().where({ story: "body" }).paragraphs()
196
+ .map(({ id, text, style }) => ({ id, text, style }));
197
+ const json = JSON.stringify(paragraphs);
198
+ ```
199
+
200
+ The same projection supports AI context, search indexing, and backend messages without coupling your data format to engine internals.
201
+
202
+ ## Low-level stages
203
+
204
+ `doc.source` is the typed source model and `doc.semantic` contains completed document meaning. Element `source` locations refer into `doc.source.sourceFacts` and provenance. These identifiers belong to that loaded document, not arbitrary later revisions. The models describe supported DOCX content; they do not preserve every XML detail for round-trip writing.
205
+
206
+ The main SDK also exports the core stage APIs: `parseDocumentSource`, `createSemanticDocument`, `parseDocumentForRendering`, font preparation, `createLayoutDocument`, and `createPageDisplayList`. Low-level `materializePageDisplayListToCanvas` is exported by `@onodocs/canvas`. Advanced consumers can stop at a stage or replay paint through their own adapter. Use canonical readonly contracts; do not mutate stage outputs. Layout remains the authority for all physical geometry.
207
+
208
+ ### Extract a table as text
209
+
210
+ ```ts
211
+ const data = doc.query.tables()
212
+ .where({ headers: ["Deliverable", "Owner", "Due"] })
213
+ .one().textRows;
214
+ ```
215
+
216
+ `textRows` includes the first row and keeps cells grouped by their own table rows. The header filter matches the first row exactly, in order and case sensitively; it does not infer header formatting. Nested tables remain text inside their containing cells. Merged cells retain authored topology; the result is not padded into a rectangular grid. Use `data.slice(1)` to omit the header. The readonly matrix is a snapshot; query again after an update.
217
+
218
+ ## Large documents
219
+
220
+ Use the progress subscription shown above to display early pages while layout continues. Early pages are provisional: page counts, fields, notes and placement can change as formatting settles. Opening resolves with the complete document. Abort the loading signal to dispose the source and its subscribed views.
221
+
222
+ Mounted views paint the visible neighborhood and release distant canvas pixels. Zoom and scrolling render pages on demand. Decoded images are acquired only for a page render and released afterward. Parsing and document layout still require memory proportional to document content; virtualization does not make the document model constant-memory. Auto-fit tables and document-wide fields can require later content before their output settles.
223
+
224
+ Await page renders before reading or exporting the canvas:
225
+
226
+ ```js
227
+ await canvasDocument.pages[0].render(canvas, { dpi: 144, signal: controller.signal });
228
+ const png = canvas.toDataURL("image/png");
229
+ ```