@drawmotive/textgraph 0.1.0-alpha.1 → 0.2.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 +55 -7
- package/docs/api-v1.md +11 -4
- package/docs/react.md +55 -0
- package/docs/releases.md +23 -15
- package/generated/wasm/DrawMotive.TextGraph.Bridge.runtimeconfig.json +1 -1
- package/generated/wasm/DrawMotive.TextGraph.Bridge.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.AspNetCore.Components.Web.wasm +0 -0
- package/generated/wasm/Microsoft.Extensions.DependencyInjection.Abstractions.wasm +0 -0
- package/generated/wasm/Microsoft.Extensions.DependencyInjection.wasm +0 -0
- package/generated/wasm/Microsoft.Extensions.Logging.Abstractions.wasm +0 -0
- package/generated/wasm/Microsoft.Extensions.Logging.wasm +0 -0
- package/generated/wasm/Microsoft.Extensions.Options.wasm +0 -0
- package/generated/wasm/Microsoft.Extensions.Primitives.wasm +0 -0
- package/generated/wasm/Polly.Core.wasm +0 -0
- package/generated/wasm/RBush.wasm +0 -0
- package/generated/wasm/SkiaSharp.HarfBuzz.wasm +0 -0
- package/generated/wasm/SkiaSharp.wasm +0 -0
- package/generated/wasm/System.Collections.Concurrent.wasm +0 -0
- package/generated/wasm/System.Collections.Immutable.wasm +0 -0
- package/generated/wasm/System.Collections.NonGeneric.wasm +0 -0
- package/generated/wasm/System.Collections.Specialized.wasm +0 -0
- package/generated/wasm/System.Collections.wasm +0 -0
- package/generated/wasm/System.ComponentModel.Annotations.wasm +0 -0
- package/generated/wasm/System.ComponentModel.Primitives.wasm +0 -0
- package/generated/wasm/System.ComponentModel.TypeConverter.wasm +0 -0
- package/generated/wasm/System.ComponentModel.wasm +0 -0
- package/generated/wasm/System.Console.wasm +0 -0
- package/generated/wasm/System.Diagnostics.StackTrace.wasm +0 -0
- package/generated/wasm/System.Diagnostics.Tracing.wasm +0 -0
- package/generated/wasm/System.IO.Compression.Brotli.wasm +0 -0
- package/generated/wasm/System.IO.Compression.wasm +0 -0
- package/generated/wasm/System.IO.Pipelines.wasm +0 -0
- package/generated/wasm/System.Linq.Expressions.wasm +0 -0
- package/generated/wasm/System.Linq.wasm +0 -0
- package/generated/wasm/System.Memory.wasm +0 -0
- package/generated/wasm/System.Net.Http.wasm +0 -0
- package/generated/wasm/System.Net.Primitives.wasm +0 -0
- package/generated/wasm/System.Numerics.Vectors.wasm +0 -0
- package/generated/wasm/System.ObjectModel.wasm +0 -0
- package/generated/wasm/System.Private.CoreLib.wasm +0 -0
- package/generated/wasm/System.Private.Uri.wasm +0 -0
- package/generated/wasm/System.Reflection.Emit.ILGeneration.wasm +0 -0
- package/generated/wasm/System.Reflection.Emit.wasm +0 -0
- package/generated/wasm/System.Reflection.Primitives.wasm +0 -0
- package/generated/wasm/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/generated/wasm/System.Runtime.InteropServices.wasm +0 -0
- package/generated/wasm/System.Runtime.Intrinsics.wasm +0 -0
- package/generated/wasm/System.Runtime.Loader.wasm +0 -0
- package/generated/wasm/System.Runtime.Numerics.wasm +0 -0
- package/generated/wasm/System.Runtime.Serialization.Primitives.wasm +0 -0
- package/generated/wasm/System.Runtime.wasm +0 -0
- package/generated/wasm/System.Security.Cryptography.wasm +0 -0
- package/generated/wasm/System.Text.Encoding.Extensions.wasm +0 -0
- package/generated/wasm/System.Text.Encodings.Web.wasm +0 -0
- package/generated/wasm/System.Text.Json.wasm +0 -0
- package/generated/wasm/System.Text.RegularExpressions.wasm +0 -0
- package/generated/wasm/System.Threading.Channels.wasm +0 -0
- package/generated/wasm/System.Threading.Thread.wasm +0 -0
- package/generated/wasm/System.Threading.ThreadPool.wasm +0 -0
- package/generated/wasm/System.Threading.wasm +0 -0
- package/generated/wasm/System.wasm +0 -0
- package/generated/wasm/dotnet.boot.js +63 -63
- package/generated/wasm/dotnet.js +1 -1
- package/generated/wasm/dotnet.native.js +745 -22
- package/generated/wasm/dotnet.native.wasm +0 -0
- package/generated/wasm/dotnet.runtime.js +1 -1
- package/generated/wasm/netstandard.wasm +0 -0
- package/generated/wasm-manifest.js +78 -78
- package/generated/wasm-manifest.json +78 -78
- package/package.json +39 -7
- package/schemas/render.schema.json +33 -1
- package/scripts/copy-assets.mjs +49 -0
- package/src/index.d.ts +4 -2
- package/src/platform/node.d.ts +2 -0
- package/src/platform/node.js +4 -1
- package/src/react/index.d.ts +20 -0
- package/src/react/index.js +59 -0
- package/src/react/runtime.js +37 -0
- package/src/runtime/dotnet.js +3 -1
- package/src/runtime/node.js +22 -1
- package/src/runtime/rendering.js +11 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cp, mkdir, realpath } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const source = fileURLToPath(new URL('../generated/wasm/', import.meta.url));
|
|
7
|
+
|
|
8
|
+
/** Resolve existing ancestors too, so an output symlink cannot point into the installed runtime. */
|
|
9
|
+
async function canonicalPath(directory) {
|
|
10
|
+
try {
|
|
11
|
+
return await realpath(directory);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error.code !== 'ENOENT') throw error;
|
|
14
|
+
return path.join(await canonicalPath(path.dirname(directory)), path.basename(directory));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function contains(parent, child) {
|
|
19
|
+
const relative = path.relative(parent, child);
|
|
20
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Copy the complete installed runtime, including scripts, fonts and licenses, without deleting output. */
|
|
24
|
+
export async function copyRuntimeAssets(destination) {
|
|
25
|
+
if (typeof destination !== 'string' || destination.trim() === '') {
|
|
26
|
+
throw new Error('Usage: textgraph-copy-assets <destination>');
|
|
27
|
+
}
|
|
28
|
+
const runtime = await realpath(source);
|
|
29
|
+
const output = await canonicalPath(path.resolve(destination));
|
|
30
|
+
if (output === path.parse(output).root || contains(runtime, output) || contains(output, runtime)) {
|
|
31
|
+
throw new Error('Choose a destination outside the installed runtime and below a static asset directory.');
|
|
32
|
+
}
|
|
33
|
+
await mkdir(output, { recursive: true });
|
|
34
|
+
await cp(runtime, output, { recursive: true, force: true });
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Canonicalize both paths: Windows short temp paths and directory aliases can
|
|
39
|
+
// give argv and the ESM loader different spellings for the same executable.
|
|
40
|
+
if (process.argv[1] && await realpath(process.argv[1]) === await realpath(fileURLToPath(import.meta.url))) {
|
|
41
|
+
try {
|
|
42
|
+
const [destination, ...extra] = process.argv.slice(2);
|
|
43
|
+
if (extra.length) throw new Error('Usage: textgraph-copy-assets <destination>');
|
|
44
|
+
console.log(`TextGraph runtime copied to ${await copyRuntimeAssets(destination)}`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(error.message);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -114,7 +114,7 @@ export type TextGraphPngEncoding = 'bytes' | 'base64';
|
|
|
114
114
|
export interface TextGraphRenderPngOptions<Encoding extends TextGraphPngEncoding = TextGraphPngEncoding> {
|
|
115
115
|
/** Defaults to bytes. Base64 contains no data-URL prefix. */
|
|
116
116
|
encoding?: Encoding;
|
|
117
|
-
/** Positive
|
|
117
|
+
/** Positive raster density relative to logical diagram size. Defaults to 1. */
|
|
118
118
|
scale?: number;
|
|
119
119
|
/** Non-negative padding in diagram units. Defaults to 10. */
|
|
120
120
|
padding?: number;
|
|
@@ -125,7 +125,9 @@ export interface TextGraphRenderPngOptions<Encoding extends TextGraphPngEncoding
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export type TextGraphRenderPngResult<Png extends Uint8Array | string = Uint8Array | string> =
|
|
128
|
-
| { readonly success: true; readonly png: Png; readonly width: number; readonly height: number;
|
|
128
|
+
| { readonly success: true; readonly png: Png; readonly width: number; readonly height: number;
|
|
129
|
+
/** Logical display dimensions before density scaling; omitted by older runtimes. */
|
|
130
|
+
readonly displayWidth?: number; readonly displayHeight?: number; readonly diagnostics: readonly TextGraphRenderDiagnostic[] }
|
|
129
131
|
| { readonly success: false; readonly diagnostics: readonly TextGraphRenderDiagnostic[] };
|
|
130
132
|
|
|
131
133
|
export interface TextGraphRuntimeInfo extends TextGraphRuntimeManifest {
|
package/src/platform/node.d.ts
CHANGED
package/src/platform/node.js
CHANGED
|
@@ -6,7 +6,10 @@ import { DrawMotiveError } from '../runtime/errors.js';
|
|
|
6
6
|
|
|
7
7
|
export async function initializeTextGraph(options = {}) {
|
|
8
8
|
if (!options || typeof options !== 'object') throw new DrawMotiveError('INVALID_ARGUMENT', 'Initialization options must be an object');
|
|
9
|
-
|
|
9
|
+
// The workspace development command opts in explicitly; imports and browser
|
|
10
|
+
// loaders never consult a local build directory or replace packaged assets.
|
|
11
|
+
const directory = process.env.DRAWMOTIVE_TEXTGRAPH_RUNTIME;
|
|
12
|
+
return initialize({ ...options, loadRuntime: options.loadRuntime ?? (context => loadNodeRuntime(context, manifest, directory)) });
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export const platform = 'node';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ImgHTMLAttributes, ReactElement, ReactNode } from 'react';
|
|
2
|
+
import type { TextGraphInitializeOptions, TextGraphRenderPngOptions } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/** Keep options stable (module constant or useMemo); replacing them replaces the runtime. */
|
|
5
|
+
export interface TextGraphProviderProps {
|
|
6
|
+
options?: TextGraphInitializeOptions;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The component owns its image source and cancellation; other image attributes pass through. */
|
|
11
|
+
export interface TextGraphProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet' | 'children' | 'loading'> {
|
|
12
|
+
source: string;
|
|
13
|
+
alt?: string;
|
|
14
|
+
/** Defaults to scale 1; the image displays at logical size. */
|
|
15
|
+
renderOptions?: Omit<TextGraphRenderPngOptions, 'encoding' | 'signal'>;
|
|
16
|
+
loading?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export declare function TextGraphProvider(props: TextGraphProviderProps): ReactElement;
|
|
20
|
+
export declare function TextGraph(props: TextGraphProps): ReactElement;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, createElement, useContext, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { createRuntimeResource } from './runtime.js';
|
|
5
|
+
|
|
6
|
+
const RuntimeContext = createContext(null);
|
|
7
|
+
const defaultOptions = Object.freeze({});
|
|
8
|
+
|
|
9
|
+
/** Shares a lazy browser runtime across diagrams and owns its eventual disposal. */
|
|
10
|
+
export function TextGraphProvider({ options = defaultOptions, children }) {
|
|
11
|
+
const resource = useMemo(() => createRuntimeResource(options), [options]);
|
|
12
|
+
useEffect(() => resource.retain(), [resource]);
|
|
13
|
+
return createElement(RuntimeContext.Provider, { value: resource }, children);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Renders DSL as an image; only the latest committed input may publish a result. */
|
|
17
|
+
export function TextGraph({ source, alt = 'TextGraph diagram', renderOptions = defaultOptions,
|
|
18
|
+
loading = 'Rendering diagram…', style, ...imageProps }) {
|
|
19
|
+
const shared = useContext(RuntimeContext);
|
|
20
|
+
const resource = useMemo(() => shared ?? createRuntimeResource(defaultOptions), [shared]);
|
|
21
|
+
const { scale = 1, padding, maxWidth } = renderOptions;
|
|
22
|
+
const request = useMemo(() => ({ source, scale, padding, maxWidth, resource }),
|
|
23
|
+
[source, scale, padding, maxWidth, resource]);
|
|
24
|
+
const [state, setState] = useState(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const release = resource.retain();
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
void (async () => {
|
|
30
|
+
try {
|
|
31
|
+
const runtime = await resource.get();
|
|
32
|
+
if (controller.signal.aborted) return;
|
|
33
|
+
const result = await runtime.renderPng(source, { scale, padding, maxWidth, encoding: 'base64', signal: controller.signal });
|
|
34
|
+
if (!controller.signal.aborted) setState({ request, result });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!controller.signal.aborted) {
|
|
37
|
+
// Host loaders may reject arbitrary values, including null. Keep the
|
|
38
|
+
// failure branch explicit so reporting an error cannot break React render.
|
|
39
|
+
setState({ request, error: error instanceof Error ? error : new Error(String(error ?? 'Could not render diagram')) });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
return () => { controller.abort(); release(); };
|
|
44
|
+
}, [request]);
|
|
45
|
+
|
|
46
|
+
// State is keyed by the whole request so old images disappear immediately,
|
|
47
|
+
// even before passive effects clean up the previous asynchronous render.
|
|
48
|
+
if (state?.request !== request) return createElement('span', { role: 'status' }, loading);
|
|
49
|
+
if (state.error || !state.result.success) {
|
|
50
|
+
const message = state.error?.message ?? state.result.diagnostics.map(item => item.message).join('\n');
|
|
51
|
+
return createElement('span', { role: 'alert' }, message || 'Could not render diagram');
|
|
52
|
+
}
|
|
53
|
+
const result = state.result;
|
|
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 },
|
|
58
|
+
src: `data:image/png;base64,${result.png}`, alt });
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { initializeTextGraph } from '../platform/browser.js';
|
|
2
|
+
|
|
3
|
+
/** A provider owns one lazy runtime; retaining it has no I/O during React render. */
|
|
4
|
+
export function createRuntimeResource(options) {
|
|
5
|
+
let users = 0;
|
|
6
|
+
let pending;
|
|
7
|
+
return {
|
|
8
|
+
retain() {
|
|
9
|
+
users++;
|
|
10
|
+
let released = false;
|
|
11
|
+
return () => {
|
|
12
|
+
if (released) return;
|
|
13
|
+
released = true;
|
|
14
|
+
users--;
|
|
15
|
+
// StrictMode replays effects synchronously. Defer final release so that
|
|
16
|
+
// replay reuses the same .NET module graph instead of starting another.
|
|
17
|
+
queueMicrotask(() => {
|
|
18
|
+
if (users !== 0 || !pending) return;
|
|
19
|
+
const retiring = pending;
|
|
20
|
+
pending = undefined;
|
|
21
|
+
// Startup may finish after unmount. Dispose that instance as well;
|
|
22
|
+
// there is no mounted view to receive startup or cleanup errors.
|
|
23
|
+
void retiring.then(runtime => runtime.dispose()).catch(() => undefined);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
get() {
|
|
28
|
+
if (!pending) {
|
|
29
|
+
const attempt = initializeTextGraph(options);
|
|
30
|
+
pending = attempt;
|
|
31
|
+
// A later input change may retry a failed startup.
|
|
32
|
+
void attempt.catch(() => { if (pending === attempt) pending = undefined; });
|
|
33
|
+
}
|
|
34
|
+
return pending;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/runtime/dotnet.js
CHANGED
|
@@ -83,7 +83,9 @@ export async function startTextGraphRuntime(options, manifest, readAsset) {
|
|
|
83
83
|
export function createBridgeRuntime({ manifest, options, readAsset, bridge, info }) {
|
|
84
84
|
const renders = manifest.capabilities.includes(renderingCapability);
|
|
85
85
|
return {
|
|
86
|
-
|
|
86
|
+
// A bridge may support operations intentionally omitted by the selected
|
|
87
|
+
// manifest; advertise only the intersection that this loader exposes.
|
|
88
|
+
abiVersion: info.abiVersion, capabilities: info.capabilities.filter(capability => manifest.capabilities.includes(capability)),
|
|
87
89
|
validate: source => bridge[manifest.bridge.validate](source),
|
|
88
90
|
...(renders ? { execute: createRenderingExecutor({ manifest, options, readAsset, execute: request => bridge[manifest.bridge.execute](request) }) } : {}),
|
|
89
91
|
async dispose() {
|
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:') {
|
package/src/runtime/rendering.js
CHANGED
|
@@ -7,7 +7,7 @@ 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 } = options;
|
|
11
11
|
if (!['bytes', 'base64'].includes(encoding)) invalid('encoding must be bytes or base64');
|
|
12
12
|
// The native geometry contract uses float32; reject values lost at that boundary.
|
|
13
13
|
if (!Number.isFinite(scale) || !Number.isFinite(Math.fround(scale)) || Math.fround(scale) <= 0) invalid('scale must be positive and representable as a finite float32');
|
|
@@ -34,17 +34,25 @@ export function decodeRender(json, encoding) {
|
|
|
34
34
|
const diagnostics = decodeDiagnostics(value.diagnostics, stages);
|
|
35
35
|
if (value.success === diagnostics.some(item => item.severity === 'error')) throw new Error('Inconsistent success');
|
|
36
36
|
if (!value.success) {
|
|
37
|
-
if (['png', 'width', 'height'].some(key => Object.hasOwn(value, key))) throw new Error('Failure contains image data');
|
|
37
|
+
if (['png', 'width', 'height', 'displayWidth', 'displayHeight'].some(key => Object.hasOwn(value, key))) throw new Error('Failure contains image data');
|
|
38
38
|
return Object.freeze({ success: false, diagnostics });
|
|
39
39
|
}
|
|
40
40
|
if (![value.width, value.height].every(n => Number.isSafeInteger(n) && n > 0 && n <= 0x7fffffff)) throw new Error('Invalid dimensions');
|
|
41
|
+
// Only the native exporter knows the effective density after maxWidth clamps
|
|
42
|
+
// the raster. Older runtimes may omit this additive pair entirely.
|
|
43
|
+
const displayDimensions = {};
|
|
44
|
+
if (Object.hasOwn(value, 'displayWidth') || Object.hasOwn(value, 'displayHeight')) {
|
|
45
|
+
if (![value.displayWidth, value.displayHeight].every(n => Number.isFinite(n) && n > 0)) throw new Error('Invalid display dimensions');
|
|
46
|
+
displayDimensions.displayWidth = value.displayWidth;
|
|
47
|
+
displayDimensions.displayHeight = value.displayHeight;
|
|
48
|
+
}
|
|
41
49
|
const bytes = decodeBase64(value.png);
|
|
42
50
|
const signature = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
43
51
|
if (bytes.length < 33 || !signature.every((byte, index) => bytes[index] === byte)) throw new Error('Invalid PNG signature');
|
|
44
52
|
const header = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
45
53
|
if (header.getUint32(8) !== 13 || header.getUint32(12) !== 0x49484452
|
|
46
54
|
|| 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 });
|
|
55
|
+
return Object.freeze({ success: true, png: encoding === 'base64' ? value.png : bytes, width: value.width, height: value.height, ...displayDimensions, diagnostics });
|
|
48
56
|
} catch (cause) {
|
|
49
57
|
throw new DrawMotiveError('INVALID_RESPONSE', 'The rendering response violates protocol 1', { cause });
|
|
50
58
|
}
|