@file-viewer/doc 2.1.23 → 2.1.25

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.
@@ -0,0 +1,1171 @@
1
+ import { BinaryReader } from '../core/binary.js';
2
+ import { dataUrlFromBytes, escapeHtml } from '../core/utils.js';
3
+ const DEFAULT_PEN = { color: '#000000', width: 1 };
4
+ const DEFAULT_BRUSH = { color: '#ffffff', none: true };
5
+ const DEFAULT_FONT = { family: 'sans-serif', size: 12 };
6
+ const IDENTITY_MATRIX = { m11: 1, m12: 0, m21: 0, m22: 1, dx: 0, dy: 0 };
7
+ const PLACEABLE_WMF_KEY = 0x9ac6cdd7;
8
+ const WMF_HEADER_SIZE = 18;
9
+ const WMF_RECORD = {
10
+ META_EOF: 0x0000,
11
+ META_SETBKMODE: 0x0102,
12
+ META_SETPOLYFILLMODE: 0x0106,
13
+ META_SETBKCOLOR: 0x0201,
14
+ META_SETTEXTCOLOR: 0x0209,
15
+ META_SETWINDOWORG: 0x020b,
16
+ META_SETWINDOWEXT: 0x020c,
17
+ META_SETVIEWPORTORG: 0x020d,
18
+ META_SETVIEWPORTEXT: 0x020e,
19
+ META_OFFSETWINDOWORG: 0x020f,
20
+ META_OFFSETVIEWPORTORG: 0x0211,
21
+ META_LINETO: 0x0213,
22
+ META_MOVETO: 0x0214,
23
+ META_POLYGON: 0x0324,
24
+ META_POLYLINE: 0x0325,
25
+ META_TEXTOUT: 0x0521,
26
+ META_POLYPOLYGON: 0x0538,
27
+ META_ELLIPSE: 0x0418,
28
+ META_RECTANGLE: 0x041b,
29
+ META_ROUNDRECT: 0x061c,
30
+ META_CREATEBRUSHINDIRECT: 0x02fc,
31
+ META_CREATEPENINDIRECT: 0x02fa,
32
+ META_CREATEFONTINDIRECT: 0x02fb,
33
+ META_SELECTOBJECT: 0x012d,
34
+ META_DELETEOBJECT: 0x01f0,
35
+ };
36
+ const EMF_RECORD = {
37
+ EMR_HEADER: 0x00000001,
38
+ EMR_POLYBEZIER: 0x00000002,
39
+ EMR_POLYGON: 0x00000003,
40
+ EMR_POLYLINE: 0x00000004,
41
+ EMR_POLYBEZIERTO: 0x00000005,
42
+ EMR_POLYLINETO: 0x00000006,
43
+ EMR_POLYPOLYLINE: 0x00000007,
44
+ EMR_POLYPOLYGON: 0x00000008,
45
+ EMR_SETWINDOWEXTEX: 0x00000009,
46
+ EMR_SETWINDOWORGEX: 0x0000000a,
47
+ EMR_SETVIEWPORTEXTEX: 0x0000000b,
48
+ EMR_SETVIEWPORTORGEX: 0x0000000c,
49
+ EMR_EOF: 0x0000000e,
50
+ EMR_SETBKMODE: 0x00000012,
51
+ EMR_SETPOLYFILLMODE: 0x00000013,
52
+ EMR_SETTEXTALIGN: 0x00000016,
53
+ EMR_SETTEXTCOLOR: 0x00000018,
54
+ EMR_SETBKCOLOR: 0x00000019,
55
+ EMR_MOVETOEX: 0x0000001b,
56
+ EMR_SAVEDC: 0x00000021,
57
+ EMR_RESTOREDC: 0x00000022,
58
+ EMR_SETWORLDTRANSFORM: 0x00000023,
59
+ EMR_MODIFYWORLDTRANSFORM: 0x00000024,
60
+ EMR_SELECTOBJECT: 0x00000025,
61
+ EMR_CREATEPEN: 0x00000026,
62
+ EMR_CREATEBRUSHINDIRECT: 0x00000027,
63
+ EMR_DELETEOBJECT: 0x00000028,
64
+ EMR_ELLIPSE: 0x0000002a,
65
+ EMR_RECTANGLE: 0x0000002b,
66
+ EMR_ROUNDRECT: 0x0000002c,
67
+ EMR_BEGINPATH: 0x0000003b,
68
+ EMR_ENDPATH: 0x0000003c,
69
+ EMR_CLOSEFIGURE: 0x0000003d,
70
+ EMR_FILLPATH: 0x0000003e,
71
+ EMR_STROKEANDFILLPATH: 0x0000003f,
72
+ EMR_STROKEPATH: 0x00000040,
73
+ EMR_SELECTCLIPPATH: 0x00000043,
74
+ EMR_LINETO: 0x00000036,
75
+ EMR_BITBLT: 0x0000004c,
76
+ EMR_STRETCHBLT: 0x0000004d,
77
+ EMR_STRETCHDIBITS: 0x00000051,
78
+ EMR_EXTCREATEFONTINDIRECTW: 0x00000052,
79
+ EMR_EXTTEXTOUTA: 0x00000053,
80
+ EMR_EXTTEXTOUTW: 0x00000054,
81
+ EMR_POLYBEZIER16: 0x00000055,
82
+ EMR_POLYGON16: 0x00000056,
83
+ EMR_POLYLINE16: 0x00000057,
84
+ EMR_POLYBEZIERTO16: 0x00000058,
85
+ EMR_POLYLINETO16: 0x00000059,
86
+ EMR_POLYPOLYLINE16: 0x0000005a,
87
+ EMR_POLYPOLYGON16: 0x0000005b,
88
+ EMR_EXTCREATEPEN: 0x0000005f,
89
+ EMR_SMALLTEXTOUT: 0x0000006c,
90
+ EMR_ALPHABLEND: 0x00000072,
91
+ };
92
+ const STOCK_OBJECT_BASE = 0x80000000;
93
+ const STOCK_OBJECT = {
94
+ WHITE_BRUSH: STOCK_OBJECT_BASE,
95
+ LTGRAY_BRUSH: STOCK_OBJECT_BASE + 1,
96
+ GRAY_BRUSH: STOCK_OBJECT_BASE + 2,
97
+ DKGRAY_BRUSH: STOCK_OBJECT_BASE + 3,
98
+ BLACK_BRUSH: STOCK_OBJECT_BASE + 4,
99
+ NULL_BRUSH: STOCK_OBJECT_BASE + 5,
100
+ WHITE_PEN: STOCK_OBJECT_BASE + 6,
101
+ BLACK_PEN: STOCK_OBJECT_BASE + 7,
102
+ NULL_PEN: STOCK_OBJECT_BASE + 8,
103
+ };
104
+ const MODIFY_WORLD_TRANSFORM = {
105
+ MWT_IDENTITY: 1,
106
+ MWT_LEFTMULTIPLY: 2,
107
+ MWT_RIGHTMULTIPLY: 3,
108
+ MWT_SET: 4,
109
+ };
110
+ function numberCss(value) {
111
+ const rounded = Math.round(value * 100) / 100;
112
+ return Number.isInteger(rounded) ? String(rounded) : String(rounded);
113
+ }
114
+ function rectWidth(rect) {
115
+ return Math.max(1, rect.right - rect.left || 0);
116
+ }
117
+ function rectHeight(rect) {
118
+ return Math.max(1, rect.bottom - rect.top || 0);
119
+ }
120
+ function normalizeRect(rect) {
121
+ return {
122
+ left: Math.min(rect.left, rect.right),
123
+ top: Math.min(rect.top, rect.bottom),
124
+ right: Math.max(rect.left, rect.right),
125
+ bottom: Math.max(rect.top, rect.bottom),
126
+ };
127
+ }
128
+ function updateBounds(ctx, points) {
129
+ for (const point of points) {
130
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y))
131
+ continue;
132
+ if (!ctx.contentBounds) {
133
+ ctx.contentBounds = { left: point.x, top: point.y, right: point.x, bottom: point.y };
134
+ continue;
135
+ }
136
+ ctx.contentBounds.left = Math.min(ctx.contentBounds.left, point.x);
137
+ ctx.contentBounds.top = Math.min(ctx.contentBounds.top, point.y);
138
+ ctx.contentBounds.right = Math.max(ctx.contentBounds.right, point.x);
139
+ ctx.contentBounds.bottom = Math.max(ctx.contentBounds.bottom, point.y);
140
+ }
141
+ }
142
+ function colorRefToCss(colorRef) {
143
+ const red = colorRef & 0xff;
144
+ const green = (colorRef >>> 8) & 0xff;
145
+ const blue = (colorRef >>> 16) & 0xff;
146
+ return `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`;
147
+ }
148
+ function createBitmapFileHeader(pixelOffset, fileSize) {
149
+ const header = new Uint8Array(14);
150
+ const view = new DataView(header.buffer);
151
+ header[0] = 0x42;
152
+ header[1] = 0x4d;
153
+ view.setUint32(2, fileSize >>> 0, true);
154
+ view.setUint32(10, pixelOffset >>> 0, true);
155
+ return header;
156
+ }
157
+ function bitmapInfoToBmp(bmi, bits) {
158
+ if (bmi.length < 12 || !bits.length)
159
+ return null;
160
+ const pixelOffset = 14 + bmi.length;
161
+ const fileSize = pixelOffset + bits.length;
162
+ const out = new Uint8Array(fileSize);
163
+ out.set(createBitmapFileHeader(pixelOffset, fileSize), 0);
164
+ out.set(bmi, 14);
165
+ out.set(bits, pixelOffset);
166
+ return out;
167
+ }
168
+ function penStyleFromCode(style, width, color) {
169
+ const normalizedWidth = Math.max(1, Math.abs(width) || 1);
170
+ const base = { color, width: normalizedWidth };
171
+ switch (style & 0x000f) {
172
+ case 1:
173
+ return { ...base, dasharray: `${normalizedWidth * 4} ${normalizedWidth * 2}` };
174
+ case 2:
175
+ return { ...base, dasharray: `${normalizedWidth} ${normalizedWidth * 2}` };
176
+ case 3:
177
+ return { ...base, dasharray: `${normalizedWidth * 4} ${normalizedWidth * 2} ${normalizedWidth} ${normalizedWidth * 2}` };
178
+ case 4:
179
+ return { ...base, dasharray: `${normalizedWidth * 4} ${normalizedWidth * 2} ${normalizedWidth} ${normalizedWidth * 2} ${normalizedWidth} ${normalizedWidth * 2}` };
180
+ case 5:
181
+ return { ...base, none: true };
182
+ default:
183
+ return base;
184
+ }
185
+ }
186
+ function brushStyleFromCode(style, color) {
187
+ if (style === 1)
188
+ return { color, none: true };
189
+ return { color };
190
+ }
191
+ function cloneGraphicsState(state) {
192
+ return {
193
+ ...state,
194
+ windowOrg: { ...state.windowOrg },
195
+ windowExt: { ...state.windowExt },
196
+ viewportOrg: { ...state.viewportOrg },
197
+ viewportExt: { ...state.viewportExt },
198
+ worldTransform: { ...state.worldTransform },
199
+ currentPosition: { ...state.currentPosition },
200
+ pen: { ...state.pen },
201
+ brush: { ...state.brush },
202
+ font: { ...state.font },
203
+ };
204
+ }
205
+ function readXForm(reader, offset) {
206
+ const view = reader.view;
207
+ return {
208
+ m11: reader.ensure(offset, 4) ? view.getFloat32(offset, true) : 1,
209
+ m12: reader.ensure(offset + 4, 4) ? view.getFloat32(offset + 4, true) : 0,
210
+ m21: reader.ensure(offset + 8, 4) ? view.getFloat32(offset + 8, true) : 0,
211
+ m22: reader.ensure(offset + 12, 4) ? view.getFloat32(offset + 12, true) : 1,
212
+ dx: reader.ensure(offset + 16, 4) ? view.getFloat32(offset + 16, true) : 0,
213
+ dy: reader.ensure(offset + 20, 4) ? view.getFloat32(offset + 20, true) : 0,
214
+ };
215
+ }
216
+ function multiplyMatrix(left, right) {
217
+ return {
218
+ m11: left.m11 * right.m11 + left.m12 * right.m21,
219
+ m12: left.m11 * right.m12 + left.m12 * right.m22,
220
+ m21: left.m21 * right.m11 + left.m22 * right.m21,
221
+ m22: left.m21 * right.m12 + left.m22 * right.m22,
222
+ dx: left.dx * right.m11 + left.dy * right.m21 + right.dx,
223
+ dy: left.dx * right.m12 + left.dy * right.m22 + right.dy,
224
+ };
225
+ }
226
+ function transformPoint(matrix, point) {
227
+ return {
228
+ x: point.x * matrix.m11 + point.y * matrix.m21 + matrix.dx,
229
+ y: point.x * matrix.m12 + point.y * matrix.m22 + matrix.dy,
230
+ };
231
+ }
232
+ function transformVector(matrix, vector) {
233
+ return {
234
+ x: vector.x * matrix.m11 + vector.y * matrix.m21,
235
+ y: vector.x * matrix.m12 + vector.y * matrix.m22,
236
+ };
237
+ }
238
+ function defaultContext(width, height, viewBox) {
239
+ return {
240
+ width,
241
+ height,
242
+ viewBox,
243
+ recordCount: 0,
244
+ contentBounds: null,
245
+ nodes: [],
246
+ stateStack: [],
247
+ capturePath: false,
248
+ pathData: [],
249
+ pathPoints: [],
250
+ objects: [],
251
+ state: {
252
+ windowOrg: { x: viewBox.left, y: viewBox.top },
253
+ windowExt: { x: rectWidth(viewBox), y: rectHeight(viewBox) },
254
+ viewportOrg: { x: viewBox.left, y: viewBox.top },
255
+ viewportExt: { x: rectWidth(viewBox), y: rectHeight(viewBox) },
256
+ worldTransform: { ...IDENTITY_MATRIX },
257
+ currentPosition: { x: 0, y: 0 },
258
+ pen: { ...DEFAULT_PEN },
259
+ brush: { ...DEFAULT_BRUSH },
260
+ font: { ...DEFAULT_FONT },
261
+ textColor: '#000000',
262
+ bkColor: '#ffffff',
263
+ bkMode: 1,
264
+ textAlign: 0,
265
+ polygonFillMode: 1,
266
+ },
267
+ };
268
+ }
269
+ function mapPoint(ctx, point) {
270
+ const transformed = transformPoint(ctx.state.worldTransform, point);
271
+ const windowExtX = ctx.state.windowExt.x || rectWidth(ctx.viewBox) || 1;
272
+ const windowExtY = ctx.state.windowExt.y || rectHeight(ctx.viewBox) || 1;
273
+ const viewportExtX = ctx.state.viewportExt.x || rectWidth(ctx.viewBox) || 1;
274
+ const viewportExtY = ctx.state.viewportExt.y || rectHeight(ctx.viewBox) || 1;
275
+ return {
276
+ x: ctx.state.viewportOrg.x + ((transformed.x - ctx.state.windowOrg.x) * viewportExtX) / windowExtX,
277
+ y: ctx.state.viewportOrg.y + ((transformed.y - ctx.state.windowOrg.y) * viewportExtY) / windowExtY,
278
+ };
279
+ }
280
+ function mapVector(ctx, vector) {
281
+ const transformed = transformVector(ctx.state.worldTransform, vector);
282
+ const windowExtX = ctx.state.windowExt.x || rectWidth(ctx.viewBox) || 1;
283
+ const windowExtY = ctx.state.windowExt.y || rectHeight(ctx.viewBox) || 1;
284
+ const viewportExtX = ctx.state.viewportExt.x || rectWidth(ctx.viewBox) || 1;
285
+ const viewportExtY = ctx.state.viewportExt.y || rectHeight(ctx.viewBox) || 1;
286
+ return {
287
+ x: (transformed.x * viewportExtX) / windowExtX,
288
+ y: (transformed.y * viewportExtY) / windowExtY,
289
+ };
290
+ }
291
+ function mapRect(ctx, rect) {
292
+ const points = [
293
+ mapPoint(ctx, { x: rect.left, y: rect.top }),
294
+ mapPoint(ctx, { x: rect.right, y: rect.top }),
295
+ mapPoint(ctx, { x: rect.right, y: rect.bottom }),
296
+ mapPoint(ctx, { x: rect.left, y: rect.bottom }),
297
+ ];
298
+ return normalizeRect({
299
+ left: Math.min(...points.map((point) => point.x)),
300
+ top: Math.min(...points.map((point) => point.y)),
301
+ right: Math.max(...points.map((point) => point.x)),
302
+ bottom: Math.max(...points.map((point) => point.y)),
303
+ });
304
+ }
305
+ function pointAttr(point) {
306
+ return `${numberCss(point.x)},${numberCss(point.y)}`;
307
+ }
308
+ function effectivePenWidth(ctx, pen) {
309
+ const width = Math.max(1, Math.abs(pen.width) || 1);
310
+ const vx = mapVector(ctx, { x: width, y: 0 });
311
+ const vy = mapVector(ctx, { x: 0, y: width });
312
+ const sx = Math.hypot(vx.x, vx.y);
313
+ const sy = Math.hypot(vy.x, vy.y);
314
+ const scaled = (sx + sy) / 2;
315
+ if (!Number.isFinite(scaled) || scaled <= 0)
316
+ return width;
317
+ return Math.max(0.5, scaled);
318
+ }
319
+ function scaledDashArray(ctx, pen) {
320
+ if (!pen.dasharray)
321
+ return undefined;
322
+ const baseWidth = Math.max(1, Math.abs(pen.width) || 1);
323
+ const scale = effectivePenWidth(ctx, pen) / baseWidth;
324
+ return pen.dasharray
325
+ .split(/\s+/)
326
+ .map((part) => Number(part))
327
+ .filter((value) => Number.isFinite(value) && value > 0)
328
+ .map((value) => numberCss(value * scale))
329
+ .join(' ') || undefined;
330
+ }
331
+ function strokeAttributes(ctx, pen) {
332
+ if (pen.none)
333
+ return 'stroke="none"';
334
+ const attrs = [`stroke="${escapeHtml(pen.color)}"`, `stroke-width="${numberCss(effectivePenWidth(ctx, pen))}"`, 'fill="none"'];
335
+ const dasharray = scaledDashArray(ctx, pen);
336
+ if (dasharray)
337
+ attrs.push(`stroke-dasharray="${escapeHtml(dasharray)}"`);
338
+ return attrs.join(' ');
339
+ }
340
+ function fillAttributes(ctx) {
341
+ const attrs = [ctx.state.pen.none ? 'stroke="none"' : `stroke="${escapeHtml(ctx.state.pen.color)}"`, ctx.state.pen.none ? '' : `stroke-width="${numberCss(effectivePenWidth(ctx, ctx.state.pen))}"`].filter(Boolean);
342
+ const dasharray = scaledDashArray(ctx, ctx.state.pen);
343
+ if (dasharray && !ctx.state.pen.none)
344
+ attrs.push(`stroke-dasharray="${escapeHtml(dasharray)}"`);
345
+ attrs.push(ctx.state.brush.none ? 'fill="none"' : `fill="${escapeHtml(ctx.state.brush.color)}"`);
346
+ attrs.push(`fill-rule="${ctx.state.polygonFillMode === 2 ? 'nonzero' : 'evenodd'}"`);
347
+ return attrs.join(' ');
348
+ }
349
+ function fillOnlyAttributes(ctx) {
350
+ const attrs = ['stroke="none"'];
351
+ attrs.push(ctx.state.brush.none ? 'fill="none"' : `fill="${escapeHtml(ctx.state.brush.color)}"`);
352
+ attrs.push(`fill-rule="${ctx.state.polygonFillMode === 2 ? 'nonzero' : 'evenodd'}"`);
353
+ return attrs.join(' ');
354
+ }
355
+ function ensureObjectSlot(objects) {
356
+ const free = objects.findIndex((item) => item == null);
357
+ if (free >= 0)
358
+ return free;
359
+ objects.push(null);
360
+ return objects.length - 1;
361
+ }
362
+ function setObject(objects, index, value) {
363
+ while (objects.length <= index)
364
+ objects.push(null);
365
+ objects[index] = value;
366
+ }
367
+ function selectStockObject(ctx, index) {
368
+ switch (index >>> 0) {
369
+ case STOCK_OBJECT.WHITE_BRUSH:
370
+ ctx.state.brush = { color: '#ffffff' };
371
+ return true;
372
+ case STOCK_OBJECT.LTGRAY_BRUSH:
373
+ ctx.state.brush = { color: '#c0c0c0' };
374
+ return true;
375
+ case STOCK_OBJECT.GRAY_BRUSH:
376
+ ctx.state.brush = { color: '#808080' };
377
+ return true;
378
+ case STOCK_OBJECT.DKGRAY_BRUSH:
379
+ ctx.state.brush = { color: '#404040' };
380
+ return true;
381
+ case STOCK_OBJECT.BLACK_BRUSH:
382
+ ctx.state.brush = { color: '#000000' };
383
+ return true;
384
+ case STOCK_OBJECT.NULL_BRUSH:
385
+ ctx.state.brush = { color: 'none', none: true };
386
+ return true;
387
+ case STOCK_OBJECT.WHITE_PEN:
388
+ ctx.state.pen = { color: '#ffffff', width: 1 };
389
+ return true;
390
+ case STOCK_OBJECT.BLACK_PEN:
391
+ ctx.state.pen = { color: '#000000', width: 1 };
392
+ return true;
393
+ case STOCK_OBJECT.NULL_PEN:
394
+ ctx.state.pen = { color: 'none', width: 1, none: true };
395
+ return true;
396
+ default:
397
+ return false;
398
+ }
399
+ }
400
+ function selectObject(ctx, index) {
401
+ if (selectStockObject(ctx, index))
402
+ return;
403
+ const object = ctx.objects[index] || null;
404
+ if (!object)
405
+ return;
406
+ if (object.kind === 'pen')
407
+ ctx.state.pen = { ...object.value };
408
+ if (object.kind === 'brush')
409
+ ctx.state.brush = { ...object.value };
410
+ if (object.kind === 'font')
411
+ ctx.state.font = { ...object.value };
412
+ }
413
+ function addNode(ctx, svg, points) {
414
+ ctx.nodes.push(svg);
415
+ updateBounds(ctx, points);
416
+ }
417
+ function addPathFragment(ctx, fragment, points) {
418
+ if (!fragment)
419
+ return;
420
+ ctx.pathData.push(fragment);
421
+ ctx.pathPoints.push(...points);
422
+ }
423
+ function flushPath(ctx, attrs) {
424
+ if (!ctx.pathData.length)
425
+ return;
426
+ addNode(ctx, `<path d="${ctx.pathData.join(' ')}" ${attrs} />`, ctx.pathPoints);
427
+ ctx.pathData = [];
428
+ ctx.pathPoints = [];
429
+ ctx.capturePath = false;
430
+ }
431
+ function addLine(ctx, start, end) {
432
+ const a = mapPoint(ctx, start);
433
+ const b = mapPoint(ctx, end);
434
+ ctx.state.currentPosition = { ...end };
435
+ if (ctx.capturePath) {
436
+ addPathFragment(ctx, `${ctx.pathData.length ? '' : `M ${pointAttr(a)}`} L ${pointAttr(b)}`.trim(), [a, b]);
437
+ return;
438
+ }
439
+ addNode(ctx, `<line x1="${numberCss(a.x)}" y1="${numberCss(a.y)}" x2="${numberCss(b.x)}" y2="${numberCss(b.y)}" ${strokeAttributes(ctx, ctx.state.pen)} />`, [a, b]);
440
+ }
441
+ function addPolyline(ctx, points, closed) {
442
+ if (!points.length)
443
+ return;
444
+ const mapped = points.map((point) => mapPoint(ctx, point));
445
+ ctx.state.currentPosition = { ...points[points.length - 1] };
446
+ const attr = mapped.map(pointAttr).join(' ');
447
+ if (ctx.capturePath) {
448
+ const commands = [`M ${pointAttr(mapped[0])}`, ...mapped.slice(1).map((point) => `L ${pointAttr(point)}`)];
449
+ if (closed)
450
+ commands.push('Z');
451
+ addPathFragment(ctx, commands.join(' '), mapped);
452
+ return;
453
+ }
454
+ const tag = closed ? 'polygon' : 'polyline';
455
+ const styleAttrs = closed ? fillAttributes(ctx) : strokeAttributes(ctx, ctx.state.pen);
456
+ addNode(ctx, `<${tag} points="${attr}" ${styleAttrs} />`, mapped);
457
+ }
458
+ function addBezierPath(ctx, points, relativeToCurrent = false) {
459
+ const logical = [];
460
+ if (relativeToCurrent)
461
+ logical.push({ ...ctx.state.currentPosition });
462
+ logical.push(...points);
463
+ if (logical.length < 4)
464
+ return;
465
+ const mapped = logical.map((point) => mapPoint(ctx, point));
466
+ let path = `M ${pointAttr(mapped[0])}`;
467
+ for (let i = 1; i + 2 < mapped.length; i += 3) {
468
+ path += ` C ${pointAttr(mapped[i])} ${pointAttr(mapped[i + 1])} ${pointAttr(mapped[i + 2])}`;
469
+ }
470
+ ctx.state.currentPosition = { ...logical[logical.length - 1] };
471
+ if (ctx.capturePath) {
472
+ addPathFragment(ctx, path, mapped);
473
+ return;
474
+ }
475
+ addNode(ctx, `<path d="${path}" ${strokeAttributes(ctx, ctx.state.pen)} />`, mapped);
476
+ }
477
+ function addRect(ctx, rect, radius) {
478
+ const mapped = mapRect(ctx, rect);
479
+ const attrs = [
480
+ `x="${numberCss(mapped.left)}"`,
481
+ `y="${numberCss(mapped.top)}"`,
482
+ `width="${numberCss(rectWidth(mapped))}"`,
483
+ `height="${numberCss(rectHeight(mapped))}"`,
484
+ ];
485
+ if (radius) {
486
+ const r = mapPoint(ctx, { x: radius.x, y: radius.y });
487
+ attrs.push(`rx="${numberCss(Math.abs(r.x - ctx.state.viewportOrg.x))}"`);
488
+ attrs.push(`ry="${numberCss(Math.abs(r.y - ctx.state.viewportOrg.y))}"`);
489
+ }
490
+ if (ctx.capturePath) {
491
+ addPathFragment(ctx, `M ${numberCss(mapped.left)},${numberCss(mapped.top)} L ${numberCss(mapped.right)},${numberCss(mapped.top)} L ${numberCss(mapped.right)},${numberCss(mapped.bottom)} L ${numberCss(mapped.left)},${numberCss(mapped.bottom)} Z`, [
492
+ { x: mapped.left, y: mapped.top },
493
+ { x: mapped.right, y: mapped.bottom },
494
+ ]);
495
+ return;
496
+ }
497
+ addNode(ctx, `<rect ${attrs.join(' ')} ${fillAttributes(ctx)} />`, [
498
+ { x: mapped.left, y: mapped.top },
499
+ { x: mapped.right, y: mapped.bottom },
500
+ ]);
501
+ }
502
+ function addEllipse(ctx, rect) {
503
+ const mapped = mapRect(ctx, rect);
504
+ const cx = (mapped.left + mapped.right) / 2;
505
+ const cy = (mapped.top + mapped.bottom) / 2;
506
+ const rx = rectWidth(mapped) / 2;
507
+ const ry = rectHeight(mapped) / 2;
508
+ if (ctx.capturePath) {
509
+ addPathFragment(ctx, `M ${numberCss(cx - rx)},${numberCss(cy)} A ${numberCss(rx)},${numberCss(ry)} 0 1 0 ${numberCss(cx + rx)},${numberCss(cy)} A ${numberCss(rx)},${numberCss(ry)} 0 1 0 ${numberCss(cx - rx)},${numberCss(cy)} Z`, [
510
+ { x: mapped.left, y: mapped.top },
511
+ { x: mapped.right, y: mapped.bottom },
512
+ ]);
513
+ return;
514
+ }
515
+ addNode(ctx, `<ellipse cx="${numberCss(cx)}" cy="${numberCss(cy)}" rx="${numberCss(rx)}" ry="${numberCss(ry)}" ${fillAttributes(ctx)} />`, [
516
+ { x: mapped.left, y: mapped.top },
517
+ { x: mapped.right, y: mapped.bottom },
518
+ ]);
519
+ }
520
+ function addRasterImage(ctx, rect, bmpBytes) {
521
+ if (!bmpBytes.length)
522
+ return;
523
+ const mapped = mapRect(ctx, rect);
524
+ const width = rectWidth(mapped);
525
+ const height = rectHeight(mapped);
526
+ const href = dataUrlFromBytes(bmpBytes, 'image/bmp');
527
+ addNode(ctx, `<image x="${numberCss(mapped.left)}" y="${numberCss(mapped.top)}" width="${numberCss(width)}" height="${numberCss(height)}" href="${href}" preserveAspectRatio="none" />`, [
528
+ { x: mapped.left, y: mapped.top },
529
+ { x: mapped.right, y: mapped.bottom },
530
+ ]);
531
+ }
532
+ function decodeAnsi(bytes) {
533
+ return new TextDecoder('windows-1252').decode(bytes).replace(/\0+$/g, '');
534
+ }
535
+ function decodeUtf16(bytes) {
536
+ return new TextDecoder('utf-16le').decode(bytes).replace(/\0+$/g, '');
537
+ }
538
+ function textAnchor(textAlign) {
539
+ const horizontal = textAlign & 0x0006;
540
+ if (horizontal === 0x0002)
541
+ return 'end';
542
+ if (horizontal === 0x0006)
543
+ return 'middle';
544
+ return 'start';
545
+ }
546
+ function dominantBaseline(textAlign) {
547
+ const vertical = textAlign & 0x0018;
548
+ if (vertical === 0x0008)
549
+ return 'text-after-edge';
550
+ if (vertical === 0x0018)
551
+ return 'alphabetic';
552
+ return 'text-before-edge';
553
+ }
554
+ function addText(ctx, point, text) {
555
+ if (!text)
556
+ return;
557
+ const mapped = mapPoint(ctx, point);
558
+ const mappedSize = mapVector(ctx, { x: 0, y: Math.abs(ctx.state.font.size) || 12 });
559
+ const scaledSize = Math.hypot(mappedSize.x, mappedSize.y);
560
+ const fontSize = Math.max(1, Number.isFinite(scaledSize) && scaledSize > 0 ? scaledSize : Math.abs(ctx.state.font.size) || 12);
561
+ const family = String(ctx.state.font.family || 'sans-serif')
562
+ .replace(/\s+(?:Baltic|CE|Cyr|Greek|Tur|Vietnamese|Hebrew|Arabic|Thai|Symbol)$/i, '')
563
+ || 'sans-serif';
564
+ const attrs = [
565
+ `x="${numberCss(mapped.x)}"`,
566
+ `y="${numberCss(mapped.y)}"`,
567
+ `fill="${escapeHtml(ctx.state.textColor)}"`,
568
+ `font-size="${numberCss(fontSize)}"`,
569
+ `font-family="${escapeHtml(family)}"`,
570
+ `text-anchor="${textAnchor(ctx.state.textAlign)}"`,
571
+ `dominant-baseline="${dominantBaseline(ctx.state.textAlign)}"`,
572
+ ];
573
+ if (ctx.state.font.weight && ctx.state.font.weight >= 600)
574
+ attrs.push('font-weight="700"');
575
+ if (ctx.state.font.italic)
576
+ attrs.push('font-style="italic"');
577
+ const decorations = [ctx.state.font.underline ? 'underline' : '', ctx.state.font.strike ? 'line-through' : ''].filter(Boolean);
578
+ if (decorations.length)
579
+ attrs.push(`text-decoration="${decorations.join(' ')}"`);
580
+ if (ctx.state.font.escapement) {
581
+ attrs.push(`transform="rotate(${numberCss(ctx.state.font.escapement / 10)}, ${numberCss(mapped.x)}, ${numberCss(mapped.y)})"`);
582
+ }
583
+ addNode(ctx, `<text ${attrs.join(' ')}>${escapeHtml(text)}</text>`, [mapped]);
584
+ }
585
+ function finalizeSvg(ctx, sourceMime) {
586
+ if (!ctx.nodes.length)
587
+ return null;
588
+ const fallback = ctx.contentBounds ? normalizeRect(ctx.contentBounds) : normalizeRect(ctx.viewBox);
589
+ const viewBox = normalizeRect({
590
+ left: Number.isFinite(ctx.viewBox.left) ? ctx.viewBox.left : fallback.left,
591
+ top: Number.isFinite(ctx.viewBox.top) ? ctx.viewBox.top : fallback.top,
592
+ right: Number.isFinite(ctx.viewBox.right) ? ctx.viewBox.right : fallback.right,
593
+ bottom: Number.isFinite(ctx.viewBox.bottom) ? ctx.viewBox.bottom : fallback.bottom,
594
+ });
595
+ const width = Math.max(1, Math.round(rectWidth(viewBox)));
596
+ const height = Math.max(1, Math.round(rectHeight(viewBox)));
597
+ const svg = `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${numberCss(viewBox.left)} ${numberCss(viewBox.top)} ${numberCss(rectWidth(viewBox))} ${numberCss(rectHeight(viewBox))}" preserveAspectRatio="xMinYMin meet">\n${ctx.nodes.join('\n')}\n</svg>`;
598
+ const encoded = new TextEncoder().encode(svg);
599
+ return {
600
+ mime: 'image/svg+xml',
601
+ bytes: encoded,
602
+ dataUrl: dataUrlFromBytes(encoded, 'image/svg+xml'),
603
+ width,
604
+ height,
605
+ sourceMime,
606
+ recordCount: ctx.recordCount,
607
+ };
608
+ }
609
+ function parseWmf(bytes) {
610
+ const reader = new BinaryReader(bytes);
611
+ let offset = 0;
612
+ let placeableBounds = null;
613
+ if (reader.u32(0) === PLACEABLE_WMF_KEY && bytes.length >= 22) {
614
+ placeableBounds = normalizeRect({
615
+ left: reader.i16(6),
616
+ top: reader.i16(8),
617
+ right: reader.i16(10),
618
+ bottom: reader.i16(12),
619
+ });
620
+ offset = 22;
621
+ }
622
+ if (offset + WMF_HEADER_SIZE > bytes.length)
623
+ return null;
624
+ const header = new BinaryReader(bytes.subarray(offset));
625
+ const headerSizeWords = header.u16(2);
626
+ const numberOfObjects = header.u16(10);
627
+ const recordOffset = offset + headerSizeWords * 2;
628
+ if (recordOffset > bytes.length)
629
+ return null;
630
+ const viewBox = placeableBounds || { left: 0, top: 0, right: 1000, bottom: 1000 };
631
+ const ctx = defaultContext(rectWidth(viewBox), rectHeight(viewBox), viewBox);
632
+ ctx.objects = new Array(Math.max(0, numberOfObjects)).fill(null);
633
+ if (placeableBounds) {
634
+ ctx.state.windowOrg = { x: placeableBounds.left, y: placeableBounds.top };
635
+ ctx.state.windowExt = { x: rectWidth(placeableBounds), y: rectHeight(placeableBounds) };
636
+ ctx.state.viewportOrg = { x: placeableBounds.left, y: placeableBounds.top };
637
+ ctx.state.viewportExt = { x: rectWidth(placeableBounds), y: rectHeight(placeableBounds) };
638
+ }
639
+ let cursor = recordOffset;
640
+ while (cursor + 6 <= bytes.length) {
641
+ const recordReader = new BinaryReader(bytes.subarray(cursor));
642
+ const sizeWords = recordReader.u32(0);
643
+ const recordSize = sizeWords * 2;
644
+ if (recordSize < 6 || cursor + recordSize > bytes.length)
645
+ break;
646
+ const fn = recordReader.u16(4);
647
+ ctx.recordCount += 1;
648
+ switch (fn) {
649
+ case WMF_RECORD.META_EOF:
650
+ cursor += recordSize;
651
+ return finalizeSvg(ctx, 'image/wmf');
652
+ case WMF_RECORD.META_SETWINDOWORG:
653
+ ctx.state.windowOrg = { x: recordReader.i16(8), y: recordReader.i16(6) };
654
+ break;
655
+ case WMF_RECORD.META_SETWINDOWEXT:
656
+ ctx.state.windowExt = { x: recordReader.i16(8), y: recordReader.i16(6) };
657
+ break;
658
+ case WMF_RECORD.META_SETVIEWPORTORG:
659
+ ctx.state.viewportOrg = { x: recordReader.i16(8), y: recordReader.i16(6) };
660
+ break;
661
+ case WMF_RECORD.META_SETVIEWPORTEXT:
662
+ ctx.state.viewportExt = { x: recordReader.i16(8), y: recordReader.i16(6) };
663
+ break;
664
+ case WMF_RECORD.META_OFFSETWINDOWORG:
665
+ ctx.state.windowOrg = { x: ctx.state.windowOrg.x + recordReader.i16(8), y: ctx.state.windowOrg.y + recordReader.i16(6) };
666
+ break;
667
+ case WMF_RECORD.META_OFFSETVIEWPORTORG:
668
+ ctx.state.viewportOrg = { x: ctx.state.viewportOrg.x + recordReader.i16(8), y: ctx.state.viewportOrg.y + recordReader.i16(6) };
669
+ break;
670
+ case WMF_RECORD.META_SETBKMODE:
671
+ ctx.state.bkMode = recordReader.u16(6);
672
+ break;
673
+ case WMF_RECORD.META_SETPOLYFILLMODE:
674
+ ctx.state.polygonFillMode = recordReader.u16(6);
675
+ break;
676
+ case WMF_RECORD.META_SETBKCOLOR:
677
+ ctx.state.bkColor = colorRefToCss(recordReader.u32(6));
678
+ break;
679
+ case WMF_RECORD.META_SETTEXTCOLOR:
680
+ ctx.state.textColor = colorRefToCss(recordReader.u32(6));
681
+ break;
682
+ case WMF_RECORD.META_MOVETO:
683
+ ctx.state.currentPosition = { x: recordReader.i16(8), y: recordReader.i16(6) };
684
+ break;
685
+ case WMF_RECORD.META_LINETO: {
686
+ const end = { x: recordReader.i16(8), y: recordReader.i16(6) };
687
+ addLine(ctx, ctx.state.currentPosition, end);
688
+ break;
689
+ }
690
+ case WMF_RECORD.META_POLYLINE:
691
+ case WMF_RECORD.META_POLYGON: {
692
+ const count = recordReader.u16(6);
693
+ const points = [];
694
+ let pointOffset = 8;
695
+ for (let i = 0; i < count && pointOffset + 4 <= recordSize; i += 1) {
696
+ points.push({ x: recordReader.i16(pointOffset), y: recordReader.i16(pointOffset + 2) });
697
+ pointOffset += 4;
698
+ }
699
+ addPolyline(ctx, points, fn === WMF_RECORD.META_POLYGON);
700
+ break;
701
+ }
702
+ case WMF_RECORD.META_POLYPOLYGON: {
703
+ const polygons = recordReader.u16(6);
704
+ const counts = [];
705
+ let dataOffset = 8;
706
+ for (let i = 0; i < polygons && dataOffset + 2 <= recordSize; i += 1) {
707
+ counts.push(recordReader.u16(dataOffset));
708
+ dataOffset += 2;
709
+ }
710
+ for (const count of counts) {
711
+ const points = [];
712
+ for (let i = 0; i < count && dataOffset + 4 <= recordSize; i += 1) {
713
+ points.push({ x: recordReader.i16(dataOffset), y: recordReader.i16(dataOffset + 2) });
714
+ dataOffset += 4;
715
+ }
716
+ addPolyline(ctx, points, true);
717
+ }
718
+ break;
719
+ }
720
+ case WMF_RECORD.META_RECTANGLE: {
721
+ addRect(ctx, {
722
+ left: recordReader.i16(12),
723
+ top: recordReader.i16(10),
724
+ right: recordReader.i16(8),
725
+ bottom: recordReader.i16(6),
726
+ });
727
+ break;
728
+ }
729
+ case WMF_RECORD.META_ELLIPSE: {
730
+ addEllipse(ctx, {
731
+ left: recordReader.i16(12),
732
+ top: recordReader.i16(10),
733
+ right: recordReader.i16(8),
734
+ bottom: recordReader.i16(6),
735
+ });
736
+ break;
737
+ }
738
+ case WMF_RECORD.META_ROUNDRECT: {
739
+ addRect(ctx, {
740
+ left: recordReader.i16(16),
741
+ top: recordReader.i16(14),
742
+ right: recordReader.i16(12),
743
+ bottom: recordReader.i16(10),
744
+ }, {
745
+ x: recordReader.i16(8),
746
+ y: recordReader.i16(6),
747
+ });
748
+ break;
749
+ }
750
+ case WMF_RECORD.META_TEXTOUT: {
751
+ const length = recordReader.u16(6);
752
+ const textOffset = 8;
753
+ const paddedLength = length + (length % 2);
754
+ const text = decodeAnsi(recordReader.slice(textOffset, Math.min(length, Math.max(0, recordSize - textOffset - 4))));
755
+ const yOffset = textOffset + paddedLength;
756
+ const y = recordReader.i16(yOffset);
757
+ const x = recordReader.i16(yOffset + 2);
758
+ addText(ctx, { x, y }, text);
759
+ break;
760
+ }
761
+ case WMF_RECORD.META_CREATEPENINDIRECT: {
762
+ const style = recordReader.u16(6);
763
+ const width = recordReader.i16(8);
764
+ const color = colorRefToCss(recordReader.u32(12));
765
+ const index = ensureObjectSlot(ctx.objects);
766
+ setObject(ctx.objects, index, { kind: 'pen', value: penStyleFromCode(style, width, color) });
767
+ break;
768
+ }
769
+ case WMF_RECORD.META_CREATEBRUSHINDIRECT: {
770
+ const style = recordReader.u16(6);
771
+ const color = colorRefToCss(recordReader.u32(8));
772
+ const index = ensureObjectSlot(ctx.objects);
773
+ setObject(ctx.objects, index, { kind: 'brush', value: brushStyleFromCode(style, color) });
774
+ break;
775
+ }
776
+ case WMF_RECORD.META_CREATEFONTINDIRECT: {
777
+ const height = Math.abs(recordReader.i16(6));
778
+ const escapement = recordReader.i16(10);
779
+ const weight = recordReader.u16(14);
780
+ const italic = Boolean(recordReader.u8(16));
781
+ const underline = Boolean(recordReader.u8(17));
782
+ const strike = Boolean(recordReader.u8(18));
783
+ const faceName = decodeAnsi(recordReader.slice(24, Math.max(0, recordSize - 24))).split('\0')[0] || 'sans-serif';
784
+ const index = ensureObjectSlot(ctx.objects);
785
+ setObject(ctx.objects, index, {
786
+ kind: 'font',
787
+ value: { family: faceName, size: height || 12, weight, italic, underline, strike, escapement },
788
+ });
789
+ break;
790
+ }
791
+ case WMF_RECORD.META_SELECTOBJECT:
792
+ selectObject(ctx, recordReader.u16(6));
793
+ break;
794
+ case WMF_RECORD.META_DELETEOBJECT: {
795
+ const index = recordReader.u16(6);
796
+ if (index >= 0 && index < ctx.objects.length)
797
+ ctx.objects[index] = null;
798
+ break;
799
+ }
800
+ default:
801
+ break;
802
+ }
803
+ cursor += recordSize;
804
+ }
805
+ return finalizeSvg(ctx, 'image/wmf');
806
+ }
807
+ function readEmfPointArray(reader, offset, count, pointSize) {
808
+ const points = [];
809
+ for (let i = 0; i < count; i += 1) {
810
+ const pointOffset = offset + i * pointSize;
811
+ if (pointSize === 8) {
812
+ points.push({ x: reader.i32(pointOffset), y: reader.i32(pointOffset + 4) });
813
+ }
814
+ else {
815
+ points.push({ x: reader.i16(pointOffset), y: reader.i16(pointOffset + 2) });
816
+ }
817
+ }
818
+ return points;
819
+ }
820
+ function parseEmfText(reader, unicode) {
821
+ if (!reader.ensure(36, 40))
822
+ return null;
823
+ const point = { x: reader.i32(36), y: reader.i32(40) };
824
+ const chars = reader.u32(44);
825
+ const offString = reader.u32(48);
826
+ if (!chars || !offString || offString >= reader.length)
827
+ return null;
828
+ const byteLength = unicode ? chars * 2 : chars;
829
+ const bytes = reader.slice(offString, byteLength);
830
+ const text = unicode ? decodeUtf16(bytes) : decodeAnsi(bytes);
831
+ return { point, text };
832
+ }
833
+ function parseSmallEmfText(reader) {
834
+ if (!reader.ensure(8, 24))
835
+ return null;
836
+ const point = { x: reader.i32(8), y: reader.i32(12) };
837
+ const chars = reader.u32(16);
838
+ const options = reader.u32(20);
839
+ let textOffset = 36;
840
+ if (options & 0x0002)
841
+ textOffset += 16;
842
+ if (!chars || !reader.ensure(textOffset, chars))
843
+ return null;
844
+ const text = decodeAnsi(reader.slice(textOffset, chars));
845
+ return { point, text };
846
+ }
847
+ function readEmfEmbeddedBmp(record, offBmiOffset, cbBmiOffset, offBitsOffset, cbBitsOffset) {
848
+ const offBmi = record.u32(offBmiOffset);
849
+ const cbBmi = record.u32(cbBmiOffset);
850
+ const offBits = record.u32(offBitsOffset);
851
+ const cbBits = record.u32(cbBitsOffset);
852
+ if (!offBmi || !cbBmi || !offBits || !cbBits)
853
+ return null;
854
+ if (!record.ensure(offBmi, cbBmi) || !record.ensure(offBits, cbBits))
855
+ return null;
856
+ return bitmapInfoToBmp(record.slice(offBmi, cbBmi), record.slice(offBits, cbBits));
857
+ }
858
+ function addEmfStretchDibits(ctx, record) {
859
+ if (!record.ensure(24, 56))
860
+ return;
861
+ const bmp = readEmfEmbeddedBmp(record, 48, 52, 56, 60);
862
+ if (!bmp)
863
+ return;
864
+ addRasterImage(ctx, {
865
+ left: record.i32(24),
866
+ top: record.i32(28),
867
+ right: record.i32(24) + record.i32(72),
868
+ bottom: record.i32(28) + record.i32(76),
869
+ }, bmp);
870
+ }
871
+ function addEmfBltBitmap(ctx, record) {
872
+ if (!record.ensure(24, 76))
873
+ return;
874
+ const bmp = readEmfEmbeddedBmp(record, 84, 88, 92, 96);
875
+ if (!bmp)
876
+ return;
877
+ addRasterImage(ctx, {
878
+ left: record.i32(24),
879
+ top: record.i32(28),
880
+ right: record.i32(24) + record.i32(32),
881
+ bottom: record.i32(28) + record.i32(36),
882
+ }, bmp);
883
+ }
884
+ function closeCurrentPathFigure(ctx) {
885
+ if (!ctx.pathData.length)
886
+ return;
887
+ ctx.pathData.push('Z');
888
+ }
889
+ function parseEmf(bytes) {
890
+ const reader = new BinaryReader(bytes);
891
+ if (!reader.ensure(0, 52) || reader.u32(0) !== EMF_RECORD.EMR_HEADER)
892
+ return null;
893
+ const bounds = normalizeRect({
894
+ left: reader.i32(8),
895
+ top: reader.i32(12),
896
+ right: reader.i32(16),
897
+ bottom: reader.i32(20),
898
+ });
899
+ const viewBox = rectWidth(bounds) > 0 && rectHeight(bounds) > 0 ? bounds : { left: 0, top: 0, right: 1000, bottom: 1000 };
900
+ const ctx = defaultContext(rectWidth(viewBox), rectHeight(viewBox), viewBox);
901
+ ctx.state.windowOrg = { x: viewBox.left, y: viewBox.top };
902
+ ctx.state.windowExt = { x: rectWidth(viewBox), y: rectHeight(viewBox) };
903
+ ctx.state.viewportOrg = { x: viewBox.left, y: viewBox.top };
904
+ ctx.state.viewportExt = { x: rectWidth(viewBox), y: rectHeight(viewBox) };
905
+ let cursor = 0;
906
+ while (cursor + 8 <= bytes.length) {
907
+ const record = new BinaryReader(bytes.subarray(cursor));
908
+ const type = record.u32(0);
909
+ const size = record.u32(4);
910
+ if (size < 8 || cursor + size > bytes.length)
911
+ break;
912
+ ctx.recordCount += 1;
913
+ switch (type) {
914
+ case EMF_RECORD.EMR_EOF:
915
+ cursor += size;
916
+ return finalizeSvg(ctx, 'image/emf');
917
+ case EMF_RECORD.EMR_SETWINDOWORGEX:
918
+ ctx.state.windowOrg = { x: record.i32(8), y: record.i32(12) };
919
+ break;
920
+ case EMF_RECORD.EMR_SETWINDOWEXTEX:
921
+ ctx.state.windowExt = { x: record.i32(8), y: record.i32(12) };
922
+ break;
923
+ case EMF_RECORD.EMR_SETVIEWPORTORGEX:
924
+ ctx.state.viewportOrg = { x: record.i32(8), y: record.i32(12) };
925
+ break;
926
+ case EMF_RECORD.EMR_SETVIEWPORTEXTEX:
927
+ ctx.state.viewportExt = { x: record.i32(8), y: record.i32(12) };
928
+ break;
929
+ case EMF_RECORD.EMR_SETBKMODE:
930
+ ctx.state.bkMode = record.u32(8);
931
+ break;
932
+ case EMF_RECORD.EMR_SETPOLYFILLMODE:
933
+ ctx.state.polygonFillMode = record.u32(8);
934
+ break;
935
+ case EMF_RECORD.EMR_SETTEXTALIGN:
936
+ ctx.state.textAlign = record.u32(8);
937
+ break;
938
+ case EMF_RECORD.EMR_SETTEXTCOLOR:
939
+ ctx.state.textColor = colorRefToCss(record.u32(8));
940
+ break;
941
+ case EMF_RECORD.EMR_SETBKCOLOR:
942
+ ctx.state.bkColor = colorRefToCss(record.u32(8));
943
+ break;
944
+ case EMF_RECORD.EMR_SAVEDC:
945
+ ctx.stateStack.push(cloneGraphicsState(ctx.state));
946
+ break;
947
+ case EMF_RECORD.EMR_RESTOREDC: {
948
+ const saved = record.i32(8);
949
+ if (saved < 0) {
950
+ const steps = Math.min(ctx.stateStack.length, Math.abs(saved));
951
+ for (let i = 0; i < steps - 1; i += 1)
952
+ ctx.stateStack.pop();
953
+ const state = ctx.stateStack.pop();
954
+ if (state)
955
+ ctx.state = cloneGraphicsState(state);
956
+ }
957
+ else if (saved > 0 && saved <= ctx.stateStack.length) {
958
+ const state = ctx.stateStack[saved - 1];
959
+ ctx.stateStack.length = saved - 1;
960
+ if (state)
961
+ ctx.state = cloneGraphicsState(state);
962
+ }
963
+ break;
964
+ }
965
+ case EMF_RECORD.EMR_SETWORLDTRANSFORM:
966
+ ctx.state.worldTransform = readXForm(record, 8);
967
+ break;
968
+ case EMF_RECORD.EMR_MODIFYWORLDTRANSFORM: {
969
+ const xform = readXForm(record, 8);
970
+ const mode = record.u32(32);
971
+ if (mode === MODIFY_WORLD_TRANSFORM.MWT_IDENTITY)
972
+ ctx.state.worldTransform = { ...IDENTITY_MATRIX };
973
+ else if (mode === MODIFY_WORLD_TRANSFORM.MWT_SET)
974
+ ctx.state.worldTransform = xform;
975
+ else if (mode === MODIFY_WORLD_TRANSFORM.MWT_LEFTMULTIPLY)
976
+ ctx.state.worldTransform = multiplyMatrix(xform, ctx.state.worldTransform);
977
+ else if (mode === MODIFY_WORLD_TRANSFORM.MWT_RIGHTMULTIPLY)
978
+ ctx.state.worldTransform = multiplyMatrix(ctx.state.worldTransform, xform);
979
+ break;
980
+ }
981
+ case EMF_RECORD.EMR_MOVETOEX:
982
+ ctx.state.currentPosition = { x: record.i32(8), y: record.i32(12) };
983
+ break;
984
+ case EMF_RECORD.EMR_LINETO: {
985
+ const end = { x: record.i32(8), y: record.i32(12) };
986
+ addLine(ctx, ctx.state.currentPosition, end);
987
+ break;
988
+ }
989
+ case EMF_RECORD.EMR_POLYLINE:
990
+ case EMF_RECORD.EMR_POLYGON:
991
+ case EMF_RECORD.EMR_POLYLINE16:
992
+ case EMF_RECORD.EMR_POLYGON16: {
993
+ const count = record.u32(24);
994
+ const pointOffset = 28;
995
+ const pointSize = (type === EMF_RECORD.EMR_POLYLINE16 || type === EMF_RECORD.EMR_POLYGON16) ? 4 : 8;
996
+ const points = readEmfPointArray(record, pointOffset, count, pointSize);
997
+ addPolyline(ctx, points, type === EMF_RECORD.EMR_POLYGON || type === EMF_RECORD.EMR_POLYGON16);
998
+ break;
999
+ }
1000
+ case EMF_RECORD.EMR_POLYBEZIER:
1001
+ case EMF_RECORD.EMR_POLYBEZIER16: {
1002
+ const count = record.u32(24);
1003
+ const pointOffset = 28;
1004
+ const pointSize = type === EMF_RECORD.EMR_POLYBEZIER16 ? 4 : 8;
1005
+ const points = readEmfPointArray(record, pointOffset, count, pointSize);
1006
+ addBezierPath(ctx, points, false);
1007
+ break;
1008
+ }
1009
+ case EMF_RECORD.EMR_POLYBEZIERTO:
1010
+ case EMF_RECORD.EMR_POLYBEZIERTO16: {
1011
+ const count = record.u32(24);
1012
+ const pointOffset = 28;
1013
+ const pointSize = type === EMF_RECORD.EMR_POLYBEZIERTO16 ? 4 : 8;
1014
+ const points = readEmfPointArray(record, pointOffset, count, pointSize);
1015
+ addBezierPath(ctx, points, true);
1016
+ break;
1017
+ }
1018
+ case EMF_RECORD.EMR_POLYLINETO:
1019
+ case EMF_RECORD.EMR_POLYLINETO16: {
1020
+ const count = record.u32(24);
1021
+ const pointOffset = 28;
1022
+ const pointSize = type === EMF_RECORD.EMR_POLYLINETO16 ? 4 : 8;
1023
+ const points = readEmfPointArray(record, pointOffset, count, pointSize);
1024
+ addPolyline(ctx, [{ ...ctx.state.currentPosition }, ...points], false);
1025
+ break;
1026
+ }
1027
+ case EMF_RECORD.EMR_POLYPOLYLINE:
1028
+ case EMF_RECORD.EMR_POLYPOLYGON:
1029
+ case EMF_RECORD.EMR_POLYPOLYLINE16:
1030
+ case EMF_RECORD.EMR_POLYPOLYGON16: {
1031
+ const polygonCount = record.u32(24);
1032
+ const pointCount = record.u32(28);
1033
+ const countsOffset = 32;
1034
+ const pointSize = (type === EMF_RECORD.EMR_POLYPOLYLINE16 || type === EMF_RECORD.EMR_POLYPOLYGON16) ? 4 : 8;
1035
+ const pointsOffset = countsOffset + polygonCount * 4;
1036
+ const counts = [];
1037
+ for (let i = 0; i < polygonCount; i += 1)
1038
+ counts.push(record.u32(countsOffset + i * 4));
1039
+ const points = readEmfPointArray(record, pointsOffset, pointCount, pointSize);
1040
+ let index = 0;
1041
+ for (const count of counts) {
1042
+ addPolyline(ctx, points.slice(index, index + count), type === EMF_RECORD.EMR_POLYPOLYGON || type === EMF_RECORD.EMR_POLYPOLYGON16);
1043
+ index += count;
1044
+ }
1045
+ break;
1046
+ }
1047
+ case EMF_RECORD.EMR_RECTANGLE:
1048
+ addRect(ctx, { left: record.i32(8), top: record.i32(12), right: record.i32(16), bottom: record.i32(20) });
1049
+ break;
1050
+ case EMF_RECORD.EMR_ELLIPSE:
1051
+ addEllipse(ctx, { left: record.i32(8), top: record.i32(12), right: record.i32(16), bottom: record.i32(20) });
1052
+ break;
1053
+ case EMF_RECORD.EMR_ROUNDRECT:
1054
+ addRect(ctx, { left: record.i32(8), top: record.i32(12), right: record.i32(16), bottom: record.i32(20) }, { x: record.i32(24), y: record.i32(28) });
1055
+ break;
1056
+ case EMF_RECORD.EMR_BEGINPATH:
1057
+ ctx.capturePath = true;
1058
+ ctx.pathData = [];
1059
+ ctx.pathPoints = [];
1060
+ break;
1061
+ case EMF_RECORD.EMR_ENDPATH:
1062
+ ctx.capturePath = false;
1063
+ break;
1064
+ case EMF_RECORD.EMR_CLOSEFIGURE:
1065
+ closeCurrentPathFigure(ctx);
1066
+ break;
1067
+ case EMF_RECORD.EMR_FILLPATH:
1068
+ flushPath(ctx, fillOnlyAttributes(ctx));
1069
+ break;
1070
+ case EMF_RECORD.EMR_STROKEANDFILLPATH:
1071
+ flushPath(ctx, fillAttributes(ctx));
1072
+ break;
1073
+ case EMF_RECORD.EMR_STROKEPATH:
1074
+ flushPath(ctx, strokeAttributes(ctx, ctx.state.pen));
1075
+ break;
1076
+ case EMF_RECORD.EMR_SELECTCLIPPATH:
1077
+ ctx.capturePath = false;
1078
+ ctx.pathData = [];
1079
+ ctx.pathPoints = [];
1080
+ break;
1081
+ case EMF_RECORD.EMR_CREATEPEN: {
1082
+ const index = record.u32(8);
1083
+ const style = record.u32(12);
1084
+ const width = record.i32(16);
1085
+ const color = colorRefToCss(record.u32(24));
1086
+ setObject(ctx.objects, index, { kind: 'pen', value: penStyleFromCode(style, width, color) });
1087
+ break;
1088
+ }
1089
+ case EMF_RECORD.EMR_CREATEBRUSHINDIRECT: {
1090
+ const index = record.u32(8);
1091
+ const style = record.u32(12);
1092
+ const color = colorRefToCss(record.u32(16));
1093
+ setObject(ctx.objects, index, { kind: 'brush', value: brushStyleFromCode(style, color) });
1094
+ break;
1095
+ }
1096
+ case EMF_RECORD.EMR_EXTCREATEPEN: {
1097
+ const index = record.u32(8);
1098
+ const style = record.u32(28);
1099
+ const width = record.i32(32);
1100
+ const brushStyle = record.u32(36);
1101
+ const color = brushStyle === 1 ? 'none' : colorRefToCss(record.u32(40));
1102
+ setObject(ctx.objects, index, { kind: 'pen', value: brushStyle === 1 ? { color, width, none: true } : penStyleFromCode(style, width, color) });
1103
+ break;
1104
+ }
1105
+ case EMF_RECORD.EMR_EXTCREATEFONTINDIRECTW: {
1106
+ const index = record.u32(8);
1107
+ // LOGFONTW starts at offset 12: lfHeight(+0), ..., lfFaceName(+28).
1108
+ const height = Math.abs(record.i32(12));
1109
+ const escapement = record.i32(20);
1110
+ const weight = record.i32(28);
1111
+ const italic = Boolean(record.u8(32));
1112
+ const underline = Boolean(record.u8(33));
1113
+ const strike = Boolean(record.u8(34));
1114
+ const faceName = decodeUtf16(record.slice(40, 64)).split('\0')[0] || 'sans-serif';
1115
+ setObject(ctx.objects, index, {
1116
+ kind: 'font',
1117
+ value: { family: faceName, size: height || 12, weight, italic, underline, strike, escapement },
1118
+ });
1119
+ break;
1120
+ }
1121
+ case EMF_RECORD.EMR_SELECTOBJECT:
1122
+ selectObject(ctx, record.u32(8));
1123
+ break;
1124
+ case EMF_RECORD.EMR_DELETEOBJECT: {
1125
+ const index = record.u32(8);
1126
+ if (index >= 0 && index < ctx.objects.length)
1127
+ ctx.objects[index] = null;
1128
+ break;
1129
+ }
1130
+ case EMF_RECORD.EMR_EXTTEXTOUTA: {
1131
+ const parsed = parseEmfText(record, false);
1132
+ if (parsed)
1133
+ addText(ctx, parsed.point, parsed.text);
1134
+ break;
1135
+ }
1136
+ case EMF_RECORD.EMR_EXTTEXTOUTW: {
1137
+ const parsed = parseEmfText(record, true);
1138
+ if (parsed)
1139
+ addText(ctx, parsed.point, parsed.text);
1140
+ break;
1141
+ }
1142
+ case EMF_RECORD.EMR_SMALLTEXTOUT: {
1143
+ const parsed = parseSmallEmfText(record);
1144
+ if (parsed)
1145
+ addText(ctx, parsed.point, parsed.text);
1146
+ break;
1147
+ }
1148
+ case EMF_RECORD.EMR_STRETCHDIBITS:
1149
+ addEmfStretchDibits(ctx, record);
1150
+ break;
1151
+ case EMF_RECORD.EMR_BITBLT:
1152
+ case EMF_RECORD.EMR_STRETCHBLT:
1153
+ case EMF_RECORD.EMR_ALPHABLEND:
1154
+ addEmfBltBitmap(ctx, record);
1155
+ break;
1156
+ default:
1157
+ break;
1158
+ }
1159
+ cursor += size;
1160
+ }
1161
+ return finalizeSvg(ctx, 'image/emf');
1162
+ }
1163
+ export function convertMetafileToSvg(mime, bytes) {
1164
+ if (!bytes.length)
1165
+ return null;
1166
+ if (/image\/emf/i.test(mime))
1167
+ return parseEmf(bytes);
1168
+ if (/image\/wmf/i.test(mime))
1169
+ return parseWmf(bytes);
1170
+ return null;
1171
+ }