@drawmotive/textgraph 0.2.0 → 0.2.2-alpha.1
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/README.md +176 -74
- package/docs/api-v1.md +53 -7
- package/docs/images/README.md +28 -0
- package/docs/images/service-flow.png +0 -0
- package/docs/react.md +7 -4
- package/docs/releases.md +34 -15
- package/examples/service-flow.textgraph +6 -0
- package/generated/wasm/DrawMotive.TextGraph.Bridge.wasm +0 -0
- package/generated/wasm/ExCSS.wasm +0 -0
- package/generated/wasm/Graphics.Core.wasm +0 -0
- package/generated/wasm/Grpc.Core.Api.wasm +0 -0
- package/generated/wasm/HarfBuzzSharp.wasm +0 -0
- package/generated/wasm/Math.Core.wasm +0 -0
- package/generated/wasm/Microsoft.Extensions.Logging.Abstractions.wasm +0 -0
- package/generated/wasm/NotoSans-Regular.ttf +0 -0
- package/generated/wasm/Polly.Core.wasm +0 -0
- package/generated/wasm/RBush.wasm +0 -0
- package/generated/wasm/SkiaSharp.wasm +0 -0
- package/generated/wasm/System.Linq.wasm +0 -0
- package/generated/wasm/System.Memory.wasm +0 -0
- package/generated/wasm/System.Private.CoreLib.wasm +0 -0
- package/generated/wasm/System.Runtime.wasm +0 -0
- package/generated/wasm/System.Text.Json.wasm +0 -0
- package/generated/wasm/System.Text.RegularExpressions.wasm +0 -0
- package/generated/wasm/dotnet.boot.js +22 -17
- package/generated/wasm/dotnet.native.wasm +0 -0
- package/generated/wasm/themes.css +25 -12
- package/generated/wasm-manifest.js +42 -35
- package/generated/wasm-manifest.json +42 -35
- package/package.json +15 -3
- package/schemas/render.schema.json +33 -1
- package/src/index.d.ts +21 -3
- package/src/index.js +2 -2
- package/src/platform/node.d.ts +2 -0
- package/src/platform/node.js +20 -1
- package/src/react/index.d.ts +1 -0
- package/src/react/index.js +6 -3
- package/src/runtime/browser.js +4 -5
- package/src/runtime/dotnet.js +10 -6
- package/src/runtime/font-resources.js +148 -0
- package/src/runtime/language-packs.js +32 -1
- package/src/runtime/node.js +22 -1
- package/src/runtime/render-resources.js +13 -12
- package/src/runtime/rendering.js +13 -4
package/package.json
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawmotive/textgraph",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "TextGraph
|
|
3
|
+
"version": "0.2.2-alpha.1",
|
|
4
|
+
"description": "Render TextGraph flowcharts and directed graphs as PNG images in Node.js, browsers, React, and Web Workers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"textgraph",
|
|
7
|
+
"diagram",
|
|
8
|
+
"flowchart",
|
|
9
|
+
"directed-graph",
|
|
10
|
+
"text-to-diagram",
|
|
11
|
+
"automatic-layout",
|
|
12
|
+
"png",
|
|
13
|
+
"react",
|
|
14
|
+
"webassembly",
|
|
15
|
+
"wasm"
|
|
16
|
+
],
|
|
5
17
|
"type": "module",
|
|
6
18
|
"types": "./src/index.d.ts",
|
|
7
19
|
"exports": {
|
|
@@ -82,7 +94,7 @@
|
|
|
82
94
|
"publishConfig": {
|
|
83
95
|
"access": "public",
|
|
84
96
|
"registry": "https://registry.npmjs.org/",
|
|
85
|
-
"tag": "
|
|
97
|
+
"tag": "alpha"
|
|
86
98
|
},
|
|
87
99
|
"peerDependencies": {
|
|
88
100
|
"react": "^18.2.0 || ^19.0.0"
|
|
@@ -76,6 +76,14 @@
|
|
|
76
76
|
"type": "integer",
|
|
77
77
|
"minimum": 1,
|
|
78
78
|
"maximum": 2147483647
|
|
79
|
+
},
|
|
80
|
+
"displayWidth": {
|
|
81
|
+
"type": "number",
|
|
82
|
+
"exclusiveMinimum": 0
|
|
83
|
+
},
|
|
84
|
+
"displayHeight": {
|
|
85
|
+
"type": "number",
|
|
86
|
+
"exclusiveMinimum": 0
|
|
79
87
|
}
|
|
80
88
|
},
|
|
81
89
|
"required": [
|
|
@@ -145,6 +153,22 @@
|
|
|
145
153
|
"properties": {
|
|
146
154
|
"height": {}
|
|
147
155
|
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"required": [
|
|
159
|
+
"displayWidth"
|
|
160
|
+
],
|
|
161
|
+
"properties": {
|
|
162
|
+
"displayWidth": {}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"required": [
|
|
167
|
+
"displayHeight"
|
|
168
|
+
],
|
|
169
|
+
"properties": {
|
|
170
|
+
"displayHeight": {}
|
|
171
|
+
}
|
|
148
172
|
}
|
|
149
173
|
]
|
|
150
174
|
},
|
|
@@ -163,5 +187,13 @@
|
|
|
163
187
|
}
|
|
164
188
|
}
|
|
165
189
|
}
|
|
166
|
-
]
|
|
190
|
+
],
|
|
191
|
+
"dependentRequired": {
|
|
192
|
+
"displayWidth": [
|
|
193
|
+
"displayHeight"
|
|
194
|
+
],
|
|
195
|
+
"displayHeight": [
|
|
196
|
+
"displayWidth"
|
|
197
|
+
]
|
|
198
|
+
}
|
|
167
199
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -93,14 +93,28 @@ export interface TextGraphInitializeOptions {
|
|
|
93
93
|
adapters?: TextGraphAdapters;
|
|
94
94
|
resolveAsset?(asset: RuntimeAsset, defaultUrl: URL): string | URL;
|
|
95
95
|
fetch?: typeof globalThis.fetch;
|
|
96
|
-
/**
|
|
96
|
+
/** Legacy descriptors load on first render; descriptors with coverage load on demand. */
|
|
97
97
|
languagePacks?: readonly TextGraphLanguagePack[];
|
|
98
|
+
/** Optional deployed catalog; without a package, online rendering uses the staging catalog. */
|
|
99
|
+
fontAssets?: TextGraphFontAssets;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface TextGraphFontAssets {
|
|
103
|
+
/** Catalog URL; files resolve relative to this URL. A configured catalog is authoritative. */
|
|
104
|
+
readonly catalog?: URL;
|
|
105
|
+
/** Disable external fallback for offline hosts, or select another remote catalog. */
|
|
106
|
+
readonly fallback?: false | URL;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
export interface TextGraphFont {
|
|
101
110
|
readonly family: string;
|
|
102
111
|
/** Use a URL (including file: in Node) or bytes; bytes are copied during initialization. */
|
|
103
112
|
readonly source: URL | Uint8Array;
|
|
113
|
+
/** Sorted, disjoint inclusive Unicode ranges. Supply all metadata fields together for lazy loading. */
|
|
114
|
+
readonly coverage?: readonly (readonly [number, number])[];
|
|
115
|
+
readonly languages?: readonly string[];
|
|
116
|
+
readonly bytes?: number;
|
|
117
|
+
readonly sha256?: string;
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
export interface TextGraphLanguagePack {
|
|
@@ -112,9 +126,11 @@ export interface TextGraphLanguagePack {
|
|
|
112
126
|
export type TextGraphPngEncoding = 'bytes' | 'base64';
|
|
113
127
|
|
|
114
128
|
export interface TextGraphRenderPngOptions<Encoding extends TextGraphPngEncoding = TextGraphPngEncoding> {
|
|
129
|
+
/** Optional language hint for ambiguous Han text, e.g. ja or zh-CN. */
|
|
130
|
+
language?: string;
|
|
115
131
|
/** Defaults to bytes. Base64 contains no data-URL prefix. */
|
|
116
132
|
encoding?: Encoding;
|
|
117
|
-
/** Positive
|
|
133
|
+
/** Positive raster density relative to logical diagram size. Defaults to 1. */
|
|
118
134
|
scale?: number;
|
|
119
135
|
/** Non-negative padding in diagram units. Defaults to 10. */
|
|
120
136
|
padding?: number;
|
|
@@ -125,7 +141,9 @@ export interface TextGraphRenderPngOptions<Encoding extends TextGraphPngEncoding
|
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
export type TextGraphRenderPngResult<Png extends Uint8Array | string = Uint8Array | string> =
|
|
128
|
-
| { readonly success: true; readonly png: Png; readonly width: number; readonly height: number;
|
|
144
|
+
| { readonly success: true; readonly png: Png; readonly width: number; readonly height: number;
|
|
145
|
+
/** Logical display dimensions before density scaling; omitted by older runtimes. */
|
|
146
|
+
readonly displayWidth?: number; readonly displayHeight?: number; readonly diagnostics: readonly TextGraphRenderDiagnostic[] }
|
|
129
147
|
| { readonly success: false; readonly diagnostics: readonly TextGraphRenderDiagnostic[] };
|
|
130
148
|
|
|
131
149
|
export interface TextGraphRuntimeInfo extends TextGraphRuntimeManifest {
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { validateTextGraphAdapters } from './adapters.js';
|
|
|
2
2
|
import { createManagedInstance, throwIfAborted, validateAbi, waitForInitialResource } from './runtime/lifecycle.js';
|
|
3
3
|
import { decodeValidation, validateSignal } from './runtime/validation.js';
|
|
4
4
|
import { DrawMotiveError } from './runtime/errors.js';
|
|
5
|
-
import { normalizeLanguagePacks } from './runtime/language-packs.js';
|
|
5
|
+
import { normalizeFontAssets, normalizeLanguagePacks } from './runtime/language-packs.js';
|
|
6
6
|
import { decodeRender, normalizeRenderOptions } from './runtime/rendering.js';
|
|
7
7
|
import manifest from '../generated/wasm-manifest.js';
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ export async function initializeTextGraph(options = {}) {
|
|
|
26
26
|
if (typeof options.loadRuntime !== 'function') {
|
|
27
27
|
throw new TypeError('initializeTextGraph requires a loadRuntime function');
|
|
28
28
|
}
|
|
29
|
-
const normalized = Object.freeze({ ...options, adapters: validateTextGraphAdapters(options.adapters), languagePacks: normalizeLanguagePacks(options.languagePacks) });
|
|
29
|
+
const normalized = Object.freeze({ ...options, adapters: validateTextGraphAdapters(options.adapters), languagePacks: normalizeLanguagePacks(options.languagePacks), fontAssets: normalizeFontAssets(options.fontAssets) });
|
|
30
30
|
const runtime = await waitForInitialResource(() => options.loadRuntime(normalized), options.signal);
|
|
31
31
|
await validateAbi(runtime, abiManifest.abiVersion);
|
|
32
32
|
const instance = createManagedInstance([runtime]);
|
package/src/platform/node.d.ts
CHANGED
package/src/platform/node.js
CHANGED
|
@@ -3,10 +3,29 @@ import { initializeTextGraph as initialize } from '../index.js';
|
|
|
3
3
|
import { loadNodeRuntime } from '../runtime/node.js';
|
|
4
4
|
import manifest from '../../generated/wasm-manifest.js';
|
|
5
5
|
import { DrawMotiveError } from '../runtime/errors.js';
|
|
6
|
+
import { normalizeFontAssets, normalizeLanguagePacks } from '../runtime/language-packs.js';
|
|
6
7
|
|
|
7
8
|
export async function initializeTextGraph(options = {}) {
|
|
8
9
|
if (!options || typeof options !== 'object') throw new DrawMotiveError('INVALID_ARGUMENT', 'Initialization options must be an object');
|
|
9
|
-
|
|
10
|
+
options = { ...options };
|
|
11
|
+
// The workspace development command opts in explicitly; imports and browser
|
|
12
|
+
// loaders never consult a local build directory or replace packaged assets.
|
|
13
|
+
const directory = process.env.DRAWMOTIVE_TEXTGRAPH_RUNTIME;
|
|
14
|
+
const languagePacks = normalizeLanguagePacks(options.languagePacks);
|
|
15
|
+
let fontAssets = normalizeFontAssets(options.fontAssets);
|
|
16
|
+
if (!options.loadRuntime && fontAssets?.catalog === undefined && options.languagePacks === undefined) {
|
|
17
|
+
let entry;
|
|
18
|
+
try { entry = import.meta.resolve('@drawmotive/textgraph-fonts'); }
|
|
19
|
+
catch (error) { if (error.code !== 'ERR_MODULE_NOT_FOUND') throw error; }
|
|
20
|
+
// Resolve the optional package from the installed SDK tree. A broken installed
|
|
21
|
+
// package must surface its error; only absence enables the remote source.
|
|
22
|
+
if (entry) {
|
|
23
|
+
const pack = await import(entry);
|
|
24
|
+
if (!(pack.fontCatalogUrl instanceof URL)) throw new DrawMotiveError('INVALID_MANIFEST', 'Installed font package has no catalog URL');
|
|
25
|
+
fontAssets = { ...fontAssets, catalog: pack.fontCatalogUrl };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return initialize({ ...options, languagePacks, fontAssets, loadRuntime: options.loadRuntime ?? (context => loadNodeRuntime(context, manifest, directory)) });
|
|
10
29
|
}
|
|
11
30
|
|
|
12
31
|
export const platform = 'node';
|
package/src/react/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface TextGraphProviderProps {
|
|
|
11
11
|
export interface TextGraphProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet' | 'children' | 'loading'> {
|
|
12
12
|
source: string;
|
|
13
13
|
alt?: string;
|
|
14
|
+
/** Defaults to scale 1; the image displays at logical size. */
|
|
14
15
|
renderOptions?: Omit<TextGraphRenderPngOptions, 'encoding' | 'signal'>;
|
|
15
16
|
loading?: ReactNode;
|
|
16
17
|
}
|
package/src/react/index.js
CHANGED
|
@@ -15,10 +15,10 @@ export function TextGraphProvider({ options = defaultOptions, children }) {
|
|
|
15
15
|
|
|
16
16
|
/** Renders DSL as an image; only the latest committed input may publish a result. */
|
|
17
17
|
export function TextGraph({ source, alt = 'TextGraph diagram', renderOptions = defaultOptions,
|
|
18
|
-
loading = 'Rendering diagram…', ...imageProps }) {
|
|
18
|
+
loading = 'Rendering diagram…', style, ...imageProps }) {
|
|
19
19
|
const shared = useContext(RuntimeContext);
|
|
20
20
|
const resource = useMemo(() => shared ?? createRuntimeResource(defaultOptions), [shared]);
|
|
21
|
-
const { scale, padding, maxWidth } = renderOptions;
|
|
21
|
+
const { scale = 1, padding, maxWidth } = renderOptions;
|
|
22
22
|
const request = useMemo(() => ({ source, scale, padding, maxWidth, resource }),
|
|
23
23
|
[source, scale, padding, maxWidth, resource]);
|
|
24
24
|
const [state, setState] = useState(null);
|
|
@@ -51,6 +51,9 @@ export function TextGraph({ source, alt = 'TextGraph diagram', renderOptions = d
|
|
|
51
51
|
return createElement('span', { role: 'alert' }, message || 'Could not render diagram');
|
|
52
52
|
}
|
|
53
53
|
const result = state.result;
|
|
54
|
-
|
|
54
|
+
// Density improves sharpness without enlarging the diagram. Native dimensions
|
|
55
|
+
// also preserve logical size when maxWidth lowers the actual render density.
|
|
56
|
+
return createElement('img', { width: result.displayWidth ?? result.width / scale, height: result.displayHeight ?? result.height / scale, ...imageProps,
|
|
57
|
+
style: { maxWidth: '100%', ...(imageProps.height == null ? { height: 'auto' } : {}), ...style },
|
|
55
58
|
src: `data:image/png;base64,${result.png}`, alt });
|
|
56
59
|
}
|
package/src/runtime/browser.js
CHANGED
|
@@ -25,17 +25,16 @@ export function createTextGraphRuntimeLoader(options) {
|
|
|
25
25
|
return {
|
|
26
26
|
plan,
|
|
27
27
|
async loadAssets() {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Fetch independent assets together; retain manifest order in the result.
|
|
29
|
+
return new Map(await Promise.all(plan.map(async item => {
|
|
30
30
|
const response = await fetchResource(item.url);
|
|
31
31
|
if (!response.ok) {
|
|
32
32
|
throw new DrawMotiveError('RESOURCE_NOT_FOUND', `Failed to load ${item.asset.path}: HTTP ${response.status}`, {
|
|
33
33
|
details: { asset: item.asset.path, status: response.status, url: item.url.href },
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
return loaded;
|
|
36
|
+
return [item.asset.path, { ...item.asset, url: item.url, bytes: new Uint8Array(await response.arrayBuffer()) }];
|
|
37
|
+
})));
|
|
39
38
|
},
|
|
40
39
|
};
|
|
41
40
|
}
|
package/src/runtime/dotnet.js
CHANGED
|
@@ -5,6 +5,7 @@ import { throwIfAborted } from './lifecycle.js';
|
|
|
5
5
|
import { readVerifiedAsset } from './integrity.js';
|
|
6
6
|
import { createRenderingExecutor } from './render-resources.js';
|
|
7
7
|
import { decodeConfiguration } from './rendering.js';
|
|
8
|
+
import { fontCapability } from './font-resources.js';
|
|
8
9
|
|
|
9
10
|
/** Starts one isolated .NET module graph; platform loaders own all data I/O. */
|
|
10
11
|
export async function startTextGraphRuntime(options, manifest, readAsset) {
|
|
@@ -26,12 +27,12 @@ export async function startTextGraphRuntime(options, manifest, readAsset) {
|
|
|
26
27
|
try {
|
|
27
28
|
// .NET treats download failures as fatal and may terminate a Node host. Own
|
|
28
29
|
// loading/cancellation here, before entering its non-cancellable startup.
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// Data assets are independent: start them together, but verify every response
|
|
31
|
+
// before importing modules or giving .NET ownership of the bytes.
|
|
32
|
+
const data = new Map(await Promise.all(plan.filter(item => !/[.](mjs|js)$/.test(item.asset.path)).map(async item => {
|
|
31
33
|
throwIfAborted(options.signal);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
34
|
+
return [item.asset.path, await readVerifiedAsset(item, options, readAsset)];
|
|
35
|
+
})));
|
|
35
36
|
throwIfAborted(options.signal);
|
|
36
37
|
// Detect unsupported WASM features before the runtime takes ownership of failure handling.
|
|
37
38
|
await WebAssembly.compile(data.get(manifest.runtimeWasm));
|
|
@@ -70,6 +71,7 @@ export async function startTextGraphRuntime(options, manifest, readAsset) {
|
|
|
70
71
|
if (!Array.isArray(info.capabilities) || !info.capabilities.includes(validationCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement validation');
|
|
71
72
|
const renders = manifest.capabilities.includes(renderingCapability);
|
|
72
73
|
if (renders && !info.capabilities.includes(renderingCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement rendering');
|
|
74
|
+
if (manifest.capabilities.includes(fontCapability) && !info.capabilities.includes(fontCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement lazy fonts');
|
|
73
75
|
if (renders && typeof bridge[manifest.bridge.execute] !== 'function') throw new DrawMotiveError('ABI_MISMATCH', 'Rendering bridge export is missing');
|
|
74
76
|
return createBridgeRuntime({ manifest, options, readAsset, bridge, info });
|
|
75
77
|
} catch (cause) {
|
|
@@ -83,7 +85,9 @@ export async function startTextGraphRuntime(options, manifest, readAsset) {
|
|
|
83
85
|
export function createBridgeRuntime({ manifest, options, readAsset, bridge, info }) {
|
|
84
86
|
const renders = manifest.capabilities.includes(renderingCapability);
|
|
85
87
|
return {
|
|
86
|
-
|
|
88
|
+
// A bridge may support operations intentionally omitted by the selected
|
|
89
|
+
// manifest; advertise only the intersection that this loader exposes.
|
|
90
|
+
abiVersion: info.abiVersion, capabilities: info.capabilities.filter(capability => manifest.capabilities.includes(capability)),
|
|
87
91
|
validate: source => bridge[manifest.bridge.validate](source),
|
|
88
92
|
...(renders ? { execute: createRenderingExecutor({ manifest, options, readAsset, execute: request => bridge[manifest.bridge.execute](request) }) } : {}),
|
|
89
93
|
async dispose() {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { DrawMotiveError } from './errors.js';
|
|
2
|
+
import { normalizeFontMetadata } from './language-packs.js';
|
|
3
|
+
import { decodeDiagnostics } from './validation.js';
|
|
4
|
+
import { decodeConfiguration } from './rendering.js';
|
|
5
|
+
import { readVerifiedAsset } from './integrity.js';
|
|
6
|
+
import { throwIfAborted } from './lifecycle.js';
|
|
7
|
+
|
|
8
|
+
export const fontCapability = 'textgraph-fonts-v1';
|
|
9
|
+
const stagingCatalog = 'https://staging.drawmotive.com/static/font-catalog.json';
|
|
10
|
+
|
|
11
|
+
export function fontBase64(bytes) {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for (let offset = 0; offset < bytes.length; offset += 32768) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 32768)));
|
|
14
|
+
return btoa(chunks.join(''));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Catalog paths stay within their deployment directory, independent of the hosting origin. */
|
|
18
|
+
function decodeCatalog(value, url) {
|
|
19
|
+
try {
|
|
20
|
+
if (value?.schemaVersion !== 1 || !Array.isArray(value.fonts) || !value.fonts.length) throw new Error('Invalid font catalog envelope');
|
|
21
|
+
const ids = new Set(), families = new Set(), paths = new Set();
|
|
22
|
+
return value.fonts.map(font => {
|
|
23
|
+
if (!font || typeof font.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(font.id) || ids.has(font.id)
|
|
24
|
+
|| typeof font.family !== 'string' || !font.family.trim() || font.family !== font.family.trim() || families.has(font.family)
|
|
25
|
+
|| typeof font.path !== 'string' || !/^[A-Za-z0-9_./-]+[.](ttf|otf)$/.test(font.path) || font.path.startsWith('/')
|
|
26
|
+
|| font.path.split('/').some(segment => !segment || segment === '.' || segment === '..') || paths.has(font.path) || font.coverage === undefined) throw new Error('Invalid or duplicate font catalog entry');
|
|
27
|
+
ids.add(font.id); families.add(font.family); paths.add(font.path);
|
|
28
|
+
const metadata = normalizeFontMetadata(font);
|
|
29
|
+
const source = new URL(font.path, url);
|
|
30
|
+
// A stable content identity lets HTTP caches distinguish font revisions.
|
|
31
|
+
// File URLs remain plain paths for local/offline hosts.
|
|
32
|
+
if (['http:', 'https:'].includes(source.protocol)) source.searchParams.set('v', metadata.sha256);
|
|
33
|
+
return Object.freeze({ family: font.family, source, ...metadata });
|
|
34
|
+
});
|
|
35
|
+
} catch (cause) {
|
|
36
|
+
throw new DrawMotiveError('INVALID_MANIFEST', 'Invalid font catalog', { cause, details: { url: url.href } });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function decodePreparation(json) {
|
|
41
|
+
try {
|
|
42
|
+
const value = JSON.parse(json);
|
|
43
|
+
if (value?.protocolVersion !== 1 || typeof value.success !== 'boolean') throw new Error('Invalid envelope');
|
|
44
|
+
const diagnostics = decodeDiagnostics(value.diagnostics, ['parse', 'semantic', 'layout', 'render', 'font']);
|
|
45
|
+
if (value.success === diagnostics.some(d => d.severity === 'error')) throw new Error('Inconsistent success');
|
|
46
|
+
if (!Array.isArray(value.fontRuns) || value.fontRuns.some(run => !run || typeof run.text !== 'string' || !run.text
|
|
47
|
+
|| typeof run.missing !== 'boolean' || (run.language !== undefined && run.language !== null && typeof run.language !== 'string'))) throw new Error('Invalid font runs');
|
|
48
|
+
return { ...value, diagnostics };
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
throw new DrawMotiveError('INVALID_RESPONSE', 'Invalid native font preparation response', { cause });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function contains(ranges, point) {
|
|
55
|
+
let low = 0, high = ranges.length - 1;
|
|
56
|
+
while (low <= high) {
|
|
57
|
+
const middle = (low + high) >>> 1, range = ranges[middle];
|
|
58
|
+
if (point < range[0]) high = middle - 1;
|
|
59
|
+
else if (point > range[1]) low = middle + 1;
|
|
60
|
+
else return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function matches(font, run) {
|
|
66
|
+
// Coverage overlap does not grant a language-specific pack authority for another
|
|
67
|
+
// script: loading history must not turn a JP font into implicit Korean fallback.
|
|
68
|
+
if (!run.language || !font.languages.some(language => language.split('-')[0] === run.language.split('-')[0])) return false;
|
|
69
|
+
// Variation selectors and joiners describe shaping; they need no standalone cmap glyph.
|
|
70
|
+
const points = [...run.text].filter(character => !/[\s\p{Default_Ignorable_Code_Point}]/u.test(character)).map(character => character.codePointAt(0));
|
|
71
|
+
return points.length > 0 && points.every(point => contains(font.coverage, point));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Owns optional font I/O inside the instance queue; native owns text, shaping and final coverage. */
|
|
75
|
+
export function createFontPreparation({ options, readAsset, execute }) {
|
|
76
|
+
const packs = options.languagePacks ?? [];
|
|
77
|
+
const fallbackOrder = [...new Set(packs.flatMap(pack => pack.fallbackFamilies))];
|
|
78
|
+
const descriptors = packs.flatMap(pack => pack.fonts);
|
|
79
|
+
const explicit = fallbackOrder.map(family => descriptors.find(font => font.family === family)).filter(font => font?.coverage);
|
|
80
|
+
const installed = new Set(packs.flatMap(pack => pack.fonts).filter(font => !font.coverage).map(font => font.family));
|
|
81
|
+
const settings = options.fontAssets ?? {};
|
|
82
|
+
// A configured package is authoritative, including its failures. Never probe another source silently.
|
|
83
|
+
const catalogUrl = settings.catalog ?? (packs.length ? undefined : settings.fallback === false ? undefined : settings.fallback ?? new URL(stagingCatalog));
|
|
84
|
+
let catalog;
|
|
85
|
+
async function candidates(context) {
|
|
86
|
+
if (!catalogUrl) return explicit;
|
|
87
|
+
if (!catalog) {
|
|
88
|
+
const response = await readAsset({ asset: { path: 'font-catalog.json', mediaType: 'application/json' }, url: catalogUrl }, context);
|
|
89
|
+
throwIfAborted(context.signal);
|
|
90
|
+
let value;
|
|
91
|
+
try { value = await response.json(); }
|
|
92
|
+
catch (cause) { throw new DrawMotiveError('INVALID_MANIFEST', 'Could not decode font catalog', { cause }); }
|
|
93
|
+
const decoded = decodeCatalog(value, catalogUrl);
|
|
94
|
+
throwIfAborted(context.signal);
|
|
95
|
+
const overridden = new Set(packs.flatMap(pack => pack.fonts).map(font => font.family));
|
|
96
|
+
catalog = decoded.filter(font => !overridden.has(font.family));
|
|
97
|
+
}
|
|
98
|
+
return [...explicit, ...catalog];
|
|
99
|
+
}
|
|
100
|
+
return async (request, context) => {
|
|
101
|
+
const render = JSON.parse(request);
|
|
102
|
+
const loading = { ...options, signal: context.signal };
|
|
103
|
+
const attempted = new Set();
|
|
104
|
+
let consumed = false;
|
|
105
|
+
try {
|
|
106
|
+
while (true) {
|
|
107
|
+
throwIfAborted(context.signal);
|
|
108
|
+
const prepared = decodePreparation(await execute(JSON.stringify({ protocolVersion: 1, operation: 'prepare-fonts', source: render.source, ...(render.language === undefined ? {} : { language: render.language }) })));
|
|
109
|
+
throwIfAborted(context.signal);
|
|
110
|
+
if (!prepared.success) return JSON.stringify({ protocolVersion: 1, success: false, diagnostics: prepared.diagnostics });
|
|
111
|
+
const missing = prepared.fontRuns.filter(run => run.missing && run.language);
|
|
112
|
+
if (!missing.length) break;
|
|
113
|
+
const selected = new Set();
|
|
114
|
+
let available = explicit;
|
|
115
|
+
for (const run of missing) {
|
|
116
|
+
const usable = font => !installed.has(font.family) && !attempted.has(font.family) && matches(font, run);
|
|
117
|
+
let candidate = available.find(usable);
|
|
118
|
+
if (!candidate && catalogUrl) {
|
|
119
|
+
available = await candidates(loading);
|
|
120
|
+
candidate = available.find(usable);
|
|
121
|
+
}
|
|
122
|
+
if (candidate) selected.add(candidate);
|
|
123
|
+
}
|
|
124
|
+
if (!selected.size) break;
|
|
125
|
+
const fonts = [];
|
|
126
|
+
for (const font of selected) {
|
|
127
|
+
attempted.add(font.family);
|
|
128
|
+
throwIfAborted(context.signal);
|
|
129
|
+
const bytes = await readVerifiedAsset({ asset: { path: `font:${font.family}`, bytes: font.bytes, sha256: font.sha256, mediaType: 'font/ttf' }, url: font.source }, loading,
|
|
130
|
+
font.source instanceof Uint8Array ? async () => new Response(font.source) : readAsset);
|
|
131
|
+
throwIfAborted(context.signal);
|
|
132
|
+
fonts.push({ family: font.family, data: fontBase64(bytes), languages: font.languages });
|
|
133
|
+
}
|
|
134
|
+
const installedAfter = new Set([...installed, ...fonts.map(font => font.family)]);
|
|
135
|
+
const order = [...fallbackOrder, ...(catalog ?? []).map(font => font.family)].filter(family => installedAfter.has(family));
|
|
136
|
+
decodeConfiguration(await execute(JSON.stringify({ protocolVersion: 1, operation: 'install-fonts', fonts, fallbackFamilies: order })), 'font installation');
|
|
137
|
+
// Native has committed before cancellation can be observed: preserve that installation identity.
|
|
138
|
+
for (const font of fonts) installed.add(font.family);
|
|
139
|
+
}
|
|
140
|
+
throwIfAborted(context.signal);
|
|
141
|
+
const result = await execute(request);
|
|
142
|
+
consumed = true;
|
|
143
|
+
return result;
|
|
144
|
+
} finally {
|
|
145
|
+
if (!consumed) decodeConfiguration(await execute(JSON.stringify({ protocolVersion: 1, operation: 'cancel-prepare' })), 'font preparation cleanup');
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
import { DrawMotiveError } from './errors.js';
|
|
2
2
|
|
|
3
3
|
const defaultFamilies = ['NotoSans-Regular', 'FuzzyBubbles-Regular'];
|
|
4
|
+
const fail = message => { throw new DrawMotiveError('INVALID_ARGUMENT', message); };
|
|
5
|
+
|
|
6
|
+
function copyUrl(value, name) {
|
|
7
|
+
if (!(value instanceof URL) || !['https:', 'http:', 'file:', 'data:', 'blob:'].includes(value.protocol)) fail(`${name} must be a supported URL`);
|
|
8
|
+
return new URL(value.href);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Host configuration selects a source, never probes the network during initialization. */
|
|
12
|
+
export function normalizeFontAssets(value = {}) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail('fontAssets must be an object');
|
|
14
|
+
if (value.fallback !== undefined && value.fallback !== false && !(value.fallback instanceof URL)) fail('fontAssets.fallback must be false or a catalog URL');
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
...(value.catalog === undefined ? {} : { catalog: copyUrl(value.catalog, 'fontAssets.catalog') }),
|
|
17
|
+
...(value.fallback === undefined ? {} : { fallback: value.fallback === false ? false : copyUrl(value.fallback, 'fontAssets.fallback') }),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Official lazy descriptors carry complete immutable coverage and content identity. */
|
|
22
|
+
export function normalizeFontMetadata(font) {
|
|
23
|
+
if (font.coverage === undefined) return {};
|
|
24
|
+
if (!Array.isArray(font.coverage) || !font.coverage.length || !Array.isArray(font.languages) || !font.languages.length
|
|
25
|
+
|| !font.languages.every(language => typeof language === 'string' && /^[a-z]{2,8}(?:-[A-Za-z0-9]+)*$/.test(language))
|
|
26
|
+
|| !Number.isSafeInteger(font.bytes) || font.bytes <= 0 || !/^[a-f0-9]{64}$/.test(font.sha256)) fail('Lazy fonts require coverage, languages, bytes and sha256');
|
|
27
|
+
let end = -1;
|
|
28
|
+
const coverage = font.coverage.map(range => {
|
|
29
|
+
if (!Array.isArray(range) || range.length !== 2 || !range.every(Number.isSafeInteger) || range[0] <= end || range[0] < 0 || range[1] < range[0] || range[1] > 0x10ffff) fail('Font coverage must contain sorted non-overlapping Unicode ranges');
|
|
30
|
+
end = range[1];
|
|
31
|
+
return Object.freeze([...range]);
|
|
32
|
+
});
|
|
33
|
+
return { coverage: Object.freeze(coverage), languages: Object.freeze([...font.languages]), bytes: font.bytes, sha256: font.sha256 };
|
|
34
|
+
}
|
|
4
35
|
|
|
5
36
|
/** Copy caller-owned font descriptors before asynchronous startup without performing font I/O. */
|
|
6
37
|
export function normalizeLanguagePacks(packs = []) {
|
|
@@ -19,7 +50,7 @@ export function normalizeLanguagePacks(packs = []) {
|
|
|
19
50
|
source = new URL(font.source.href);
|
|
20
51
|
} else if (font.source instanceof Uint8Array && font.source.byteLength > 0) source = new Uint8Array(font.source);
|
|
21
52
|
else invalid(`Font ${font.family} source must be a URL or non-empty Uint8Array`);
|
|
22
|
-
return Object.freeze({ family: font.family, source });
|
|
53
|
+
return Object.freeze({ family: font.family, source, ...normalizeFontMetadata(font) });
|
|
23
54
|
});
|
|
24
55
|
const fallbackFamilies = pack.fallbackFamilies ?? [];
|
|
25
56
|
if (!Array.isArray(fallbackFamilies) || !fallbackFamilies.every(family => typeof family === 'string' && family.trim() && family === family.trim())) invalid('fallbackFamilies must be an array of font family names');
|
package/src/runtime/node.js
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
2
4
|
import { startTextGraphRuntime } from './dotnet.js';
|
|
3
5
|
import { DrawMotiveError } from './errors.js';
|
|
4
6
|
|
|
5
7
|
/** Node file loading is isolated from the browser module graph. */
|
|
6
|
-
export function loadNodeRuntime(options, manifest) {
|
|
8
|
+
export async function loadNodeRuntime(options, manifest, directory) {
|
|
9
|
+
if (directory !== undefined) {
|
|
10
|
+
if (!path.isAbsolute(directory)) throw new DrawMotiveError('INVALID_ARGUMENT', 'DRAWMOTIVE_TEXTGRAPH_RUNTIME must be an absolute generated directory');
|
|
11
|
+
const manifestPath = path.join(directory, 'wasm-manifest.json');
|
|
12
|
+
let json;
|
|
13
|
+
try { json = await readFile(manifestPath, { encoding: 'utf8', signal: options.signal }); }
|
|
14
|
+
catch (cause) {
|
|
15
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
16
|
+
throw new DrawMotiveError('RESOURCE_NOT_FOUND', 'Could not read the selected local runtime manifest', { cause, details: { manifest: manifestPath } });
|
|
17
|
+
}
|
|
18
|
+
try { manifest = JSON.parse(json); }
|
|
19
|
+
catch (cause) { throw new DrawMotiveError('INVALID_MANIFEST', 'The selected local runtime manifest is not valid JSON', { cause }); }
|
|
20
|
+
const resolveAsset = options.resolveAsset;
|
|
21
|
+
// Use the selected manifest and directory together for every native module,
|
|
22
|
+
// theme and font. A missing local artifact must never load packaged bytes.
|
|
23
|
+
options = { ...options, resolveAsset: asset => {
|
|
24
|
+
const defaultUrl = pathToFileURL(path.join(directory, asset.path));
|
|
25
|
+
return resolveAsset ? resolveAsset(asset, defaultUrl) : defaultUrl;
|
|
26
|
+
} };
|
|
27
|
+
}
|
|
7
28
|
return startTextGraphRuntime(options, manifest, async (item, context) => {
|
|
8
29
|
try {
|
|
9
30
|
if (item.url.protocol === 'file:') {
|
|
@@ -2,19 +2,18 @@ import { createRuntimeAssetPlan } from './shared.js';
|
|
|
2
2
|
import { readVerifiedAsset } from './integrity.js';
|
|
3
3
|
import { decodeConfiguration } from './rendering.js';
|
|
4
4
|
import { throwIfAborted } from './lifecycle.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const chunks = [];
|
|
8
|
-
for (let offset = 0; offset < bytes.length; offset += 32768) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 32768)));
|
|
9
|
-
return btoa(chunks.join(''));
|
|
10
|
-
}
|
|
5
|
+
import { createFontPreparation, fontBase64 as base64, fontCapability } from './font-resources.js';
|
|
6
|
+
import { DrawMotiveError } from './errors.js';
|
|
11
7
|
|
|
12
8
|
/** Preparation lives inside the instance operation queue; validation never triggers font I/O. */
|
|
13
9
|
export function createRenderingExecutor({ manifest, options, readAsset, execute }) {
|
|
14
10
|
let configured = false;
|
|
11
|
+
const lazy = manifest.capabilities?.includes(fontCapability);
|
|
12
|
+
const prepareFonts = lazy ? createFontPreparation({ options, readAsset, execute }) : undefined;
|
|
15
13
|
return async (request, context = {}) => {
|
|
16
14
|
const loadingOptions = { ...options, signal: context.signal };
|
|
17
15
|
throwIfAborted(context.signal);
|
|
16
|
+
if (!lazy && options.fontAssets?.catalog) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'This runtime does not support lazy font assets');
|
|
18
17
|
if (!configured) {
|
|
19
18
|
const selected = new Set([manifest.rendering.theme, ...manifest.rendering.fonts.map(font => font.asset)]);
|
|
20
19
|
const plan = createRuntimeAssetPlan({ manifest: { assets: manifest.assets.filter(asset => selected.has(asset.path)) }, moduleUrl: import.meta.url, resolveAsset: options.resolveAsset });
|
|
@@ -31,14 +30,16 @@ export function createRenderingExecutor({ manifest, options, readAsset, execute
|
|
|
31
30
|
const fallbackFamilies = [];
|
|
32
31
|
for (const pack of options.languagePacks ?? []) {
|
|
33
32
|
for (const font of pack.fonts) {
|
|
33
|
+
if (lazy && font.coverage) continue;
|
|
34
34
|
throwIfAborted(context.signal);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const item = { asset: { path: `font:${font.family}`, mediaType: 'font/ttf', bytes: font.bytes, sha256: font.sha256 }, url: font.source };
|
|
36
|
+
const bytes = font.coverage
|
|
37
|
+
? await readVerifiedAsset(item, loadingOptions, font.source instanceof Uint8Array ? async () => new Response(font.source) : readAsset)
|
|
38
|
+
: font.source instanceof Uint8Array ? font.source : new Uint8Array(await (await readAsset(item, loadingOptions)).arrayBuffer());
|
|
38
39
|
throwIfAborted(context.signal);
|
|
39
|
-
fonts.push({ family: font.family, data: base64(bytes) });
|
|
40
|
+
fonts.push({ family: font.family, data: base64(bytes), ...(font.languages ? { languages: font.languages } : {}) });
|
|
40
41
|
}
|
|
41
|
-
for (const family of pack.fallbackFamilies) if (!fallbackFamilies.includes(family)) fallbackFamilies.push(family);
|
|
42
|
+
for (const family of pack.fallbackFamilies) if (fonts.some(font => font.family === family) && !fallbackFamilies.includes(family)) fallbackFamilies.push(family);
|
|
42
43
|
}
|
|
43
44
|
throwIfAborted(context.signal);
|
|
44
45
|
decodeConfiguration(await execute(JSON.stringify({ protocolVersion: 1, operation: 'configure', theme, fonts, fallbackFamilies })));
|
|
@@ -46,6 +47,6 @@ export function createRenderingExecutor({ manifest, options, readAsset, execute
|
|
|
46
47
|
configured = true;
|
|
47
48
|
throwIfAborted(context.signal);
|
|
48
49
|
}
|
|
49
|
-
return execute(request);
|
|
50
|
+
return prepareFonts ? prepareFonts(request, context) : execute(request);
|
|
50
51
|
};
|
|
51
52
|
}
|
package/src/runtime/rendering.js
CHANGED
|
@@ -7,14 +7,15 @@ const stages = ['parse', 'semantic', 'layout', 'render', 'font'];
|
|
|
7
7
|
export function normalizeRenderOptions(source, options) {
|
|
8
8
|
const invalid = message => { throw new DrawMotiveError('INVALID_ARGUMENT', message); };
|
|
9
9
|
if (typeof source !== 'string' || !options || typeof options !== 'object' || Array.isArray(options)) invalid('Rendering requires a source string and options object');
|
|
10
|
-
const { encoding = 'bytes', scale =
|
|
10
|
+
const { encoding = 'bytes', scale = 1, padding = 10, maxWidth, signal, language } = options;
|
|
11
|
+
if (language !== undefined && (typeof language !== 'string' || !/^[a-z]{2,8}(?:-[A-Za-z0-9]+)*$/.test(language))) invalid('language must be a language tag');
|
|
11
12
|
if (!['bytes', 'base64'].includes(encoding)) invalid('encoding must be bytes or base64');
|
|
12
13
|
// The native geometry contract uses float32; reject values lost at that boundary.
|
|
13
14
|
if (!Number.isFinite(scale) || !Number.isFinite(Math.fround(scale)) || Math.fround(scale) <= 0) invalid('scale must be positive and representable as a finite float32');
|
|
14
15
|
if (!Number.isFinite(padding) || !Number.isFinite(Math.fround(padding)) || padding < 0) invalid('padding must be non-negative and representable as a finite float32');
|
|
15
16
|
if (maxWidth !== undefined && (!Number.isSafeInteger(maxWidth) || maxWidth <= 0)) invalid('maxWidth must be a positive integer');
|
|
16
17
|
validateSignal(signal);
|
|
17
|
-
return { encoding, signal, request: { protocolVersion: 1, operation: 'render', source, export: { format: 'png', scale, padding, ...(maxWidth === undefined ? {} : { maxWidth }) } } };
|
|
18
|
+
return { encoding, signal, request: { protocolVersion: 1, operation: 'render', source, ...(language === undefined ? {} : { language }), export: { format: 'png', scale, padding, ...(maxWidth === undefined ? {} : { maxWidth }) } } };
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/** Accept canonical base64 only; platform atob implementations otherwise tolerate corruption. */
|
|
@@ -34,17 +35,25 @@ export function decodeRender(json, encoding) {
|
|
|
34
35
|
const diagnostics = decodeDiagnostics(value.diagnostics, stages);
|
|
35
36
|
if (value.success === diagnostics.some(item => item.severity === 'error')) throw new Error('Inconsistent success');
|
|
36
37
|
if (!value.success) {
|
|
37
|
-
if (['png', 'width', 'height'].some(key => Object.hasOwn(value, key))) throw new Error('Failure contains image data');
|
|
38
|
+
if (['png', 'width', 'height', 'displayWidth', 'displayHeight'].some(key => Object.hasOwn(value, key))) throw new Error('Failure contains image data');
|
|
38
39
|
return Object.freeze({ success: false, diagnostics });
|
|
39
40
|
}
|
|
40
41
|
if (![value.width, value.height].every(n => Number.isSafeInteger(n) && n > 0 && n <= 0x7fffffff)) throw new Error('Invalid dimensions');
|
|
42
|
+
// Only the native exporter knows the effective density after maxWidth clamps
|
|
43
|
+
// the raster. Older runtimes may omit this additive pair entirely.
|
|
44
|
+
const displayDimensions = {};
|
|
45
|
+
if (Object.hasOwn(value, 'displayWidth') || Object.hasOwn(value, 'displayHeight')) {
|
|
46
|
+
if (![value.displayWidth, value.displayHeight].every(n => Number.isFinite(n) && n > 0)) throw new Error('Invalid display dimensions');
|
|
47
|
+
displayDimensions.displayWidth = value.displayWidth;
|
|
48
|
+
displayDimensions.displayHeight = value.displayHeight;
|
|
49
|
+
}
|
|
41
50
|
const bytes = decodeBase64(value.png);
|
|
42
51
|
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
43
52
|
if (bytes.length < 33 || !signature.every((byte, index) => bytes[index] === byte)) throw new Error('Invalid PNG signature');
|
|
44
53
|
const header = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
45
54
|
if (header.getUint32(8) !== 13 || header.getUint32(12) !== 0x49484452
|
|
46
55
|
|| header.getUint32(16) !== value.width || header.getUint32(20) !== value.height) throw new Error('PNG dimensions disagree');
|
|
47
|
-
return Object.freeze({ success: true, png: encoding === 'base64' ? value.png : bytes, width: value.width, height: value.height, diagnostics });
|
|
56
|
+
return Object.freeze({ success: true, png: encoding === 'base64' ? value.png : bytes, width: value.width, height: value.height, ...displayDimensions, diagnostics });
|
|
48
57
|
} catch (cause) {
|
|
49
58
|
throw new DrawMotiveError('INVALID_RESPONSE', 'The rendering response violates protocol 1', { cause });
|
|
50
59
|
}
|