dwf-viewer 0.5.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 (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +235 -0
  3. package/NOTICE +10 -0
  4. package/PRODUCTION_3D_NOTES.md +48 -0
  5. package/README.md +203 -0
  6. package/dist/format/document.d.ts +186 -0
  7. package/dist/format/document.js +9 -0
  8. package/dist/format/dwf.d.ts +4 -0
  9. package/dist/format/dwf.js +372 -0
  10. package/dist/format/dwfx.d.ts +6 -0
  11. package/dist/format/dwfx.js +425 -0
  12. package/dist/format/emodelMetadata.d.ts +10 -0
  13. package/dist/format/emodelMetadata.js +368 -0
  14. package/dist/format/inflate.d.ts +4 -0
  15. package/dist/format/inflate.js +28 -0
  16. package/dist/format/opc.d.ts +28 -0
  17. package/dist/format/opc.js +85 -0
  18. package/dist/format/open.d.ts +3 -0
  19. package/dist/format/open.js +69 -0
  20. package/dist/format/types.d.ts +61 -0
  21. package/dist/format/types.js +6 -0
  22. package/dist/format/util.d.ts +18 -0
  23. package/dist/format/util.js +324 -0
  24. package/dist/format/w2dBinary.d.ts +19 -0
  25. package/dist/format/w2dBinary.js +629 -0
  26. package/dist/format/w2dText.d.ts +13 -0
  27. package/dist/format/w2dText.js +166 -0
  28. package/dist/format/w3d.d.ts +8 -0
  29. package/dist/format/w3d.js +826 -0
  30. package/dist/format/zip.d.ts +30 -0
  31. package/dist/format/zip.js +141 -0
  32. package/dist/index.d.ts +12 -0
  33. package/dist/index.js +9 -0
  34. package/dist/render/PageRenderer.d.ts +27 -0
  35. package/dist/render/PageRenderer.js +92 -0
  36. package/dist/render/ThreeJsSceneAdapter.d.ts +29 -0
  37. package/dist/render/ThreeJsSceneAdapter.js +52 -0
  38. package/dist/render/ThreeW3dRenderer.d.ts +24 -0
  39. package/dist/render/ThreeW3dRenderer.js +372 -0
  40. package/dist/render/W2dRenderer.d.ts +24 -0
  41. package/dist/render/W2dRenderer.js +198 -0
  42. package/dist/render/WebGlW2dBackend.d.ts +38 -0
  43. package/dist/render/WebGlW2dBackend.js +400 -0
  44. package/dist/render/XpsRenderer.d.ts +20 -0
  45. package/dist/render/XpsRenderer.js +310 -0
  46. package/dist/render/style.d.ts +16 -0
  47. package/dist/render/style.js +115 -0
  48. package/dist/render/viewport.d.ts +16 -0
  49. package/dist/render/viewport.js +27 -0
  50. package/dist/render/xpsPath.d.ts +41 -0
  51. package/dist/render/xpsPath.js +335 -0
  52. package/dist/viewer/DwfViewer.d.ts +69 -0
  53. package/dist/viewer/DwfViewer.js +386 -0
  54. package/dist/wasm/WasmRasterBackend.d.ts +21 -0
  55. package/dist/wasm/WasmRasterBackend.js +84 -0
  56. package/package.json +91 -0
  57. package/public/dwfv-render.wasm +0 -0
  58. package/styles/dwf-viewer.css +51 -0
@@ -0,0 +1,310 @@
1
+ import { actionableDiagnostics, diag } from '../format/types.js';
2
+ import { childElements, getAttr, localName, parseNumberList, parseXml, resolvePart, blobToImage, mimeFromPath } from '../format/util.js';
3
+ import { applyPathToCanvas, flattenPath, parsePathData } from './xpsPath.js';
4
+ import { multiplyMatrix, parseBrushColor, parseMatrix } from './style.js';
5
+ import { fitPageMatrix } from './viewport.js';
6
+ import { WasmRasterBackend } from '../wasm/WasmRasterBackend.js';
7
+ export class XpsRenderer {
8
+ constructor(document) {
9
+ this.document = document;
10
+ }
11
+ async render(page, canvas, options = {}) {
12
+ const opc = this.document.opc;
13
+ if (!opc)
14
+ throw new Error('XPS page requires an OPC package view.');
15
+ const warnings = actionableDiagnostics(page.diagnostics);
16
+ const xml = await opc.readText(page.sourcePath);
17
+ const doc = parseXml(xml, page.sourcePath);
18
+ const root = doc.documentElement;
19
+ const ctx = canvas.getContext('2d');
20
+ if (!ctx)
21
+ throw new Error('CanvasRenderingContext2D is not available.');
22
+ const bg = options.background ?? '#ffffff';
23
+ const pageMatrix = fitPageMatrix({ canvasWidth: canvas.width, canvasHeight: canvas.height, pageWidth: page.width, pageHeight: page.height, zoom: options.zoom, panX: options.panX, panY: options.panY });
24
+ let commands = 0;
25
+ if (options.preferWasm) {
26
+ try {
27
+ this.wasm ?? (this.wasm = new WasmRasterBackend({ wasmUrl: options.wasmUrl }));
28
+ await this.wasm.init();
29
+ this.wasm.begin(canvas.width, canvas.height, bg);
30
+ commands += this.renderElementToWasm(root, pageMatrix, 1, warnings);
31
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
32
+ ctx.putImageData(this.wasm.toImageData(), 0, 0);
33
+ commands += await this.renderElementToCanvas(root, ctx, page.sourcePath, pageMatrix, 1, warnings, { vectors: false, overlays: true });
34
+ return { backend: 'wasm-raster', commands, warnings };
35
+ }
36
+ catch (err) {
37
+ warnings.push(diag('warning', 'WASM_BACKEND_FALLBACK', `WASM raster path failed, falling back to Canvas2D: ${String(err)}`));
38
+ }
39
+ }
40
+ ctx.save();
41
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
42
+ ctx.fillStyle = bg;
43
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
44
+ ctx.restore();
45
+ commands += await this.renderElementToCanvas(root, ctx, page.sourcePath, pageMatrix, 1, warnings, { vectors: true, overlays: true });
46
+ return { backend: 'canvas2d', commands, warnings };
47
+ }
48
+ async renderElementToCanvas(el, ctx, pagePath, matrix, opacity, warnings, mode) {
49
+ const name = localName(el);
50
+ const local = elementMatrix(el);
51
+ const composed = multiplyMatrix(matrix, local);
52
+ const ownOpacity = opacity * parseOpacity(getAttr(el, 'Opacity'));
53
+ let commands = 0;
54
+ if (name === 'Path' && mode.vectors) {
55
+ const path = extractPathCommands(el);
56
+ if (path.length > 0) {
57
+ ctx.save();
58
+ ctx.setTransform(composed.a, composed.b, composed.c, composed.d, composed.e, composed.f);
59
+ ctx.globalAlpha = ownOpacity;
60
+ const clip = getAttr(el, 'Clip');
61
+ if (clip) {
62
+ ctx.beginPath();
63
+ applyPathToCanvas(ctx, parsePathData(clip));
64
+ ctx.clip();
65
+ }
66
+ ctx.beginPath();
67
+ applyPathToCanvas(ctx, path);
68
+ const fill = extractBrush(el, 'Fill', ownOpacity);
69
+ const stroke = extractBrush(el, 'Stroke', ownOpacity);
70
+ const thickness = Number(getAttr(el, 'StrokeThickness') ?? 1);
71
+ if (fill) {
72
+ ctx.fillStyle = fill;
73
+ ctx.fill(fillRule(el));
74
+ }
75
+ if (stroke && thickness > 0) {
76
+ ctx.strokeStyle = stroke;
77
+ ctx.lineWidth = thickness;
78
+ ctx.stroke();
79
+ }
80
+ ctx.restore();
81
+ commands++;
82
+ }
83
+ }
84
+ else if (name === 'Glyphs' && mode.overlays) {
85
+ ctx.save();
86
+ ctx.setTransform(composed.a, composed.b, composed.c, composed.d, composed.e, composed.f);
87
+ ctx.globalAlpha = ownOpacity;
88
+ const text = getAttr(el, 'UnicodeString') ?? '';
89
+ const x = Number(getAttr(el, 'OriginX') ?? 0);
90
+ const y = Number(getAttr(el, 'OriginY') ?? 0);
91
+ const size = Number(getAttr(el, 'FontRenderingEmSize') ?? 12);
92
+ const fill = extractBrush(el, 'Fill', ownOpacity) ?? '#000000';
93
+ ctx.fillStyle = fill;
94
+ ctx.font = `${size}px sans-serif`;
95
+ ctx.fillText(text, x, y);
96
+ ctx.restore();
97
+ commands++;
98
+ }
99
+ else if (name === 'Image' && mode.overlays) {
100
+ const source = getAttr(el, 'Source') ?? getAttr(el, 'ImageSource');
101
+ if (source) {
102
+ try {
103
+ await this.drawImageResource(ctx, pagePath, source, composed, ownOpacity, el);
104
+ commands++;
105
+ }
106
+ catch (err) {
107
+ warnings.push(diag('warning', 'XPS_IMAGE_DRAW_FAILED', `Failed to draw image ${source}: ${String(err)}`, pagePath));
108
+ }
109
+ }
110
+ }
111
+ else if (name === 'Canvas' || name === 'FixedPage' || name.endsWith('.RenderTransform') || name.endsWith('.Resources')) {
112
+ // Container or non-rendering property element.
113
+ }
114
+ // Path.Fill with ImageBrush: draw as overlay clipped to path.
115
+ if (name === 'Path' && mode.overlays) {
116
+ const imageBrush = findPropertyBrush(el, 'Fill', 'ImageBrush');
117
+ const imageSource = imageBrush ? getAttr(imageBrush, 'ImageSource') : undefined;
118
+ if (imageBrush && imageSource) {
119
+ try {
120
+ await this.drawImageBrush(ctx, pagePath, imageSource, composed, ownOpacity, el, imageBrush);
121
+ commands++;
122
+ }
123
+ catch (err) {
124
+ warnings.push(diag('warning', 'XPS_IMAGEBRUSH_FAILED', `Failed to draw ImageBrush ${imageSource}: ${String(err)}`, pagePath));
125
+ }
126
+ }
127
+ }
128
+ for (const child of childElements(el)) {
129
+ const childName = localName(child);
130
+ if (childName.includes('.'))
131
+ continue;
132
+ commands += await this.renderElementToCanvas(child, ctx, pagePath, composed, ownOpacity, warnings, mode);
133
+ }
134
+ return commands;
135
+ }
136
+ renderElementToWasm(el, matrix, opacity, warnings) {
137
+ if (!this.wasm)
138
+ return 0;
139
+ const name = localName(el);
140
+ const local = elementMatrix(el);
141
+ const composed = multiplyMatrix(matrix, local);
142
+ const ownOpacity = opacity * parseOpacity(getAttr(el, 'Opacity'));
143
+ let commands = 0;
144
+ if (name === 'Path') {
145
+ const path = extractPathCommands(el);
146
+ if (path.length > 0) {
147
+ const fill = extractBrush(el, 'Fill', ownOpacity);
148
+ const stroke = extractBrush(el, 'Stroke', ownOpacity);
149
+ const thickness = Number(getAttr(el, 'StrokeThickness') ?? 1);
150
+ const subs = flattenPath(path, 0.5);
151
+ if (fill) {
152
+ for (const sub of subs)
153
+ if (sub.closed || sub.points.length >= 6)
154
+ this.wasm.drawPolygon(sub.points, composed, fill);
155
+ }
156
+ if (stroke && thickness > 0) {
157
+ for (const sub of subs)
158
+ this.wasm.drawPolyline(sub.points, composed, stroke, thickness * composed.a);
159
+ }
160
+ commands++;
161
+ }
162
+ }
163
+ for (const child of childElements(el)) {
164
+ const childName = localName(child);
165
+ if (childName.includes('.'))
166
+ continue;
167
+ commands += this.renderElementToWasm(child, composed, ownOpacity, warnings);
168
+ }
169
+ return commands;
170
+ }
171
+ async drawImageResource(ctx, pagePath, source, matrix, opacity, el) {
172
+ const opc = this.document.opc;
173
+ const src = resolvePart(pagePath, source.replace(/^\//, ''));
174
+ const bytes = await opc.readBytes(src);
175
+ const image = await blobToImage(bytes, opc.getContentType(src) ?? mimeFromPath(src) ?? 'image/png');
176
+ const width = Number(getAttr(el, 'Width') ?? ('width' in image ? image.width : 0));
177
+ const height = Number(getAttr(el, 'Height') ?? ('height' in image ? image.height : 0));
178
+ const x = Number(getAttr(el, 'Canvas.Left') ?? getAttr(el, 'X') ?? 0);
179
+ const y = Number(getAttr(el, 'Canvas.Top') ?? getAttr(el, 'Y') ?? 0);
180
+ ctx.save();
181
+ ctx.setTransform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
182
+ ctx.globalAlpha = opacity;
183
+ ctx.drawImage(image, x, y, width || image.width, height || image.height);
184
+ ctx.restore();
185
+ }
186
+ async drawImageBrush(ctx, pagePath, source, matrix, opacity, pathEl, brushEl) {
187
+ const opc = this.document.opc;
188
+ const src = resolvePart(pagePath, source.replace(/^\//, ''));
189
+ const bytes = await opc.readBytes(src);
190
+ const image = await blobToImage(bytes, opc.getContentType(src) ?? mimeFromPath(src) ?? 'image/png');
191
+ const viewport = parseRect(getAttr(brushEl, 'Viewport')) ?? parseRect(getAttr(brushEl, 'Viewbox')) ?? [0, 0, Number(image.width ?? 1), Number(image.height ?? 1)];
192
+ const path = extractPathCommands(pathEl);
193
+ ctx.save();
194
+ ctx.setTransform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
195
+ ctx.globalAlpha = opacity;
196
+ if (path.length > 0) {
197
+ ctx.beginPath();
198
+ applyPathToCanvas(ctx, path);
199
+ ctx.clip();
200
+ }
201
+ ctx.drawImage(image, viewport[0], viewport[1], viewport[2], viewport[3]);
202
+ ctx.restore();
203
+ }
204
+ }
205
+ function elementMatrix(el) {
206
+ let m = parseMatrix(getAttr(el, 'RenderTransform') ?? getAttr(el, 'Transform'));
207
+ for (const child of childElements(el)) {
208
+ const name = localName(child);
209
+ if (name.endsWith('.RenderTransform') || name.endsWith('.Transform')) {
210
+ const matrixEl = childElements(child).find(c => localName(c) === 'MatrixTransform');
211
+ if (matrixEl)
212
+ m = multiplyMatrix(m, parseMatrix(getAttr(matrixEl, 'Matrix')));
213
+ }
214
+ }
215
+ const left = Number(getAttr(el, 'Canvas.Left') ?? 0);
216
+ const top = Number(getAttr(el, 'Canvas.Top') ?? 0);
217
+ if (left || top)
218
+ m = multiplyMatrix({ a: 1, b: 0, c: 0, d: 1, e: left, f: top }, m);
219
+ return m;
220
+ }
221
+ function extractPathCommands(pathEl) {
222
+ const data = getAttr(pathEl, 'Data');
223
+ if (data)
224
+ return parsePathData(data);
225
+ for (const prop of childElements(pathEl)) {
226
+ if (localName(prop) !== 'Path.Data')
227
+ continue;
228
+ for (const geom of childElements(prop)) {
229
+ const figures = getAttr(geom, 'Figures');
230
+ if (figures)
231
+ return parsePathData(figures);
232
+ if (localName(geom) === 'PathGeometry') {
233
+ const built = buildPathGeometry(geom);
234
+ if (built)
235
+ return parsePathData(built);
236
+ }
237
+ }
238
+ }
239
+ return [];
240
+ }
241
+ function buildPathGeometry(geom) {
242
+ const parts = [];
243
+ for (const figure of childElements(geom).filter(e => localName(e) === 'PathFigure')) {
244
+ const start = parseNumberList(getAttr(figure, 'StartPoint') ?? '');
245
+ if (start.length >= 2)
246
+ parts.push(`M ${start[0]} ${start[1]}`);
247
+ for (const seg of childElements(figure)) {
248
+ const name = localName(seg);
249
+ if (name === 'LineSegment') {
250
+ const p = parseNumberList(getAttr(seg, 'Point') ?? '');
251
+ if (p.length >= 2)
252
+ parts.push(`L ${p[0]} ${p[1]}`);
253
+ }
254
+ else if (name === 'PolyLineSegment') {
255
+ const nums = parseNumberList(getAttr(seg, 'Points') ?? '');
256
+ for (let i = 0; i + 1 < nums.length; i += 2)
257
+ parts.push(`L ${nums[i]} ${nums[i + 1]}`);
258
+ }
259
+ else if (name === 'BezierSegment') {
260
+ const nums = parseNumberList(getAttr(seg, 'Point1') + ' ' + getAttr(seg, 'Point2') + ' ' + getAttr(seg, 'Point3'));
261
+ if (nums.length >= 6)
262
+ parts.push(`C ${nums.slice(0, 6).join(' ')}`);
263
+ }
264
+ else if (name === 'PolyBezierSegment') {
265
+ const nums = parseNumberList(getAttr(seg, 'Points') ?? '');
266
+ for (let i = 0; i + 5 < nums.length; i += 6)
267
+ parts.push(`C ${nums.slice(i, i + 6).join(' ')}`);
268
+ }
269
+ }
270
+ if (getAttr(figure, 'IsClosed') === 'true')
271
+ parts.push('Z');
272
+ }
273
+ return parts.length ? parts.join(' ') : undefined;
274
+ }
275
+ function extractBrush(el, prop, opacity) {
276
+ const direct = getAttr(el, prop);
277
+ if (direct)
278
+ return parseBrushColor(direct, opacity);
279
+ const solid = findPropertyBrush(el, prop, 'SolidColorBrush');
280
+ if (solid)
281
+ return parseBrushColor(getAttr(solid, 'Color'), opacity * parseOpacity(getAttr(solid, 'Opacity')));
282
+ return undefined;
283
+ }
284
+ function findPropertyBrush(el, prop, brushLocalName) {
285
+ const propName = `${localName(el)}.${prop}`;
286
+ for (const child of childElements(el)) {
287
+ if (localName(child) !== propName)
288
+ continue;
289
+ return childElements(child).find(c => localName(c) === brushLocalName);
290
+ }
291
+ return undefined;
292
+ }
293
+ function parseOpacity(value) {
294
+ if (!value)
295
+ return 1;
296
+ const n = Number(value);
297
+ return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1;
298
+ }
299
+ function fillRule(el) {
300
+ const data = getAttr(el, 'Data') ?? '';
301
+ return /F0/.test(data) ? 'nonzero' : 'evenodd';
302
+ }
303
+ function parseRect(s) {
304
+ if (!s)
305
+ return undefined;
306
+ const nums = parseNumberList(s);
307
+ if (nums.length >= 4)
308
+ return [nums[0], nums[1], nums[2], nums[3]];
309
+ return undefined;
310
+ }
@@ -0,0 +1,16 @@
1
+ export interface Matrix2D {
2
+ a: number;
3
+ b: number;
4
+ c: number;
5
+ d: number;
6
+ e: number;
7
+ f: number;
8
+ }
9
+ export declare const IDENTITY: Matrix2D;
10
+ export declare function multiplyMatrix(m1: Matrix2D, m2: Matrix2D): Matrix2D;
11
+ export declare function transformPoint(m: Matrix2D, x: number, y: number): [number, number];
12
+ export declare function parseMatrix(input?: string | null): Matrix2D;
13
+ export declare function applyMatrixToContext(ctx: CanvasRenderingContext2D, m: Matrix2D): void;
14
+ export declare function parseBrushColor(input?: string | null, opacity?: number): string | undefined;
15
+ export declare function colorToRgba32(css: string | undefined, fallback?: number): number;
16
+ export declare function packRgba(r: number, g: number, b: number, a: number): number;
@@ -0,0 +1,115 @@
1
+ import { parseNumberList } from '../format/util.js';
2
+ export const IDENTITY = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
3
+ export function multiplyMatrix(m1, m2) {
4
+ return {
5
+ a: m1.a * m2.a + m1.c * m2.b,
6
+ b: m1.b * m2.a + m1.d * m2.b,
7
+ c: m1.a * m2.c + m1.c * m2.d,
8
+ d: m1.b * m2.c + m1.d * m2.d,
9
+ e: m1.a * m2.e + m1.c * m2.f + m1.e,
10
+ f: m1.b * m2.e + m1.d * m2.f + m1.f
11
+ };
12
+ }
13
+ export function transformPoint(m, x, y) {
14
+ return [m.a * x + m.c * y + m.e, m.b * x + m.d * y + m.f];
15
+ }
16
+ export function parseMatrix(input) {
17
+ if (!input)
18
+ return { ...IDENTITY };
19
+ const nums = parseNumberList(input);
20
+ if (nums.length >= 6)
21
+ return { a: nums[0], b: nums[1], c: nums[2], d: nums[3], e: nums[4], f: nums[5] };
22
+ return { ...IDENTITY };
23
+ }
24
+ export function applyMatrixToContext(ctx, m) {
25
+ ctx.transform(m.a, m.b, m.c, m.d, m.e, m.f);
26
+ }
27
+ export function parseBrushColor(input, opacity = 1) {
28
+ if (!input || input === 'Transparent' || input === '{x:Null}')
29
+ return undefined;
30
+ let s = input.trim();
31
+ if (s.startsWith('{') || s.includes('Brush'))
32
+ return undefined;
33
+ const known = namedColor(s);
34
+ if (known)
35
+ s = known;
36
+ if (s.startsWith('sc#')) {
37
+ const nums = parseNumberList(s);
38
+ if (nums.length >= 3) {
39
+ const offset = nums.length >= 4 ? 1 : 0;
40
+ const a = nums.length >= 4 ? nums[0] : 1;
41
+ const r = Math.round((nums[offset] ?? 0) * 255);
42
+ const g = Math.round((nums[offset + 1] ?? 0) * 255);
43
+ const b = Math.round((nums[offset + 2] ?? 0) * 255);
44
+ return rgba(r, g, b, a * opacity);
45
+ }
46
+ }
47
+ if (s.startsWith('#')) {
48
+ const hex = s.slice(1);
49
+ if (hex.length === 3) {
50
+ const r = parseInt(hex[0] + hex[0], 16);
51
+ const g = parseInt(hex[1] + hex[1], 16);
52
+ const b = parseInt(hex[2] + hex[2], 16);
53
+ return rgba(r, g, b, opacity);
54
+ }
55
+ if (hex.length === 6) {
56
+ return rgba(parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16), opacity);
57
+ }
58
+ if (hex.length === 8) {
59
+ // XPS uses #AARRGGBB; CSS often uses #RRGGBBAA. Prefer XPS semantics for FixedPage.
60
+ const a = parseInt(hex.slice(0, 2), 16) / 255;
61
+ return rgba(parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16), parseInt(hex.slice(6, 8), 16), a * opacity);
62
+ }
63
+ }
64
+ if (/^rgba?\(/i.test(s))
65
+ return s;
66
+ return s;
67
+ }
68
+ export function colorToRgba32(css, fallback = 0xff000000) {
69
+ if (!css)
70
+ return fallback;
71
+ const s = css.trim();
72
+ const rgbaMatch = s.match(/^rgba?\(([^)]+)\)/i);
73
+ if (rgbaMatch) {
74
+ const parts = rgbaMatch[1].split(',').map(x => x.trim());
75
+ const r = Number(parts[0] ?? 0), g = Number(parts[1] ?? 0), b = Number(parts[2] ?? 0);
76
+ const a = parts[3] === undefined ? 1 : Number(parts[3]);
77
+ return packRgba(r, g, b, Math.round(a * 255));
78
+ }
79
+ if (s.startsWith('#')) {
80
+ const hex = s.slice(1);
81
+ if (hex.length === 6)
82
+ return packRgba(parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16), 255);
83
+ if (hex.length === 8)
84
+ return packRgba(parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16), parseInt(hex.slice(6, 8), 16), parseInt(hex.slice(0, 2), 16));
85
+ }
86
+ const known = namedColor(s);
87
+ return known ? colorToRgba32(known, fallback) : fallback;
88
+ }
89
+ export function packRgba(r, g, b, a) {
90
+ r = clamp255(r);
91
+ g = clamp255(g);
92
+ b = clamp255(b);
93
+ a = clamp255(a);
94
+ return (r & 255) | ((g & 255) << 8) | ((b & 255) << 16) | ((a & 255) << 24);
95
+ }
96
+ function rgba(r, g, b, a) {
97
+ return `rgba(${clamp255(r)}, ${clamp255(g)}, ${clamp255(b)}, ${Math.max(0, Math.min(1, a))})`;
98
+ }
99
+ function clamp255(v) { return Math.max(0, Math.min(255, Math.round(v))); }
100
+ function namedColor(s) {
101
+ switch (s.toLowerCase()) {
102
+ case 'black': return '#000000';
103
+ case 'white': return '#ffffff';
104
+ case 'red': return '#ff0000';
105
+ case 'green': return '#008000';
106
+ case 'blue': return '#0000ff';
107
+ case 'yellow': return '#ffff00';
108
+ case 'cyan': return '#00ffff';
109
+ case 'magenta': return '#ff00ff';
110
+ case 'gray':
111
+ case 'grey': return '#808080';
112
+ case 'transparent': return '#00000000';
113
+ default: return undefined;
114
+ }
115
+ }
@@ -0,0 +1,16 @@
1
+ import type { Matrix2D } from './style.js';
2
+ import type { W2dTextPageData } from '../format/document.js';
3
+ export interface FitOptions {
4
+ canvasWidth: number;
5
+ canvasHeight: number;
6
+ pageWidth: number;
7
+ pageHeight: number;
8
+ zoom?: number;
9
+ panX?: number;
10
+ panY?: number;
11
+ margin?: number;
12
+ sourceMinX?: number;
13
+ sourceMinY?: number;
14
+ }
15
+ export declare function fitPageMatrix(opts: FitOptions): Matrix2D;
16
+ export declare function matrixForW2d(page: W2dTextPageData, canvasWidth: number, canvasHeight: number, zoom?: number, panX?: number, panY?: number): Matrix2D;
@@ -0,0 +1,27 @@
1
+ export function fitPageMatrix(opts) {
2
+ const margin = opts.margin ?? 24;
3
+ const usableW = Math.max(1, opts.canvasWidth - margin * 2);
4
+ const usableH = Math.max(1, opts.canvasHeight - margin * 2);
5
+ const base = Math.min(usableW / Math.max(1, opts.pageWidth), usableH / Math.max(1, opts.pageHeight));
6
+ const scale = base * (opts.zoom ?? 1);
7
+ const e = (opts.canvasWidth - opts.pageWidth * scale) / 2 + (opts.panX ?? 0) - (opts.sourceMinX ?? 0) * scale;
8
+ const f = (opts.canvasHeight - opts.pageHeight * scale) / 2 + (opts.panY ?? 0) - (opts.sourceMinY ?? 0) * scale;
9
+ return { a: scale, b: 0, c: 0, d: scale, e, f };
10
+ }
11
+ export function matrixForW2d(page, canvasWidth, canvasHeight, zoom = 1, panX = 0, panY = 0) {
12
+ const b = page.bounds;
13
+ const minX = b?.minX ?? 0;
14
+ const minY = b?.minY ?? 0;
15
+ const maxY = b?.maxY ?? page.height;
16
+ const pageWidth = b ? Math.max(1, b.maxX - b.minX) : page.width;
17
+ const pageHeight = b ? Math.max(1, b.maxY - b.minY) : page.height;
18
+ const margin = 24;
19
+ const usableW = Math.max(1, canvasWidth - margin * 2);
20
+ const usableH = Math.max(1, canvasHeight - margin * 2);
21
+ const scale = Math.min(usableW / Math.max(1, pageWidth), usableH / Math.max(1, pageHeight)) * zoom;
22
+ const left = (canvasWidth - pageWidth * scale) / 2 + panX;
23
+ const top = (canvasHeight - pageHeight * scale) / 2 + panY;
24
+ // Classic WHIP!/W2D logical coordinates use a mathematical Y-up orientation. Canvas uses Y-down,
25
+ // so flip vertically while preserving fit-to-bounds.
26
+ return { a: scale, b: 0, c: 0, d: -scale, e: left - minX * scale, f: top + maxY * scale };
27
+ }
@@ -0,0 +1,41 @@
1
+ export type PathCommand = {
2
+ type: 'M';
3
+ x: number;
4
+ y: number;
5
+ } | {
6
+ type: 'L';
7
+ x: number;
8
+ y: number;
9
+ } | {
10
+ type: 'C';
11
+ x1: number;
12
+ y1: number;
13
+ x2: number;
14
+ y2: number;
15
+ x: number;
16
+ y: number;
17
+ } | {
18
+ type: 'Q';
19
+ x1: number;
20
+ y1: number;
21
+ x: number;
22
+ y: number;
23
+ } | {
24
+ type: 'A';
25
+ rx: number;
26
+ ry: number;
27
+ rotation: number;
28
+ largeArc: boolean;
29
+ sweep: boolean;
30
+ x: number;
31
+ y: number;
32
+ } | {
33
+ type: 'Z';
34
+ };
35
+ export interface FlattenedSubpath {
36
+ points: number[];
37
+ closed: boolean;
38
+ }
39
+ export declare function parsePathData(data: string): PathCommand[];
40
+ export declare function applyPathToCanvas(ctx: CanvasRenderingContext2D, commands: PathCommand[]): void;
41
+ export declare function flattenPath(commands: PathCommand[], tolerance?: number): FlattenedSubpath[];