@aseity/idocs-preview 0.2.0 → 0.3.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @aseity/idocs-preview
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add `Session.exportDocx` to export iDocs source bytes and referenced images as a Word document in the browser. Invalid source, missing images and unapproved conversion losses reject instead of producing a partial file.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -50,6 +50,7 @@ during server rendering.
50
50
  - `@aseity/idocs-preview/core`: the headless session. `createSession()` starts the
51
51
  worker; `session.open(bytes, options)` resolves to a `PreviewDocument` with the
52
52
  pages' display lists, diagnostics and conversion losses.
53
+ `session.exportDocx(bytes, options)` exports iDocs source to DOCX bytes.
53
54
  - `@aseity/idocs-preview/view`: the reference view. `createPreview(container)`
54
55
  owns a session and a view; `mountPreview(container, doc)` shows a document you
55
56
  opened yourself. Both re-export the core types.
@@ -99,6 +100,36 @@ document. A callback exception rejects its open.
99
100
  The development page enables progressive delivery with `?progressive=1`.
100
101
  There is no `compact` option or legacy JSON-only transport.
101
102
 
103
+ ## Exporting Word documents
104
+
105
+ `session.exportDocx(bytes, { name?, readResource? })` exports an iDocs source
106
+ into DOCX bytes. It accepts the same byte types and image callback as `open`.
107
+ The source bytes are copied before transfer. Every export owns its resources,
108
+ so concurrent opens and exports remain independent.
109
+
110
+ ```ts
111
+ const session = createSession();
112
+ try {
113
+ const bytes = await session.exportDocx(source, {
114
+ name: "report.idoc",
115
+ readResource: (path) => readImage(path),
116
+ });
117
+ const file = new Blob([bytes], {
118
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
119
+ });
120
+ saveFile(file, "report.docx");
121
+ } finally {
122
+ session.dispose();
123
+ }
124
+ ```
125
+
126
+ Export uses browser font measurements for layout and preserves the document's
127
+ font declarations in DOCX. Word may paginate differently on the recipient's
128
+ machine. Invalid source, unavailable or invalid images, layout errors and
129
+ unapproved conversion losses reject with `PreviewError`. Export never replaces
130
+ missing images with preview placeholders. Export failures also include the
131
+ `export-failed` code. Disposing the session rejects pending and future exports.
132
+
102
133
  ## View
103
134
 
104
135
  - Zoom 1 is 100% (one point is 4/3 CSS pixels), clamped to 0.1–10. `setZoom` keeps
@@ -3,7 +3,7 @@
3
3
  // code behind a MessageChannel. Layout is synchronous inside one wasm call;
4
4
  // resources the host has not answered yet come back as `needs`, and the open
5
5
  // is retried once they arrive.
6
- import init, { openDocument } from '../../wasm/idocs_wasm.js';
6
+ import init, { exportDocx, openDocument } from '../../wasm/idocs_wasm.js';
7
7
  import { canvasMeasure } from './fonts.js';
8
8
  export function serve(endpoint, options = {}) {
9
9
  let ready;
@@ -19,12 +19,17 @@ export function serve(endpoint, options = {}) {
19
19
  try {
20
20
  if (!measure)
21
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);
22
+ if (job.type === 'exportDocx') {
23
+ outcome = exportDocx(job.bytes, job.name, job.assets, measure);
24
+ }
25
+ else {
26
+ if (job.progressive)
27
+ post({ type: 'pageReset', id });
28
+ const onPage = job.progressive ? (packet) => {
29
+ post({ type: 'page', id, ...packet }, [packet.data.buffer, ...packet.images.map(bytes => bytes.buffer)]);
30
+ } : undefined;
31
+ outcome = openDocument(job.kind, job.bytes, job.name, job.assets, measure, onPage);
32
+ }
28
33
  }
29
34
  catch (error) {
30
35
  pending.delete(id);
@@ -40,6 +45,10 @@ export function serve(endpoint, options = {}) {
40
45
  post({ type: 'failed', id, error: JSON.parse(outcome.json) });
41
46
  return;
42
47
  }
48
+ if (outcome.status === 'exported') {
49
+ post({ type: 'exported', id, bytes: outcome.bytes }, [outcome.bytes.buffer]);
50
+ return;
51
+ }
43
52
  post({ type: 'opened', id, json: outcome.json, images: outcome.images, data: outcome.data }, [outcome.data.buffer, ...outcome.images.map((bytes) => bytes.buffer)]);
44
53
  };
45
54
  endpoint.addEventListener('message', async (event) => {
@@ -58,8 +67,12 @@ export function serve(endpoint, options = {}) {
58
67
  post({ type: 'failed', id, error: { code: 'wasm-unavailable', message: String(error), diagnostics: [] } });
59
68
  return;
60
69
  }
61
- if (message.type === 'open') {
70
+ if (message.type === 'exportDocx') {
71
+ pending.set(message.id, { type: 'exportDocx', name: message.name, bytes: message.bytes, assets: new Map() });
72
+ }
73
+ else if (message.type === 'open') {
62
74
  pending.set(message.id, {
75
+ type: 'open',
63
76
  kind: message.kind,
64
77
  progressive: message.progressive ?? false,
65
78
  name: message.name,
@@ -34,7 +34,9 @@ export declare class PreviewError extends Error {
34
34
  diagnostics: PreviewDocument['diagnostics'];
35
35
  });
36
36
  }
37
+ export type ExportOptions = Pick<OpenOptions, 'name' | 'readResource'>;
37
38
  export interface Session {
39
+ exportDocx(bytes: Bytes, options?: ExportOptions): Promise<Uint8Array>;
38
40
  open(bytes: Bytes, options?: OpenOptions): Promise<PreviewDocument>;
39
41
  /**
40
42
  * Terminates the worker; pending opens reject. A worker that fails on its
@@ -52,14 +52,14 @@ export function createSession(options = {}) {
52
52
  const job = jobs.get(message.id);
53
53
  if (!job)
54
54
  return;
55
- if (message.type === 'pageReset') {
55
+ if (message.type === 'pageReset' && job.kind === 'open') {
56
56
  job.pages = [];
57
57
  job.images = [];
58
58
  job.previewImages = [];
59
59
  job.outlines = [];
60
60
  return;
61
61
  }
62
- if (message.type === 'page') {
62
+ if (message.type === 'page' && job.kind === 'open') {
63
63
  try {
64
64
  job.images.push(...message.images);
65
65
  const partial = decodeCompact(message.json, message.data, message.images);
@@ -86,7 +86,10 @@ export function createSession(options = {}) {
86
86
  return;
87
87
  }
88
88
  jobs.delete(message.id);
89
- if (message.type === 'opened') {
89
+ if (message.type === 'exported' && job.kind === 'export') {
90
+ job.resolve(message.bytes);
91
+ }
92
+ else if (message.type === 'opened' && job.kind === 'open') {
90
93
  try {
91
94
  const doc = decodeCompact(message.json, message.data, job.options.onPage ? job.images : message.images);
92
95
  if (job.options.onPage) {
@@ -100,8 +103,10 @@ export function createSession(options = {}) {
100
103
  job.reject(new PreviewError({ code: 'internal-error', message: String(error), diagnostics: [] }));
101
104
  }
102
105
  }
103
- else
106
+ else if (message.type === 'failed')
104
107
  job.reject(new PreviewError(message.error));
108
+ else
109
+ job.reject(new PreviewError({ code: 'internal-error', message: 'Unexpected worker response', diagnostics: [] }));
105
110
  });
106
111
  // A worker script that fails to load or run, or a message that cannot be
107
112
  // deserialized, leaves no reply to wait for: give up on the worker.
@@ -124,6 +129,18 @@ export function createSession(options = {}) {
124
129
  worker.start?.();
125
130
  post({ type: 'init', wasmUrl: options.wasmUrl?.toString() });
126
131
  return {
132
+ exportDocx(input, exportOptions = {}) {
133
+ if (disposed)
134
+ return Promise.reject(new Error('The preview session is disposed'));
135
+ if (broken)
136
+ return Promise.reject(broken);
137
+ const bytes = copyBytes(input);
138
+ const id = next++;
139
+ return new Promise((resolve, reject) => {
140
+ jobs.set(id, { kind: 'export', options: exportOptions, resolve, reject });
141
+ post({ type: 'exportDocx', id, name: exportOptions.name ?? 'document.idoc', bytes }, [bytes.buffer]);
142
+ });
143
+ },
127
144
  open(input, openOptions = {}) {
128
145
  if (disposed)
129
146
  return Promise.reject(new Error('The preview session is disposed'));
@@ -132,7 +149,7 @@ export function createSession(options = {}) {
132
149
  const bytes = copyBytes(input);
133
150
  const id = next++;
134
151
  return new Promise((resolve, reject) => {
135
- jobs.set(id, { options: openOptions, resolve, reject, pages: [], images: [], previewImages: [], outlines: [] });
152
+ jobs.set(id, { kind: 'open', options: openOptions, resolve, reject, pages: [], images: [], previewImages: [], outlines: [] });
136
153
  post({
137
154
  type: 'open',
138
155
  progressive: openOptions.onPage !== undefined,
@@ -84,6 +84,11 @@ export type ResourceNeed = {
84
84
  export type ToWorker = {
85
85
  type: 'init';
86
86
  wasmUrl?: string;
87
+ } | {
88
+ type: 'exportDocx';
89
+ id: number;
90
+ name: string;
91
+ bytes: Uint8Array;
87
92
  } | {
88
93
  type: 'open';
89
94
  id: number;
@@ -120,6 +125,10 @@ export type FromWorker = {
120
125
  type: 'needs';
121
126
  id: number;
122
127
  needs: ResourceNeed[];
128
+ } | {
129
+ type: 'exported';
130
+ id: number;
131
+ bytes: Uint8Array;
123
132
  } | {
124
133
  type: 'opened';
125
134
  id: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aseity/idocs-preview",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Read-only browser preview for DOCX and iDocs source documents: Browser font measurements with layout in a Web Worker (WASM) and drawing in a Canvas page view.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,8 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
 
4
+ export function exportDocx(bytes: Uint8Array, file_name: string, assets: Map<any, any>, measure: Function): any;
5
+
4
6
  /**
5
7
  * Returns `{status: "needs" | "ready" | "failed", json, data?, images}`; see
6
8
  * `web/src/core/protocol.ts` for the JSON shapes. `measure(family,
@@ -13,6 +15,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
13
15
 
14
16
  export interface InitOutput {
15
17
  readonly memory: WebAssembly.Memory;
18
+ readonly exportDocx: (a: number, b: number, c: number, d: number, e: any, f: any) => any;
16
19
  readonly openDocument: (a: number, b: number, c: number, d: number, e: number, f: number, g: any, h: any, i: number) => any;
17
20
  readonly __wbindgen_malloc: (a: number, b: number) => number;
18
21
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
@@ -1,5 +1,21 @@
1
1
  /* @ts-self-types="./idocs_wasm.d.ts" */
2
2
 
3
+ /**
4
+ * @param {Uint8Array} bytes
5
+ * @param {string} file_name
6
+ * @param {Map<any, any>} assets
7
+ * @param {Function} measure
8
+ * @returns {any}
9
+ */
10
+ export function exportDocx(bytes, file_name, assets, measure) {
11
+ const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
12
+ const len0 = WASM_VECTOR_LEN;
13
+ const ptr1 = passStringToWasm0(file_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
14
+ const len1 = WASM_VECTOR_LEN;
15
+ const ret = wasm.exportDocx(ptr0, len0, ptr1, len1, assets, measure);
16
+ return ret;
17
+ }
18
+
3
19
  /**
4
20
  * Returns `{status: "needs" | "ready" | "failed", json, data?, images}`; see
5
21
  * `web/src/core/protocol.ts` for the JSON shapes. `measure(family,
Binary file
@@ -1,6 +1,7 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
+ export const exportDocx: (a: number, b: number, c: number, d: number, e: any, f: any) => any;
4
5
  export const openDocument: (a: number, b: number, c: number, d: number, e: number, f: number, g: any, h: any, i: number) => any;
5
6
  export const __wbindgen_malloc: (a: number, b: number) => number;
6
7
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;