@s8fy/emf2svg 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +118 -0
- package/dist/index.cjs +3756 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +3752 -0
- package/dist/index.umd.js +3760 -0
- package/package.json +69 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3756 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/binary-reader.ts
|
|
3
|
+
/**
|
|
4
|
+
* Little-endian binary reader built on DataView.
|
|
5
|
+
* EMF files are always little-endian.
|
|
6
|
+
*/
|
|
7
|
+
var BinaryReader = class BinaryReader {
|
|
8
|
+
constructor(buffer) {
|
|
9
|
+
if (buffer instanceof Uint8Array) this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
10
|
+
else this.view = new DataView(buffer);
|
|
11
|
+
this.pos = 0;
|
|
12
|
+
}
|
|
13
|
+
get position() {
|
|
14
|
+
return this.pos;
|
|
15
|
+
}
|
|
16
|
+
get remaining() {
|
|
17
|
+
return this.view.byteLength - this.pos;
|
|
18
|
+
}
|
|
19
|
+
get length() {
|
|
20
|
+
return this.view.byteLength;
|
|
21
|
+
}
|
|
22
|
+
seek(offset) {
|
|
23
|
+
if (offset < 0 || offset > this.view.byteLength) throw new RangeError(`Seek offset ${offset} out of bounds [0, ${this.view.byteLength}]`);
|
|
24
|
+
this.pos = offset;
|
|
25
|
+
}
|
|
26
|
+
skip(bytes) {
|
|
27
|
+
this.seek(this.pos + bytes);
|
|
28
|
+
}
|
|
29
|
+
readUint8() {
|
|
30
|
+
const val = this.view.getUint8(this.pos);
|
|
31
|
+
this.pos += 1;
|
|
32
|
+
return val;
|
|
33
|
+
}
|
|
34
|
+
readInt16() {
|
|
35
|
+
const val = this.view.getInt16(this.pos, true);
|
|
36
|
+
this.pos += 2;
|
|
37
|
+
return val;
|
|
38
|
+
}
|
|
39
|
+
readUint16() {
|
|
40
|
+
const val = this.view.getUint16(this.pos, true);
|
|
41
|
+
this.pos += 2;
|
|
42
|
+
return val;
|
|
43
|
+
}
|
|
44
|
+
readInt32() {
|
|
45
|
+
const val = this.view.getInt32(this.pos, true);
|
|
46
|
+
this.pos += 4;
|
|
47
|
+
return val;
|
|
48
|
+
}
|
|
49
|
+
readUint32() {
|
|
50
|
+
const val = this.view.getUint32(this.pos, true);
|
|
51
|
+
this.pos += 4;
|
|
52
|
+
return val;
|
|
53
|
+
}
|
|
54
|
+
readFloat32() {
|
|
55
|
+
const val = this.view.getFloat32(this.pos, true);
|
|
56
|
+
this.pos += 4;
|
|
57
|
+
return val;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read a UTF-16LE string of given character count.
|
|
61
|
+
*/
|
|
62
|
+
readUtf16String(charCount) {
|
|
63
|
+
const codes = [];
|
|
64
|
+
for (let i = 0; i < charCount; i++) {
|
|
65
|
+
const code = this.view.getUint16(this.pos, true);
|
|
66
|
+
this.pos += 2;
|
|
67
|
+
if (code === 0) {
|
|
68
|
+
this.pos += (charCount - i - 1) * 2;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
codes.push(code);
|
|
72
|
+
}
|
|
73
|
+
return String.fromCharCode(...codes);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read raw bytes as a new Uint8Array (copies data).
|
|
77
|
+
*/
|
|
78
|
+
readBytes(count) {
|
|
79
|
+
if (this.pos + count > this.view.byteLength) throw new RangeError(`Cannot read ${count} bytes at position ${this.pos}, only ${this.remaining} remaining`);
|
|
80
|
+
const result = new Uint8Array(count);
|
|
81
|
+
const src = new Uint8Array(this.view.buffer, this.view.byteOffset + this.pos, count);
|
|
82
|
+
result.set(src);
|
|
83
|
+
this.pos += count;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read a slice view without copying (for large bitmap data).
|
|
88
|
+
*/
|
|
89
|
+
readSlice(count) {
|
|
90
|
+
if (this.pos + count > this.view.byteLength) throw new RangeError(`Cannot slice ${count} bytes at position ${this.pos}, only ${this.remaining} remaining`);
|
|
91
|
+
const slice = new Uint8Array(this.view.buffer, this.view.byteOffset + this.pos, count);
|
|
92
|
+
this.pos += count;
|
|
93
|
+
return slice;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Create a sub-reader for a specific range (for record parsing).
|
|
97
|
+
*/
|
|
98
|
+
subReader(offset, length) {
|
|
99
|
+
const slice = new Uint8Array(this.view.buffer, this.view.byteOffset + offset, length);
|
|
100
|
+
return new BinaryReader(slice);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/constants.ts
|
|
105
|
+
const EMF_SIGNATURE = 1179469088;
|
|
106
|
+
let PenStyle;
|
|
107
|
+
(function(_PenStyle) {
|
|
108
|
+
_PenStyle.PS_SOLID = 0;
|
|
109
|
+
_PenStyle.PS_DASH = 1;
|
|
110
|
+
_PenStyle.PS_DOT = 2;
|
|
111
|
+
_PenStyle.PS_DASHDOT = 3;
|
|
112
|
+
_PenStyle.PS_DASHDOTDOT = 4;
|
|
113
|
+
_PenStyle.PS_NULL = 5;
|
|
114
|
+
_PenStyle.PS_INSIDEFRAME = 6;
|
|
115
|
+
_PenStyle.PS_USERSTYLE = 7;
|
|
116
|
+
_PenStyle.PS_ALTERNATE = 8;
|
|
117
|
+
_PenStyle.PS_STYLE_MASK = 15;
|
|
118
|
+
_PenStyle.PS_ENDCAP_ROUND = 0;
|
|
119
|
+
_PenStyle.PS_ENDCAP_SQUARE = 256;
|
|
120
|
+
_PenStyle.PS_ENDCAP_FLAT = 512;
|
|
121
|
+
_PenStyle.PS_ENDCAP_MASK = 3840;
|
|
122
|
+
_PenStyle.PS_JOIN_ROUND = 0;
|
|
123
|
+
_PenStyle.PS_JOIN_BEVEL = 4096;
|
|
124
|
+
_PenStyle.PS_JOIN_MITER = 8192;
|
|
125
|
+
_PenStyle.PS_JOIN_MASK = 61440;
|
|
126
|
+
_PenStyle.PS_COSMETIC = 0;
|
|
127
|
+
_PenStyle.PS_GEOMETRIC = 65536;
|
|
128
|
+
_PenStyle.PS_TYPE_MASK = 983040;
|
|
129
|
+
})(PenStyle || (PenStyle = {}));
|
|
130
|
+
let TextAlign;
|
|
131
|
+
(function(_TextAlign) {
|
|
132
|
+
_TextAlign.TA_NOUPDATECP = 0;
|
|
133
|
+
_TextAlign.TA_UPDATECP = 1;
|
|
134
|
+
_TextAlign.TA_LEFT = 0;
|
|
135
|
+
_TextAlign.TA_RIGHT = 2;
|
|
136
|
+
_TextAlign.TA_CENTER = 6;
|
|
137
|
+
_TextAlign.TA_TOP = 0;
|
|
138
|
+
_TextAlign.TA_BOTTOM = 8;
|
|
139
|
+
_TextAlign.TA_BASELINE = 24;
|
|
140
|
+
_TextAlign.TA_RTLREADING = 256;
|
|
141
|
+
})(TextAlign || (TextAlign = {}));
|
|
142
|
+
const SYMBOL_CHAR_MAP = {
|
|
143
|
+
34: "∀",
|
|
144
|
+
36: "∃",
|
|
145
|
+
39: "∋",
|
|
146
|
+
40: "(",
|
|
147
|
+
41: ")",
|
|
148
|
+
42: "∗",
|
|
149
|
+
43: "+",
|
|
150
|
+
45: "−",
|
|
151
|
+
61: "=",
|
|
152
|
+
64: "≅",
|
|
153
|
+
65: "Α",
|
|
154
|
+
66: "Β",
|
|
155
|
+
67: "Χ",
|
|
156
|
+
68: "Δ",
|
|
157
|
+
69: "Ε",
|
|
158
|
+
70: "Φ",
|
|
159
|
+
71: "Γ",
|
|
160
|
+
72: "Η",
|
|
161
|
+
73: "Ι",
|
|
162
|
+
74: "ϑ",
|
|
163
|
+
75: "Κ",
|
|
164
|
+
76: "Λ",
|
|
165
|
+
77: "Μ",
|
|
166
|
+
78: "Ν",
|
|
167
|
+
79: "Ο",
|
|
168
|
+
80: "Π",
|
|
169
|
+
81: "Θ",
|
|
170
|
+
82: "Ρ",
|
|
171
|
+
83: "Σ",
|
|
172
|
+
84: "Τ",
|
|
173
|
+
85: "Υ",
|
|
174
|
+
86: "ς",
|
|
175
|
+
87: "Ω",
|
|
176
|
+
88: "Ξ",
|
|
177
|
+
89: "Ψ",
|
|
178
|
+
90: "Ζ",
|
|
179
|
+
92: "∴",
|
|
180
|
+
94: "⊥",
|
|
181
|
+
96: "‾",
|
|
182
|
+
97: "α",
|
|
183
|
+
98: "β",
|
|
184
|
+
99: "χ",
|
|
185
|
+
100: "δ",
|
|
186
|
+
101: "ε",
|
|
187
|
+
102: "φ",
|
|
188
|
+
103: "γ",
|
|
189
|
+
104: "η",
|
|
190
|
+
105: "ι",
|
|
191
|
+
106: "ϕ",
|
|
192
|
+
107: "κ",
|
|
193
|
+
108: "λ",
|
|
194
|
+
109: "μ",
|
|
195
|
+
110: "ν",
|
|
196
|
+
111: "ο",
|
|
197
|
+
112: "π",
|
|
198
|
+
113: "θ",
|
|
199
|
+
114: "ρ",
|
|
200
|
+
115: "σ",
|
|
201
|
+
116: "τ",
|
|
202
|
+
117: "υ",
|
|
203
|
+
118: "ϖ",
|
|
204
|
+
119: "ω",
|
|
205
|
+
120: "ξ",
|
|
206
|
+
121: "ψ",
|
|
207
|
+
122: "ζ",
|
|
208
|
+
126: "∼",
|
|
209
|
+
160: "€",
|
|
210
|
+
161: "ϒ",
|
|
211
|
+
162: "′",
|
|
212
|
+
163: "≤",
|
|
213
|
+
164: "⁄",
|
|
214
|
+
165: "∞",
|
|
215
|
+
166: "ƒ",
|
|
216
|
+
167: "♣",
|
|
217
|
+
168: "♦",
|
|
218
|
+
169: "♥",
|
|
219
|
+
170: "♠",
|
|
220
|
+
171: "↔",
|
|
221
|
+
172: "←",
|
|
222
|
+
173: "↑",
|
|
223
|
+
174: "→",
|
|
224
|
+
175: "↓",
|
|
225
|
+
176: "°",
|
|
226
|
+
177: "±",
|
|
227
|
+
178: "″",
|
|
228
|
+
179: "≥",
|
|
229
|
+
180: "×",
|
|
230
|
+
181: "∝",
|
|
231
|
+
182: "∂",
|
|
232
|
+
183: "•",
|
|
233
|
+
184: "÷",
|
|
234
|
+
185: "≠",
|
|
235
|
+
186: "≡",
|
|
236
|
+
187: "≈",
|
|
237
|
+
188: "…",
|
|
238
|
+
189: "⏐",
|
|
239
|
+
190: "⎯",
|
|
240
|
+
191: "↵",
|
|
241
|
+
192: "ℵ",
|
|
242
|
+
193: "ℑ",
|
|
243
|
+
194: "ℜ",
|
|
244
|
+
195: "℘",
|
|
245
|
+
196: "⊗",
|
|
246
|
+
197: "⊕",
|
|
247
|
+
198: "∅",
|
|
248
|
+
199: "∩",
|
|
249
|
+
200: "∪",
|
|
250
|
+
201: "⊃",
|
|
251
|
+
202: "⊇",
|
|
252
|
+
203: "⊄",
|
|
253
|
+
204: "⊂",
|
|
254
|
+
205: "⊆",
|
|
255
|
+
206: "∈",
|
|
256
|
+
207: "∉",
|
|
257
|
+
208: "∠",
|
|
258
|
+
209: "∇",
|
|
259
|
+
210: "®",
|
|
260
|
+
211: "©",
|
|
261
|
+
212: "™",
|
|
262
|
+
213: "∏",
|
|
263
|
+
214: "√",
|
|
264
|
+
215: "⋅",
|
|
265
|
+
216: "¬",
|
|
266
|
+
217: "∧",
|
|
267
|
+
218: "∨",
|
|
268
|
+
219: "⇔",
|
|
269
|
+
220: "⇐",
|
|
270
|
+
221: "⇑",
|
|
271
|
+
222: "⇒",
|
|
272
|
+
223: "⇓",
|
|
273
|
+
224: "◊",
|
|
274
|
+
225: "〈",
|
|
275
|
+
226: "®",
|
|
276
|
+
227: "©",
|
|
277
|
+
228: "™",
|
|
278
|
+
229: "∑",
|
|
279
|
+
230: "⎛",
|
|
280
|
+
231: "⎜",
|
|
281
|
+
232: "⎝",
|
|
282
|
+
233: "⎡",
|
|
283
|
+
234: "⎢",
|
|
284
|
+
235: "⎣",
|
|
285
|
+
236: "⎧",
|
|
286
|
+
237: "⎨",
|
|
287
|
+
238: "⎩",
|
|
288
|
+
239: "⎪",
|
|
289
|
+
241: "〉",
|
|
290
|
+
242: "∫",
|
|
291
|
+
243: "⌠",
|
|
292
|
+
244: "⎮",
|
|
293
|
+
245: "⌡",
|
|
294
|
+
246: "⎞",
|
|
295
|
+
247: "⎟",
|
|
296
|
+
248: "⎠",
|
|
297
|
+
249: "⎤",
|
|
298
|
+
250: "⎥",
|
|
299
|
+
251: "⎦",
|
|
300
|
+
252: "⎫",
|
|
301
|
+
253: "⎬",
|
|
302
|
+
254: "⎭"
|
|
303
|
+
};
|
|
304
|
+
const MT_EXTRA_CHAR_MAP = { 76: "⋯" };
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/transform.ts
|
|
307
|
+
/**
|
|
308
|
+
* Affine transformation matrix operations.
|
|
309
|
+
* Matrix format: [a, b, c, d, tx, ty]
|
|
310
|
+
*
|
|
311
|
+
* Transformation: [x', y'] = [a*x + c*y + tx, b*x + d*y + ty]
|
|
312
|
+
*
|
|
313
|
+
* All operations return new matrices (immutable style).
|
|
314
|
+
*/
|
|
315
|
+
/** Identity matrix */
|
|
316
|
+
function identity() {
|
|
317
|
+
return [
|
|
318
|
+
1,
|
|
319
|
+
0,
|
|
320
|
+
0,
|
|
321
|
+
1,
|
|
322
|
+
0,
|
|
323
|
+
0
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Multiply two matrices: result = A * B
|
|
328
|
+
* This means: first apply B, then apply A.
|
|
329
|
+
*/
|
|
330
|
+
function multiply(a, b) {
|
|
331
|
+
return [
|
|
332
|
+
a[0] * b[0] + a[2] * b[1],
|
|
333
|
+
a[1] * b[0] + a[3] * b[1],
|
|
334
|
+
a[0] * b[2] + a[2] * b[3],
|
|
335
|
+
a[1] * b[2] + a[3] * b[3],
|
|
336
|
+
a[0] * b[4] + a[2] * b[5] + a[4],
|
|
337
|
+
a[1] * b[4] + a[3] * b[5] + a[5]
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
/** Transform a point by a matrix */
|
|
341
|
+
function transformPoint(m, p) {
|
|
342
|
+
return {
|
|
343
|
+
x: m[0] * p.x + m[2] * p.y + m[4],
|
|
344
|
+
y: m[1] * p.x + m[3] * p.y + m[5]
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** Convert matrix to SVG transform string: "matrix(a,b,c,d,tx,ty)" */
|
|
348
|
+
function toSvgString(m) {
|
|
349
|
+
return `matrix(${m[0]},${m[1]},${m[2]},${m[3]},${m[4]},${m[5]})`;
|
|
350
|
+
}
|
|
351
|
+
/** Check if a matrix is the identity matrix */
|
|
352
|
+
function isIdentity(m) {
|
|
353
|
+
return Math.abs(m[0] - 1) < 1e-10 && Math.abs(m[1]) < 1e-10 && Math.abs(m[2]) < 1e-10 && Math.abs(m[3] - 1) < 1e-10 && Math.abs(m[4]) < 1e-10 && Math.abs(m[5]) < 1e-10;
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/device-context.ts
|
|
357
|
+
const DEFAULT_COLOR = {
|
|
358
|
+
r: 0,
|
|
359
|
+
g: 0,
|
|
360
|
+
b: 0,
|
|
361
|
+
a: 255
|
|
362
|
+
};
|
|
363
|
+
const WHITE_COLOR = {
|
|
364
|
+
r: 255,
|
|
365
|
+
g: 255,
|
|
366
|
+
b: 255,
|
|
367
|
+
a: 255
|
|
368
|
+
};
|
|
369
|
+
function defaultBrush() {
|
|
370
|
+
return {
|
|
371
|
+
type: "brush",
|
|
372
|
+
style: 0,
|
|
373
|
+
color: WHITE_COLOR,
|
|
374
|
+
hatch: 0
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function defaultPen() {
|
|
378
|
+
return {
|
|
379
|
+
type: "pen",
|
|
380
|
+
style: PenStyle.PS_SOLID,
|
|
381
|
+
width: 1,
|
|
382
|
+
color: DEFAULT_COLOR,
|
|
383
|
+
endCap: 0,
|
|
384
|
+
lineJoin: 0
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function defaultFont() {
|
|
388
|
+
return {
|
|
389
|
+
type: "font",
|
|
390
|
+
height: 12,
|
|
391
|
+
width: 0,
|
|
392
|
+
weight: 400,
|
|
393
|
+
italic: false,
|
|
394
|
+
underline: false,
|
|
395
|
+
strikeOut: false,
|
|
396
|
+
charSet: 0,
|
|
397
|
+
faceName: "Arial",
|
|
398
|
+
escapement: 0,
|
|
399
|
+
orientation: 0
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function cloneState(s) {
|
|
403
|
+
return {
|
|
404
|
+
worldTransform: [...s.worldTransform],
|
|
405
|
+
windowOrgX: s.windowOrgX,
|
|
406
|
+
windowOrgY: s.windowOrgY,
|
|
407
|
+
windowExtX: s.windowExtX,
|
|
408
|
+
windowExtY: s.windowExtY,
|
|
409
|
+
viewportOrgX: s.viewportOrgX,
|
|
410
|
+
viewportOrgY: s.viewportOrgY,
|
|
411
|
+
viewportExtX: s.viewportExtX,
|
|
412
|
+
viewportExtY: s.viewportExtY,
|
|
413
|
+
hasExplicitWindowExt: s.hasExplicitWindowExt,
|
|
414
|
+
hasExplicitViewportExt: s.hasExplicitViewportExt,
|
|
415
|
+
currentPosX: s.currentPosX,
|
|
416
|
+
currentPosY: s.currentPosY,
|
|
417
|
+
textColor: { ...s.textColor },
|
|
418
|
+
bkColor: { ...s.bkColor },
|
|
419
|
+
bkMode: s.bkMode,
|
|
420
|
+
textAlign: s.textAlign,
|
|
421
|
+
polyFillMode: s.polyFillMode,
|
|
422
|
+
miterLimit: s.miterLimit,
|
|
423
|
+
mapMode: s.mapMode,
|
|
424
|
+
brush: {
|
|
425
|
+
...s.brush,
|
|
426
|
+
color: { ...s.brush.color }
|
|
427
|
+
},
|
|
428
|
+
pen: {
|
|
429
|
+
...s.pen,
|
|
430
|
+
color: { ...s.pen.color }
|
|
431
|
+
},
|
|
432
|
+
font: { ...s.font },
|
|
433
|
+
arcDirection: s.arcDirection,
|
|
434
|
+
clipPathIds: [...s.clipPathIds]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
var DeviceContext = class {
|
|
438
|
+
constructor() {
|
|
439
|
+
this.stack = [];
|
|
440
|
+
this.state = {
|
|
441
|
+
worldTransform: identity(),
|
|
442
|
+
windowOrgX: 0,
|
|
443
|
+
windowOrgY: 0,
|
|
444
|
+
windowExtX: 1,
|
|
445
|
+
windowExtY: 1,
|
|
446
|
+
viewportOrgX: 0,
|
|
447
|
+
viewportOrgY: 0,
|
|
448
|
+
viewportExtX: 1,
|
|
449
|
+
viewportExtY: 1,
|
|
450
|
+
hasExplicitWindowExt: false,
|
|
451
|
+
hasExplicitViewportExt: false,
|
|
452
|
+
currentPosX: 0,
|
|
453
|
+
currentPosY: 0,
|
|
454
|
+
textColor: { ...DEFAULT_COLOR },
|
|
455
|
+
bkColor: { ...WHITE_COLOR },
|
|
456
|
+
bkMode: 2,
|
|
457
|
+
textAlign: TextAlign.TA_LEFT | TextAlign.TA_TOP,
|
|
458
|
+
polyFillMode: 1,
|
|
459
|
+
miterLimit: 10,
|
|
460
|
+
mapMode: 1,
|
|
461
|
+
brush: defaultBrush(),
|
|
462
|
+
pen: defaultPen(),
|
|
463
|
+
font: defaultFont(),
|
|
464
|
+
arcDirection: 1,
|
|
465
|
+
clipPathIds: []
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
save() {
|
|
469
|
+
this.stack.push(cloneState(this.state));
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* RestoreDC: if savedDC is negative, pop |savedDC| states.
|
|
473
|
+
* If positive, pop until stack size equals savedDC.
|
|
474
|
+
*/
|
|
475
|
+
restore(savedDC) {
|
|
476
|
+
if (savedDC < 0) {
|
|
477
|
+
const count = -savedDC;
|
|
478
|
+
for (let i = 0; i < count && this.stack.length > 0; i++) this.state = this.stack.pop();
|
|
479
|
+
} else while (this.stack.length >= savedDC && this.stack.length > 0) this.state = this.stack.pop();
|
|
480
|
+
}
|
|
481
|
+
/** Get fill style for SVG from current brush */
|
|
482
|
+
getFillStyle() {
|
|
483
|
+
return {
|
|
484
|
+
enabled: true,
|
|
485
|
+
color: { ...this.state.brush.color },
|
|
486
|
+
brushStyle: this.state.brush.style
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** Get stroke style for SVG from current pen */
|
|
490
|
+
getStrokeStyle() {
|
|
491
|
+
return {
|
|
492
|
+
enabled: true,
|
|
493
|
+
color: { ...this.state.pen.color },
|
|
494
|
+
width: this.state.pen.width,
|
|
495
|
+
penStyle: this.state.pen.style
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Compute the combined Window → Viewport → World coordinate transform.
|
|
500
|
+
* Logical coord → (window/viewport mapping) → device coord → (world transform) → final.
|
|
501
|
+
*
|
|
502
|
+
* For SVG output, we use the viewBox for window/viewport mapping and
|
|
503
|
+
* emit the world transform as a <g transform="...">.
|
|
504
|
+
*/
|
|
505
|
+
getWindowToViewportScaleX() {
|
|
506
|
+
return this.state.windowExtX !== 0 ? this.state.viewportExtX / this.state.windowExtX : 1;
|
|
507
|
+
}
|
|
508
|
+
getWindowToViewportScaleY() {
|
|
509
|
+
return this.state.windowExtY !== 0 ? this.state.viewportExtY / this.state.windowExtY : 1;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/dib-decoder.ts
|
|
514
|
+
/**
|
|
515
|
+
* DIB (Device Independent Bitmap) decoder.
|
|
516
|
+
* Repacks EMF-embedded DIB data into a complete BMP file,
|
|
517
|
+
* then encodes as base64 data URI. Browsers natively render BMP.
|
|
518
|
+
*/
|
|
519
|
+
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
520
|
+
/**
|
|
521
|
+
* Decode a DIB embedded in an EMF record into a data URI.
|
|
522
|
+
* DIB = BITMAPINFOHEADER + optional color table + pixel data.
|
|
523
|
+
* We prepend a BITMAPFILEHEADER to make it a valid BMP file.
|
|
524
|
+
*
|
|
525
|
+
* @param dibData - Raw DIB bytes (starting with BITMAPINFOHEADER)
|
|
526
|
+
* @param dibHeaderSize - Size of DIB data from the header offset
|
|
527
|
+
* @param bitmapData - Raw bitmap bits
|
|
528
|
+
* @returns data:image/bmp;base64,... URI string
|
|
529
|
+
*/
|
|
530
|
+
function dibToDataUri(dibData, bitmapData) {
|
|
531
|
+
const headerSize = readUint32(dibData, 0);
|
|
532
|
+
let height = readInt32(dibData, 8);
|
|
533
|
+
const bitCount = readUint16(dibData, 14);
|
|
534
|
+
if (height < 0) height = -height;
|
|
535
|
+
let colorTableSize = 0;
|
|
536
|
+
if (bitCount <= 8) {
|
|
537
|
+
let clrUsed = readUint32(dibData, 32);
|
|
538
|
+
if (clrUsed === 0) clrUsed = 1 << bitCount;
|
|
539
|
+
colorTableSize = clrUsed * 4;
|
|
540
|
+
}
|
|
541
|
+
const dibHeaderTotal = headerSize + colorTableSize;
|
|
542
|
+
const fileHeaderSize = 14;
|
|
543
|
+
const bfOffBits = fileHeaderSize + dibHeaderTotal;
|
|
544
|
+
const totalSize = bfOffBits + bitmapData.length;
|
|
545
|
+
const bmp = new Uint8Array(totalSize);
|
|
546
|
+
bmp[0] = 66;
|
|
547
|
+
bmp[1] = 77;
|
|
548
|
+
writeUint32(bmp, 2, totalSize);
|
|
549
|
+
writeUint32(bmp, 10, bfOffBits);
|
|
550
|
+
bmp.set(dibData.subarray(0, dibHeaderTotal), fileHeaderSize);
|
|
551
|
+
bmp.set(bitmapData, bfOffBits);
|
|
552
|
+
return "data:image/bmp;base64," + uint8ToBase64(bmp);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Try to detect if DIB data is actually a PNG or JPEG
|
|
556
|
+
* (some EMF files embed compressed images directly).
|
|
557
|
+
*/
|
|
558
|
+
function detectEmbeddedFormat(data) {
|
|
559
|
+
if (data.length < 4) return null;
|
|
560
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "png";
|
|
561
|
+
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "jpeg";
|
|
562
|
+
if (data[0] === 66 && data[1] === 77) return "bmp";
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Create a data URI from raw image bytes with detected format.
|
|
567
|
+
*/
|
|
568
|
+
function rawImageToDataUri(data) {
|
|
569
|
+
const format = detectEmbeddedFormat(data);
|
|
570
|
+
if (format === "png") return "data:image/png;base64," + uint8ToBase64(data);
|
|
571
|
+
if (format === "jpeg") return "data:image/jpeg;base64," + uint8ToBase64(data);
|
|
572
|
+
if (format === "bmp") return "data:image/bmp;base64," + uint8ToBase64(data);
|
|
573
|
+
return "data:image/bmp;base64," + uint8ToBase64(data);
|
|
574
|
+
}
|
|
575
|
+
function readUint16(buf, offset) {
|
|
576
|
+
return buf[offset] | buf[offset + 1] << 8;
|
|
577
|
+
}
|
|
578
|
+
function readInt32(buf, offset) {
|
|
579
|
+
return buf[offset] | buf[offset + 1] << 8 | buf[offset + 2] << 16 | buf[offset + 3] << 24;
|
|
580
|
+
}
|
|
581
|
+
function readUint32(buf, offset) {
|
|
582
|
+
return readInt32(buf, offset) >>> 0;
|
|
583
|
+
}
|
|
584
|
+
function writeUint32(buf, offset, value) {
|
|
585
|
+
buf[offset] = value & 255;
|
|
586
|
+
buf[offset + 1] = value >> 8 & 255;
|
|
587
|
+
buf[offset + 2] = value >> 16 & 255;
|
|
588
|
+
buf[offset + 3] = value >> 24 & 255;
|
|
589
|
+
}
|
|
590
|
+
/** Pure JS base64 encoder (no btoa dependency for non-browser envs) */
|
|
591
|
+
function uint8ToBase64(bytes) {
|
|
592
|
+
const len = bytes.length;
|
|
593
|
+
let result = "";
|
|
594
|
+
let i = 0;
|
|
595
|
+
for (; i + 2 < len; i += 3) {
|
|
596
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
|
|
597
|
+
result += BASE64_CHARS[n >> 18 & 63];
|
|
598
|
+
result += BASE64_CHARS[n >> 12 & 63];
|
|
599
|
+
result += BASE64_CHARS[n >> 6 & 63];
|
|
600
|
+
result += BASE64_CHARS[n & 63];
|
|
601
|
+
}
|
|
602
|
+
if (i + 1 === len) {
|
|
603
|
+
const n = bytes[i] << 16;
|
|
604
|
+
result += BASE64_CHARS[n >> 18 & 63];
|
|
605
|
+
result += BASE64_CHARS[n >> 12 & 63];
|
|
606
|
+
result += "==";
|
|
607
|
+
} else if (i + 2 === len) {
|
|
608
|
+
const n = bytes[i] << 16 | bytes[i + 1] << 8;
|
|
609
|
+
result += BASE64_CHARS[n >> 18 & 63];
|
|
610
|
+
result += BASE64_CHARS[n >> 12 & 63];
|
|
611
|
+
result += BASE64_CHARS[n >> 6 & 63];
|
|
612
|
+
result += "=";
|
|
613
|
+
}
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/object-table.ts
|
|
618
|
+
/**
|
|
619
|
+
* GDI Object Table.
|
|
620
|
+
* Manages create/select/delete of GDI objects (pens, brushes, fonts).
|
|
621
|
+
* Handles stock objects (predefined system objects).
|
|
622
|
+
*/
|
|
623
|
+
var ObjectTable = class {
|
|
624
|
+
constructor() {
|
|
625
|
+
this.objects = /* @__PURE__ */ new Map();
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Create a GDI object at the given handle index.
|
|
629
|
+
*/
|
|
630
|
+
create(index, obj) {
|
|
631
|
+
this.objects.set(index, obj);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Delete a GDI object by handle index.
|
|
635
|
+
*/
|
|
636
|
+
delete(index) {
|
|
637
|
+
this.objects.delete(index);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Select an object (by handle or stock object ID) into the device context.
|
|
641
|
+
* Returns true if the object was found and applied.
|
|
642
|
+
*/
|
|
643
|
+
select(index, dc) {
|
|
644
|
+
if (index & 2147483648) return this.selectStockObject(index, dc);
|
|
645
|
+
const obj = this.objects.get(index);
|
|
646
|
+
if (!obj) return false;
|
|
647
|
+
this.applyObject(obj, dc);
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
applyObject(obj, dc) {
|
|
651
|
+
switch (obj.type) {
|
|
652
|
+
case "brush":
|
|
653
|
+
dc.state.brush = {
|
|
654
|
+
...obj,
|
|
655
|
+
color: { ...obj.color }
|
|
656
|
+
};
|
|
657
|
+
break;
|
|
658
|
+
case "pen":
|
|
659
|
+
dc.state.pen = {
|
|
660
|
+
...obj,
|
|
661
|
+
color: { ...obj.color }
|
|
662
|
+
};
|
|
663
|
+
break;
|
|
664
|
+
case "font": dc.state.font = { ...obj };
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
selectStockObject(index, dc) {
|
|
668
|
+
const WHITE = {
|
|
669
|
+
r: 255,
|
|
670
|
+
g: 255,
|
|
671
|
+
b: 255,
|
|
672
|
+
a: 255
|
|
673
|
+
};
|
|
674
|
+
const BLACK = {
|
|
675
|
+
r: 0,
|
|
676
|
+
g: 0,
|
|
677
|
+
b: 0,
|
|
678
|
+
a: 255
|
|
679
|
+
};
|
|
680
|
+
const LTGRAY = {
|
|
681
|
+
r: 192,
|
|
682
|
+
g: 192,
|
|
683
|
+
b: 192,
|
|
684
|
+
a: 255
|
|
685
|
+
};
|
|
686
|
+
const GRAY = {
|
|
687
|
+
r: 128,
|
|
688
|
+
g: 128,
|
|
689
|
+
b: 128,
|
|
690
|
+
a: 255
|
|
691
|
+
};
|
|
692
|
+
const DKGRAY = {
|
|
693
|
+
r: 64,
|
|
694
|
+
g: 64,
|
|
695
|
+
b: 64,
|
|
696
|
+
a: 255
|
|
697
|
+
};
|
|
698
|
+
switch (index) {
|
|
699
|
+
case 2147483648:
|
|
700
|
+
dc.state.brush = {
|
|
701
|
+
type: "brush",
|
|
702
|
+
style: 0,
|
|
703
|
+
color: WHITE,
|
|
704
|
+
hatch: 0
|
|
705
|
+
};
|
|
706
|
+
return true;
|
|
707
|
+
case 2147483649:
|
|
708
|
+
dc.state.brush = {
|
|
709
|
+
type: "brush",
|
|
710
|
+
style: 0,
|
|
711
|
+
color: LTGRAY,
|
|
712
|
+
hatch: 0
|
|
713
|
+
};
|
|
714
|
+
return true;
|
|
715
|
+
case 2147483650:
|
|
716
|
+
dc.state.brush = {
|
|
717
|
+
type: "brush",
|
|
718
|
+
style: 0,
|
|
719
|
+
color: GRAY,
|
|
720
|
+
hatch: 0
|
|
721
|
+
};
|
|
722
|
+
return true;
|
|
723
|
+
case 2147483651:
|
|
724
|
+
dc.state.brush = {
|
|
725
|
+
type: "brush",
|
|
726
|
+
style: 0,
|
|
727
|
+
color: DKGRAY,
|
|
728
|
+
hatch: 0
|
|
729
|
+
};
|
|
730
|
+
return true;
|
|
731
|
+
case 2147483652:
|
|
732
|
+
dc.state.brush = {
|
|
733
|
+
type: "brush",
|
|
734
|
+
style: 0,
|
|
735
|
+
color: BLACK,
|
|
736
|
+
hatch: 0
|
|
737
|
+
};
|
|
738
|
+
return true;
|
|
739
|
+
case 2147483653:
|
|
740
|
+
dc.state.brush = {
|
|
741
|
+
type: "brush",
|
|
742
|
+
style: 1,
|
|
743
|
+
color: WHITE,
|
|
744
|
+
hatch: 0
|
|
745
|
+
};
|
|
746
|
+
return true;
|
|
747
|
+
case 2147483654:
|
|
748
|
+
dc.state.pen = {
|
|
749
|
+
type: "pen",
|
|
750
|
+
style: PenStyle.PS_SOLID,
|
|
751
|
+
width: 1,
|
|
752
|
+
color: WHITE,
|
|
753
|
+
endCap: 0,
|
|
754
|
+
lineJoin: 0
|
|
755
|
+
};
|
|
756
|
+
return true;
|
|
757
|
+
case 2147483655:
|
|
758
|
+
dc.state.pen = {
|
|
759
|
+
type: "pen",
|
|
760
|
+
style: PenStyle.PS_SOLID,
|
|
761
|
+
width: 1,
|
|
762
|
+
color: BLACK,
|
|
763
|
+
endCap: 0,
|
|
764
|
+
lineJoin: 0
|
|
765
|
+
};
|
|
766
|
+
return true;
|
|
767
|
+
case 2147483656:
|
|
768
|
+
dc.state.pen = {
|
|
769
|
+
type: "pen",
|
|
770
|
+
style: PenStyle.PS_NULL,
|
|
771
|
+
width: 0,
|
|
772
|
+
color: BLACK,
|
|
773
|
+
endCap: 0,
|
|
774
|
+
lineJoin: 0
|
|
775
|
+
};
|
|
776
|
+
return true;
|
|
777
|
+
case 2147483658:
|
|
778
|
+
case 2147483659:
|
|
779
|
+
case 2147483664:
|
|
780
|
+
dc.state.font = {
|
|
781
|
+
type: "font",
|
|
782
|
+
height: 12,
|
|
783
|
+
width: 0,
|
|
784
|
+
weight: 400,
|
|
785
|
+
italic: false,
|
|
786
|
+
underline: false,
|
|
787
|
+
strikeOut: false,
|
|
788
|
+
charSet: 0,
|
|
789
|
+
faceName: "Courier New",
|
|
790
|
+
escapement: 0,
|
|
791
|
+
orientation: 0
|
|
792
|
+
};
|
|
793
|
+
return true;
|
|
794
|
+
case 2147483660:
|
|
795
|
+
case 2147483665:
|
|
796
|
+
dc.state.font = {
|
|
797
|
+
type: "font",
|
|
798
|
+
height: 12,
|
|
799
|
+
width: 0,
|
|
800
|
+
weight: 400,
|
|
801
|
+
italic: false,
|
|
802
|
+
underline: false,
|
|
803
|
+
strikeOut: false,
|
|
804
|
+
charSet: 0,
|
|
805
|
+
faceName: "Arial",
|
|
806
|
+
escapement: 0,
|
|
807
|
+
orientation: 0
|
|
808
|
+
};
|
|
809
|
+
return true;
|
|
810
|
+
case 2147483661:
|
|
811
|
+
case 2147483662:
|
|
812
|
+
dc.state.font = {
|
|
813
|
+
type: "font",
|
|
814
|
+
height: 16,
|
|
815
|
+
width: 0,
|
|
816
|
+
weight: 700,
|
|
817
|
+
italic: false,
|
|
818
|
+
underline: false,
|
|
819
|
+
strikeOut: false,
|
|
820
|
+
charSet: 0,
|
|
821
|
+
faceName: "System",
|
|
822
|
+
escapement: 0,
|
|
823
|
+
orientation: 0
|
|
824
|
+
};
|
|
825
|
+
return true;
|
|
826
|
+
case 2147483666: return true;
|
|
827
|
+
case 2147483667: return true;
|
|
828
|
+
default: return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/path-builder.ts
|
|
834
|
+
/**
|
|
835
|
+
* SVG path data builder.
|
|
836
|
+
* Accumulates move/line/curve/close commands and outputs SVG `d` attribute string.
|
|
837
|
+
*/
|
|
838
|
+
var PathBuilder = class {
|
|
839
|
+
constructor() {
|
|
840
|
+
this.commands = [];
|
|
841
|
+
this._currentX = 0;
|
|
842
|
+
this._currentY = 0;
|
|
843
|
+
this._startX = 0;
|
|
844
|
+
this._startY = 0;
|
|
845
|
+
this._hasContent = false;
|
|
846
|
+
this.minX = Infinity;
|
|
847
|
+
this.maxX = -Infinity;
|
|
848
|
+
this.minY = Infinity;
|
|
849
|
+
this.maxY = -Infinity;
|
|
850
|
+
}
|
|
851
|
+
get currentX() {
|
|
852
|
+
return this._currentX;
|
|
853
|
+
}
|
|
854
|
+
get currentY() {
|
|
855
|
+
return this._currentY;
|
|
856
|
+
}
|
|
857
|
+
get hasContent() {
|
|
858
|
+
return this._hasContent;
|
|
859
|
+
}
|
|
860
|
+
trackXY(x, y) {
|
|
861
|
+
if (x < this.minX) this.minX = x;
|
|
862
|
+
if (x > this.maxX) this.maxX = x;
|
|
863
|
+
if (y < this.minY) this.minY = y;
|
|
864
|
+
if (y > this.maxY) this.maxY = y;
|
|
865
|
+
}
|
|
866
|
+
moveTo(x, y) {
|
|
867
|
+
this.commands.push(`M${fmt$2(x)},${fmt$2(y)}`);
|
|
868
|
+
this._currentX = x;
|
|
869
|
+
this._currentY = y;
|
|
870
|
+
this._startX = x;
|
|
871
|
+
this._startY = y;
|
|
872
|
+
this._hasContent = true;
|
|
873
|
+
this.trackXY(x, y);
|
|
874
|
+
}
|
|
875
|
+
lineTo(x, y) {
|
|
876
|
+
this.commands.push(`L${fmt$2(x)},${fmt$2(y)}`);
|
|
877
|
+
this._currentX = x;
|
|
878
|
+
this._currentY = y;
|
|
879
|
+
this._hasContent = true;
|
|
880
|
+
this.trackXY(x, y);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Cubic bezier curve to (x, y) with control points (cx1, cy1) and (cx2, cy2).
|
|
884
|
+
*/
|
|
885
|
+
curveTo(cx1, cy1, cx2, cy2, x, y) {
|
|
886
|
+
this.commands.push(`C${fmt$2(cx1)},${fmt$2(cy1)},${fmt$2(cx2)},${fmt$2(cy2)},${fmt$2(x)},${fmt$2(y)}`);
|
|
887
|
+
this._currentX = x;
|
|
888
|
+
this._currentY = y;
|
|
889
|
+
this._hasContent = true;
|
|
890
|
+
this.trackXY(cx1, cy1);
|
|
891
|
+
this.trackXY(cx2, cy2);
|
|
892
|
+
this.trackXY(x, y);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* SVG elliptical arc command.
|
|
896
|
+
*/
|
|
897
|
+
arcTo(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y) {
|
|
898
|
+
this.commands.push(`A${fmt$2(rx)},${fmt$2(ry)},${xAxisRotation},${largeArcFlag},${sweepFlag},${fmt$2(x)},${fmt$2(y)}`);
|
|
899
|
+
this._currentX = x;
|
|
900
|
+
this._currentY = y;
|
|
901
|
+
this._hasContent = true;
|
|
902
|
+
this.trackXY(x, y);
|
|
903
|
+
}
|
|
904
|
+
closePath() {
|
|
905
|
+
this.commands.push("Z");
|
|
906
|
+
this._currentX = this._startX;
|
|
907
|
+
this._currentY = this._startY;
|
|
908
|
+
}
|
|
909
|
+
/** Get the SVG path d attribute string */
|
|
910
|
+
toPathData() {
|
|
911
|
+
return this.commands.join("");
|
|
912
|
+
}
|
|
913
|
+
/** Reset the builder */
|
|
914
|
+
clear() {
|
|
915
|
+
this.commands.length = 0;
|
|
916
|
+
this._currentX = 0;
|
|
917
|
+
this._currentY = 0;
|
|
918
|
+
this._startX = 0;
|
|
919
|
+
this._startY = 0;
|
|
920
|
+
this._hasContent = false;
|
|
921
|
+
this.minX = Infinity;
|
|
922
|
+
this.maxX = -Infinity;
|
|
923
|
+
this.minY = Infinity;
|
|
924
|
+
this.maxY = -Infinity;
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
/** Format a number for SVG: up to 2 decimal places, no trailing zeros */
|
|
928
|
+
function fmt$2(n) {
|
|
929
|
+
return Number(n.toFixed(2)).toString();
|
|
930
|
+
}
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/svg-writer.ts
|
|
933
|
+
/**
|
|
934
|
+
* SVG XML string generator. Zero DOM dependency.
|
|
935
|
+
* Builds SVG by concatenating string fragments.
|
|
936
|
+
*/
|
|
937
|
+
var SvgWriter = class {
|
|
938
|
+
constructor() {
|
|
939
|
+
this.parts = [];
|
|
940
|
+
this.clipCount = 0;
|
|
941
|
+
this.gradientCount = 0;
|
|
942
|
+
this.defs = [];
|
|
943
|
+
this.defsOpen = false;
|
|
944
|
+
this.groupScaleStack = [1];
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Begin SVG document with viewBox.
|
|
948
|
+
*/
|
|
949
|
+
beginDocument(x, y, width, height) {
|
|
950
|
+
this.parts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${fmt$1(x)} ${fmt$1(y)} ${fmt$1(width)} ${fmt$1(height)}">`);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Update the SVG viewBox after the document has been started.
|
|
954
|
+
* Used when EMF records change the logical coordinate space via SetWindowExtEx.
|
|
955
|
+
*/
|
|
956
|
+
updateViewBox(x, y, width, height) {
|
|
957
|
+
if (this.parts.length > 0) this.parts[0] = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${fmt$1(x)} ${fmt$1(y)} ${fmt$1(width)} ${fmt$1(height)}">`;
|
|
958
|
+
}
|
|
959
|
+
setRootDataAttribute(name, value) {
|
|
960
|
+
if (this.parts.length > 0 && /^data-[a-z0-9-]+$/.test(name)) this.parts[0] = this.parts[0].replace(">", ` ${name}="${escapeXml$1(value)}">`);
|
|
961
|
+
}
|
|
962
|
+
preserveAspectRatioNone() {
|
|
963
|
+
if (this.parts.length > 0) this.parts[0] = this.parts[0].replace(">", " preserveAspectRatio=\"none\">");
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* End SVG document.
|
|
967
|
+
*/
|
|
968
|
+
endDocument() {
|
|
969
|
+
let result = "";
|
|
970
|
+
if (this.parts.length > 0) {
|
|
971
|
+
result = this.parts[0];
|
|
972
|
+
if (this.defs.length > 0) result += "<defs>" + this.defs.join("") + "</defs>";
|
|
973
|
+
result += this.parts.slice(1).join("");
|
|
974
|
+
}
|
|
975
|
+
result += "</svg>";
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Draw an ellipse.
|
|
980
|
+
*/
|
|
981
|
+
ellipse(cx, cy, rx, ry, fill, stroke) {
|
|
982
|
+
const fillAttr = buildFillAttr(fill);
|
|
983
|
+
const strokeAttr = buildStrokeAttr(stroke);
|
|
984
|
+
this.parts.push(`<ellipse cx="${fmt$1(cx)}" cy="${fmt$1(cy)}" rx="${fmt$1(rx)}" ry="${fmt$1(ry)}"${fillAttr}${strokeAttr}/>`);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Draw a rectangle.
|
|
988
|
+
*/
|
|
989
|
+
rect(x, y, width, height, fill, stroke) {
|
|
990
|
+
const fillAttr = buildFillAttr(fill);
|
|
991
|
+
const strokeAttr = buildStrokeAttr(stroke);
|
|
992
|
+
this.parts.push(`<rect x="${fmt$1(x)}" y="${fmt$1(y)}" width="${fmt$1(width)}" height="${fmt$1(height)}"${fillAttr}${strokeAttr}/>`);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Draw a path from SVG path data string.
|
|
996
|
+
*/
|
|
997
|
+
path(d, fill, stroke, fillRule) {
|
|
998
|
+
if (isDegenerateSingleLinePath(d) && fill.enabled && fill.brushStyle !== 1 && (!stroke.enabled || (stroke.penStyle & PenStyle.PS_STYLE_MASK) === PenStyle.PS_NULL)) {
|
|
999
|
+
const strokeAttr = buildStrokeAttr({
|
|
1000
|
+
enabled: true,
|
|
1001
|
+
color: fill.color,
|
|
1002
|
+
width: 1,
|
|
1003
|
+
penStyle: PenStyle.PS_SOLID,
|
|
1004
|
+
opacity: degenerateLineStrokeOpacity(this.currentGroupScale())
|
|
1005
|
+
});
|
|
1006
|
+
this.parts.push(`<path d="${d}" fill="none"${strokeAttr} vector-effect="non-scaling-stroke"/>`);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const fillAttr = buildFillAttr(fill);
|
|
1010
|
+
const strokeAttr = buildStrokeAttr(stroke);
|
|
1011
|
+
const ruleAttr = fillRule ? ` fill-rule="${fillRule}"` : "";
|
|
1012
|
+
this.parts.push(`<path d="${d}"${fillAttr}${strokeAttr}${ruleAttr}/>`);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Draw a polyline (no fill).
|
|
1016
|
+
*/
|
|
1017
|
+
polyline(points, stroke) {
|
|
1018
|
+
const strokeAttr = buildStrokeAttr(stroke);
|
|
1019
|
+
this.parts.push(`<polyline points="${points}" fill="none"${strokeAttr}/>`);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Draw text.
|
|
1023
|
+
*/
|
|
1024
|
+
text(x, y, content, fontFamily, fontSize, fontWeight, fontStyle, fill, anchor, dominantBaseline, textDecoration = "none") {
|
|
1025
|
+
const escaped = escapeXml$1(content);
|
|
1026
|
+
const colorStr = colorToHex(fill);
|
|
1027
|
+
const weightAttr = fontWeight !== 400 ? ` font-weight="${fontWeight}"` : "";
|
|
1028
|
+
const styleAttr = fontStyle !== "normal" ? ` font-style="${fontStyle}"` : "";
|
|
1029
|
+
const decorationAttr = textDecoration !== "none" ? ` text-decoration="${escapeXml$1(textDecoration)}"` : "";
|
|
1030
|
+
this.parts.push(`<text x="${fmt$1(x)}" y="${fmt$1(y)}" font-family="${escapeXml$1(fontFamily)}" font-size="${fmt$1(fontSize)}"${weightAttr}${styleAttr}${decorationAttr} fill="${colorStr}" text-anchor="${anchor}" dominant-baseline="${dominantBaseline}">${escaped}</text>`);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Embed an image (base64 data URI).
|
|
1034
|
+
*/
|
|
1035
|
+
image(x, y, width, height, dataUri) {
|
|
1036
|
+
this.parts.push(`<image x="${fmt$1(x)}" y="${fmt$1(y)}" width="${fmt$1(width)}" height="${fmt$1(height)}" href="${dataUri}" preserveAspectRatio="none"/>`);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Open a group with optional transform.
|
|
1040
|
+
*/
|
|
1041
|
+
openGroup(transform, clipPathId) {
|
|
1042
|
+
let attrs = "";
|
|
1043
|
+
if (transform) attrs += ` transform="${transform}"`;
|
|
1044
|
+
if (clipPathId) attrs += ` clip-path="url(#${clipPathId})"`;
|
|
1045
|
+
this.groupScaleStack.push(this.currentGroupScale() * transformScale$1(transform));
|
|
1046
|
+
this.parts.push(`<g${attrs}>`);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Close current group.
|
|
1050
|
+
*/
|
|
1051
|
+
closeGroup() {
|
|
1052
|
+
if (this.groupScaleStack.length > 1) this.groupScaleStack.pop();
|
|
1053
|
+
this.parts.push("</g>");
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Add a clip path definition. Returns the clip path ID.
|
|
1057
|
+
*/
|
|
1058
|
+
addClipPath(pathData, fillRule, transform) {
|
|
1059
|
+
const id = `clip${this.clipCount++}`;
|
|
1060
|
+
const ruleAttr = fillRule ? ` clip-rule="${fillRule}"` : "";
|
|
1061
|
+
const transformAttr = transform ? ` transform="${transform}"` : "";
|
|
1062
|
+
this.defs.push(`<clipPath id="${id}"><path d="${pathData}"${transformAttr}${ruleAttr}/></clipPath>`);
|
|
1063
|
+
return id;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Write raw SVG string.
|
|
1067
|
+
*/
|
|
1068
|
+
raw(svg) {
|
|
1069
|
+
this.parts.push(svg);
|
|
1070
|
+
}
|
|
1071
|
+
currentGroupScale() {
|
|
1072
|
+
return this.groupScaleStack[this.groupScaleStack.length - 1] ?? 1;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Add a linear gradient definition. Returns the gradient ID.
|
|
1076
|
+
*/
|
|
1077
|
+
addLinearGradient(x1, y1, x2, y2, stops) {
|
|
1078
|
+
const id = `grad${this.gradientCount++}`;
|
|
1079
|
+
let def = `<linearGradient id="${id}" gradientUnits="userSpaceOnUse" x1="${fmt$1(x1)}" y1="${fmt$1(y1)}" x2="${fmt$1(x2)}" y2="${fmt$1(y2)}">`;
|
|
1080
|
+
for (const s of stops) def += `<stop offset="${s.offset}" stop-color="${s.color}"/>`;
|
|
1081
|
+
def += "</linearGradient>";
|
|
1082
|
+
this.defs.push(def);
|
|
1083
|
+
return id;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Draw a rectangle filled with a gradient.
|
|
1087
|
+
*/
|
|
1088
|
+
gradientRect(x, y, w, h, gradientId) {
|
|
1089
|
+
this.parts.push(`<rect x="${fmt$1(x)}" y="${fmt$1(y)}" width="${fmt$1(w)}" height="${fmt$1(h)}" fill="url(#${gradientId})"/>`);
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Draw a polygon (for triangle gradient fallback).
|
|
1093
|
+
*/
|
|
1094
|
+
gradientPolygon(points, fillColor) {
|
|
1095
|
+
this.parts.push(`<polygon points="${points}" fill="${fillColor}"/>`);
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Embed an image with opacity.
|
|
1099
|
+
*/
|
|
1100
|
+
imageWithOpacity(x, y, width, height, dataUri, opacity) {
|
|
1101
|
+
const opacityAttr = opacity < 1 ? ` opacity="${opacity.toFixed(2)}"` : "";
|
|
1102
|
+
this.parts.push(`<image x="${fmt$1(x)}" y="${fmt$1(y)}" width="${fmt$1(width)}" height="${fmt$1(height)}" href="${dataUri}" preserveAspectRatio="none"${opacityAttr}/>`);
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
function buildFillAttr(fill) {
|
|
1106
|
+
if (!fill.enabled || fill.brushStyle === 1) return " fill=\"none\"";
|
|
1107
|
+
return ` fill="${colorToRgba(fill.color)}"`;
|
|
1108
|
+
}
|
|
1109
|
+
function buildStrokeAttr(stroke) {
|
|
1110
|
+
if (!stroke.enabled || (stroke.penStyle & PenStyle.PS_STYLE_MASK) === PenStyle.PS_NULL) return "";
|
|
1111
|
+
let attr = ` stroke="${colorToRgba(stroke.color)}"`;
|
|
1112
|
+
if (stroke.width > 0) attr += ` stroke-width="${fmt$1(stroke.width)}"`;
|
|
1113
|
+
if (stroke.opacity !== void 0 && stroke.opacity < 1) attr += ` stroke-opacity="${fmt$1(stroke.opacity)}"`;
|
|
1114
|
+
const style = stroke.penStyle & PenStyle.PS_STYLE_MASK;
|
|
1115
|
+
if (style === PenStyle.PS_DASH) attr += " stroke-dasharray=\"8,4\"";
|
|
1116
|
+
else if (style === PenStyle.PS_DOT) attr += " stroke-dasharray=\"2,2\"";
|
|
1117
|
+
else if (style === PenStyle.PS_DASHDOT) attr += " stroke-dasharray=\"8,4,2,4\"";
|
|
1118
|
+
else if (style === PenStyle.PS_DASHDOTDOT) attr += " stroke-dasharray=\"8,4,2,4,2,4\"";
|
|
1119
|
+
const endCap = stroke.penStyle & PenStyle.PS_ENDCAP_MASK;
|
|
1120
|
+
if (endCap === PenStyle.PS_ENDCAP_SQUARE) attr += " stroke-linecap=\"square\"";
|
|
1121
|
+
else if (endCap === PenStyle.PS_ENDCAP_FLAT) attr += " stroke-linecap=\"butt\"";
|
|
1122
|
+
else attr += " stroke-linecap=\"round\"";
|
|
1123
|
+
const join = stroke.penStyle & PenStyle.PS_JOIN_MASK;
|
|
1124
|
+
if (join === PenStyle.PS_JOIN_BEVEL) attr += " stroke-linejoin=\"bevel\"";
|
|
1125
|
+
else if (join === PenStyle.PS_JOIN_MITER) attr += " stroke-linejoin=\"miter\"";
|
|
1126
|
+
else attr += " stroke-linejoin=\"round\"";
|
|
1127
|
+
return attr;
|
|
1128
|
+
}
|
|
1129
|
+
function isDegenerateSingleLinePath(pathData) {
|
|
1130
|
+
return /^M-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?L-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?Z$/.test(pathData);
|
|
1131
|
+
}
|
|
1132
|
+
function degenerateLineStrokeOpacity(scale) {
|
|
1133
|
+
if (!Number.isFinite(scale) || scale >= 1) return void 0;
|
|
1134
|
+
return Math.max(.35, Math.min(1, Math.sqrt(scale)));
|
|
1135
|
+
}
|
|
1136
|
+
function transformScale$1(transform) {
|
|
1137
|
+
if (!transform) return 1;
|
|
1138
|
+
const match = transform.match(/^matrix\((-?\d+(?:\.\d+)?(?:e[+-]?\d+)?),(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?),(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?),(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?),/);
|
|
1139
|
+
if (!match) return 1;
|
|
1140
|
+
const a = Number(match[1]);
|
|
1141
|
+
const b = Number(match[2]);
|
|
1142
|
+
const c = Number(match[3]);
|
|
1143
|
+
const d = Number(match[4]);
|
|
1144
|
+
const areaScale = Math.abs(a * d - b * c);
|
|
1145
|
+
if (!Number.isFinite(areaScale) || areaScale <= 0) return 1;
|
|
1146
|
+
return Math.sqrt(areaScale);
|
|
1147
|
+
}
|
|
1148
|
+
/** Convert Color to #RRGGBB hex string */
|
|
1149
|
+
function colorToHex(c) {
|
|
1150
|
+
return `#${c.r.toString(16).padStart(2, "0")}${c.g.toString(16).padStart(2, "0")}${c.b.toString(16).padStart(2, "0")}`;
|
|
1151
|
+
}
|
|
1152
|
+
/** Convert Color to rgba() or #hex string */
|
|
1153
|
+
function colorToRgba(c) {
|
|
1154
|
+
if (c.a < 255) return `rgba(${c.r},${c.g},${c.b},${(c.a / 255).toFixed(2)})`;
|
|
1155
|
+
return colorToHex(c);
|
|
1156
|
+
}
|
|
1157
|
+
function escapeXml$1(s) {
|
|
1158
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1159
|
+
}
|
|
1160
|
+
/** Format number for SVG output */
|
|
1161
|
+
function fmt$1(n) {
|
|
1162
|
+
return Number(n.toFixed(2)).toString();
|
|
1163
|
+
}
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region src/emf-parser.ts
|
|
1166
|
+
/** Compare two affine matrices for equality (within epsilon). */
|
|
1167
|
+
function matricesEqual(a, b) {
|
|
1168
|
+
for (let i = 0; i < 6; i++) if (Math.abs(a[i] - b[i]) > 1e-10) return false;
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
function usesExplicitPageTransform(dc) {
|
|
1172
|
+
const s = dc.state;
|
|
1173
|
+
if (!s.hasExplicitWindowExt || !s.hasExplicitViewportExt) return false;
|
|
1174
|
+
if (s.windowExtX === 0 || s.windowExtY === 0) return false;
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
function getPageTransform(dc) {
|
|
1178
|
+
if (!usesExplicitPageTransform(dc)) return identity();
|
|
1179
|
+
const s = dc.state;
|
|
1180
|
+
const sx = dc.getWindowToViewportScaleX();
|
|
1181
|
+
const sy = dc.getWindowToViewportScaleY();
|
|
1182
|
+
return [
|
|
1183
|
+
sx,
|
|
1184
|
+
0,
|
|
1185
|
+
0,
|
|
1186
|
+
sy,
|
|
1187
|
+
s.viewportOrgX - s.windowOrgX * sx,
|
|
1188
|
+
s.viewportOrgY - s.windowOrgY * sy
|
|
1189
|
+
];
|
|
1190
|
+
}
|
|
1191
|
+
function getEffectiveTransform(dc) {
|
|
1192
|
+
return multiply(getPageTransform(dc), dc.state.worldTransform);
|
|
1193
|
+
}
|
|
1194
|
+
function getOptionalTransformString(dc) {
|
|
1195
|
+
const transform = getEffectiveTransform(dc);
|
|
1196
|
+
return isIdentity(transform) ? void 0 : toSvgString(transform);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Keep emitted SVG clip groups synchronized with the current EMF device context.
|
|
1200
|
+
* SVG ids are nested only for active intersections; replacing the clip region
|
|
1201
|
+
* closes prior groups before opening the new stack.
|
|
1202
|
+
*/
|
|
1203
|
+
function syncClipGroups(svg, dc, state) {
|
|
1204
|
+
const targetClipIds = dc.state.clipPathIds;
|
|
1205
|
+
const currentClipIds = state.emittedClipPathIds;
|
|
1206
|
+
let commonPrefixLength = 0;
|
|
1207
|
+
while (commonPrefixLength < targetClipIds.length && commonPrefixLength < currentClipIds.length && targetClipIds[commonPrefixLength] === currentClipIds[commonPrefixLength]) commonPrefixLength++;
|
|
1208
|
+
if (commonPrefixLength === targetClipIds.length && commonPrefixLength === currentClipIds.length) return;
|
|
1209
|
+
if (state.worldTransformGroup) {
|
|
1210
|
+
svg.closeGroup();
|
|
1211
|
+
state.worldTransformGroup = false;
|
|
1212
|
+
state.lastEmittedTransform = null;
|
|
1213
|
+
}
|
|
1214
|
+
while (currentClipIds.length > commonPrefixLength) {
|
|
1215
|
+
svg.closeGroup();
|
|
1216
|
+
currentClipIds.pop();
|
|
1217
|
+
}
|
|
1218
|
+
for (let index = commonPrefixLength; index < targetClipIds.length; index++) {
|
|
1219
|
+
const clipPathId = targetClipIds[index];
|
|
1220
|
+
if (!clipPathId) continue;
|
|
1221
|
+
svg.openGroup(void 0, clipPathId);
|
|
1222
|
+
currentClipIds.push(clipPathId);
|
|
1223
|
+
state.hadClipGroups = true;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Ensure the SVG output has the correct world transform group open.
|
|
1228
|
+
* Opens/closes `<g transform="matrix(...)">` groups as the world transform changes.
|
|
1229
|
+
*/
|
|
1230
|
+
function ensureWorldTransformGroup(svg, dc, state) {
|
|
1231
|
+
syncClipGroups(svg, dc, state);
|
|
1232
|
+
const transform = getEffectiveTransform(dc);
|
|
1233
|
+
const isId = isIdentity(transform);
|
|
1234
|
+
if (state.lastEmittedTransform !== null) {
|
|
1235
|
+
if (matricesEqual(transform, state.lastEmittedTransform)) return;
|
|
1236
|
+
svg.closeGroup();
|
|
1237
|
+
state.worldTransformGroup = false;
|
|
1238
|
+
state.lastEmittedTransform = null;
|
|
1239
|
+
}
|
|
1240
|
+
if (!isId) {
|
|
1241
|
+
svg.openGroup(toSvgString(transform));
|
|
1242
|
+
state.worldTransformGroup = true;
|
|
1243
|
+
state.lastEmittedTransform = [...transform];
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
/** Expand the content bounding box with a point, applying the current world transform. */
|
|
1247
|
+
function trackPoint(state, dc, x, y) {
|
|
1248
|
+
const wt = getEffectiveTransform(dc);
|
|
1249
|
+
const tx = wt[0] * x + wt[2] * y + wt[4];
|
|
1250
|
+
const ty = wt[1] * x + wt[3] * y + wt[5];
|
|
1251
|
+
if (tx < state.contentMinX) state.contentMinX = tx;
|
|
1252
|
+
if (tx > state.contentMaxX) state.contentMaxX = tx;
|
|
1253
|
+
if (ty < state.contentMinY) state.contentMinY = ty;
|
|
1254
|
+
if (ty > state.contentMaxY) state.contentMaxY = ty;
|
|
1255
|
+
}
|
|
1256
|
+
/** Expand the clip bounding box with a point, applying the current world transform. */
|
|
1257
|
+
function trackClipPoint(state, dc, x, y) {
|
|
1258
|
+
const wt = getEffectiveTransform(dc);
|
|
1259
|
+
const tx = wt[0] * x + wt[2] * y + wt[4];
|
|
1260
|
+
const ty = wt[1] * x + wt[3] * y + wt[5];
|
|
1261
|
+
if (tx < state.clipMinX) state.clipMinX = tx;
|
|
1262
|
+
if (tx > state.clipMaxX) state.clipMaxX = tx;
|
|
1263
|
+
if (ty < state.clipMinY) state.clipMinY = ty;
|
|
1264
|
+
if (ty > state.clipMaxY) state.clipMaxY = ty;
|
|
1265
|
+
}
|
|
1266
|
+
/** Expand the content bounding box with a rectangle, applying the current world transform. */
|
|
1267
|
+
function trackRect(state, dc, x, y, w, h) {
|
|
1268
|
+
trackPoint(state, dc, x, y);
|
|
1269
|
+
trackPoint(state, dc, x + w, y);
|
|
1270
|
+
trackPoint(state, dc, x, y + h);
|
|
1271
|
+
trackPoint(state, dc, x + w, y + h);
|
|
1272
|
+
}
|
|
1273
|
+
function estimateTextWidth(text, fontSize, fontWidth) {
|
|
1274
|
+
if (fontWidth > 0) return Math.max(fontWidth * Array.from(text).length, 1);
|
|
1275
|
+
let width = 0;
|
|
1276
|
+
for (const char of Array.from(text)) width += char.codePointAt(0) > 255 ? fontSize : fontSize * .62;
|
|
1277
|
+
return Math.max(width, fontSize * .25);
|
|
1278
|
+
}
|
|
1279
|
+
const ETO_PDY = 8192;
|
|
1280
|
+
const ROP_PATCOPY$1 = 15728673;
|
|
1281
|
+
function isSymbolEncodedFont(font) {
|
|
1282
|
+
return font.charSet === 2 || font.faceName.toLowerCase() === "symbol";
|
|
1283
|
+
}
|
|
1284
|
+
function isMathTypeExtraFont(font) {
|
|
1285
|
+
return font.faceName.toLowerCase().includes("mt extra");
|
|
1286
|
+
}
|
|
1287
|
+
function decodePrivateUseMathText(text, font) {
|
|
1288
|
+
const map = isSymbolEncodedFont(font) ? SYMBOL_CHAR_MAP : isMathTypeExtraFont(font) ? MT_EXTRA_CHAR_MAP : void 0;
|
|
1289
|
+
if (!map) return text;
|
|
1290
|
+
let decoded = "";
|
|
1291
|
+
for (const char of Array.from(text)) {
|
|
1292
|
+
const code = char.codePointAt(0);
|
|
1293
|
+
const lowByte = code & 255;
|
|
1294
|
+
decoded += code >= 61440 && code <= 61695 ? map[lowByte] ?? char : char;
|
|
1295
|
+
}
|
|
1296
|
+
return decoded;
|
|
1297
|
+
}
|
|
1298
|
+
function svgFontFamilyForEmfText(font) {
|
|
1299
|
+
if (isSymbolEncodedFont(font) || isMathTypeExtraFont(font)) return "serif";
|
|
1300
|
+
return font.faceName || "Arial";
|
|
1301
|
+
}
|
|
1302
|
+
function readTextDx$1(reader, recordStart, offDx, nChars, options) {
|
|
1303
|
+
if (offDx <= 0 || nChars <= 0) return void 0;
|
|
1304
|
+
const valuesPerChar = options & ETO_PDY ? 2 : 1;
|
|
1305
|
+
const count = nChars * valuesPerChar;
|
|
1306
|
+
if (recordStart + offDx + count * 4 > reader.length) return void 0;
|
|
1307
|
+
const original = reader.position;
|
|
1308
|
+
reader.seek(recordStart + offDx);
|
|
1309
|
+
const x = [];
|
|
1310
|
+
const y = [];
|
|
1311
|
+
for (let i = 0; i < nChars; i++) {
|
|
1312
|
+
x.push(reader.readInt32());
|
|
1313
|
+
if (valuesPerChar === 2) y.push(reader.readInt32());
|
|
1314
|
+
}
|
|
1315
|
+
reader.seek(original);
|
|
1316
|
+
return {
|
|
1317
|
+
x,
|
|
1318
|
+
y
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function textOriginFromCurrentPosition(dc, x, y) {
|
|
1322
|
+
if (dc.state.textAlign & TextAlign.TA_UPDATECP) return {
|
|
1323
|
+
x: dc.state.currentPosX,
|
|
1324
|
+
y: dc.state.currentPosY
|
|
1325
|
+
};
|
|
1326
|
+
return {
|
|
1327
|
+
x,
|
|
1328
|
+
y
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
function updateCurrentTextPosition(dc, originX, originY, text, fontSize, fontWidth, dx) {
|
|
1332
|
+
if (!(dc.state.textAlign & TextAlign.TA_UPDATECP)) return;
|
|
1333
|
+
const chars = Array.from(text);
|
|
1334
|
+
const advanceX = dx?.x.length ? dx.x.slice(0, chars.length).reduce((sum, item) => sum + item, 0) : estimateTextWidth(text, fontSize, fontWidth);
|
|
1335
|
+
const advanceY = dx?.y.length ? dx.y.slice(0, chars.length).reduce((sum, item) => sum + item, 0) : 0;
|
|
1336
|
+
dc.state.currentPosX = originX + advanceX;
|
|
1337
|
+
dc.state.currentPosY = originY + advanceY;
|
|
1338
|
+
}
|
|
1339
|
+
function emitEmfText(svg, dc, state, x, y, text, fontFamily, fontSize, fontWeight, fontStyle, anchor, baseline, dx) {
|
|
1340
|
+
const chars = Array.from(text);
|
|
1341
|
+
const decoration = [dc.state.font.underline ? "underline" : "", dc.state.font.strikeOut ? "line-through" : ""].filter(Boolean).join(" ") || "none";
|
|
1342
|
+
if (!dx?.x.length || chars.length <= 1) {
|
|
1343
|
+
trackTextBounds(state, dc, x, y, text, fontSize, dc.state.font.width ?? 0, anchor, baseline);
|
|
1344
|
+
svg.text(x, y, text, fontFamily, fontSize, fontWeight, fontStyle, dc.state.textColor, anchor, baseline, decoration);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const totalAdvance = dx.x.slice(0, chars.length).reduce((sum, item) => sum + item, 0);
|
|
1348
|
+
let cursorX = x;
|
|
1349
|
+
if (anchor === "middle") cursorX -= totalAdvance / 2;
|
|
1350
|
+
else if (anchor === "end") cursorX -= totalAdvance;
|
|
1351
|
+
let cursorY = y;
|
|
1352
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1353
|
+
const char = chars[i];
|
|
1354
|
+
trackTextBounds(state, dc, cursorX, cursorY, char, fontSize, dc.state.font.width ?? 0, "start", baseline);
|
|
1355
|
+
svg.text(cursorX, cursorY, char, fontFamily, fontSize, fontWeight, fontStyle, dc.state.textColor, "start", baseline, decoration);
|
|
1356
|
+
cursorX += dx.x[i] ?? 0;
|
|
1357
|
+
cursorY += dx.y[i] ?? 0;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
function trackTextBounds(state, dc, x, y, text, fontSize, fontWidth, anchor, baseline) {
|
|
1361
|
+
const width = estimateTextWidth(text, fontSize, fontWidth);
|
|
1362
|
+
const italicPad = dc.state.font.italic ? fontSize * .2 : 0;
|
|
1363
|
+
let left = x;
|
|
1364
|
+
if (anchor === "middle") left -= width / 2;
|
|
1365
|
+
else if (anchor === "end") left -= width;
|
|
1366
|
+
left -= italicPad * .5;
|
|
1367
|
+
let top = y;
|
|
1368
|
+
let height = fontSize;
|
|
1369
|
+
if (baseline === "auto") {
|
|
1370
|
+
top = y - fontSize;
|
|
1371
|
+
height = fontSize * 1.25;
|
|
1372
|
+
} else if (baseline === "text-after-edge") top = y - fontSize;
|
|
1373
|
+
trackRect(state, dc, left, top, width + italicPad, height);
|
|
1374
|
+
}
|
|
1375
|
+
/** Expand the clip bounding box with a rectangle, applying the current world transform. */
|
|
1376
|
+
function trackClipRect(state, dc, x, y, w, h) {
|
|
1377
|
+
trackClipPoint(state, dc, x, y);
|
|
1378
|
+
trackClipPoint(state, dc, x + w, y);
|
|
1379
|
+
trackClipPoint(state, dc, x, y + h);
|
|
1380
|
+
trackClipPoint(state, dc, x + w, y + h);
|
|
1381
|
+
}
|
|
1382
|
+
function transformRectBounds(transform, x, y, w, h) {
|
|
1383
|
+
const p1 = transformPoint(transform, {
|
|
1384
|
+
x,
|
|
1385
|
+
y
|
|
1386
|
+
});
|
|
1387
|
+
const p2 = transformPoint(transform, {
|
|
1388
|
+
x: x + w,
|
|
1389
|
+
y
|
|
1390
|
+
});
|
|
1391
|
+
const p3 = transformPoint(transform, {
|
|
1392
|
+
x,
|
|
1393
|
+
y: y + h
|
|
1394
|
+
});
|
|
1395
|
+
const p4 = transformPoint(transform, {
|
|
1396
|
+
x: x + w,
|
|
1397
|
+
y: y + h
|
|
1398
|
+
});
|
|
1399
|
+
return {
|
|
1400
|
+
left: Math.min(p1.x, p2.x, p3.x, p4.x),
|
|
1401
|
+
top: Math.min(p1.y, p2.y, p3.y, p4.y),
|
|
1402
|
+
right: Math.max(p1.x, p2.x, p3.x, p4.x),
|
|
1403
|
+
bottom: Math.max(p1.y, p2.y, p3.y, p4.y)
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
/** Expand the content bounding box with an array of points. */
|
|
1407
|
+
function trackPoints(state, dc, points) {
|
|
1408
|
+
for (const p of points) trackPoint(state, dc, p.x, p.y);
|
|
1409
|
+
}
|
|
1410
|
+
/** Transfer the pathBuilder's accumulated bounds into the parser state (applying world transform). */
|
|
1411
|
+
function trackPathBounds(state, dc, pb) {
|
|
1412
|
+
if (pb.minX <= pb.maxX && pb.minY <= pb.maxY) {
|
|
1413
|
+
trackPoint(state, dc, pb.minX, pb.minY);
|
|
1414
|
+
trackPoint(state, dc, pb.maxX, pb.maxY);
|
|
1415
|
+
trackPoint(state, dc, pb.minX, pb.maxY);
|
|
1416
|
+
trackPoint(state, dc, pb.maxX, pb.minY);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
/** Transfer the pathBuilder's accumulated bounds into the clip bounds. */
|
|
1420
|
+
function trackClipPathBounds(state, dc, pb) {
|
|
1421
|
+
if (pb.minX <= pb.maxX && pb.minY <= pb.maxY) {
|
|
1422
|
+
trackClipPoint(state, dc, pb.minX, pb.minY);
|
|
1423
|
+
trackClipPoint(state, dc, pb.maxX, pb.maxY);
|
|
1424
|
+
trackClipPoint(state, dc, pb.minX, pb.maxY);
|
|
1425
|
+
trackClipPoint(state, dc, pb.maxX, pb.minY);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
|
|
1429
|
+
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Parse an EMF buffer and return SVG string.
|
|
1433
|
+
*/
|
|
1434
|
+
function parseEmf(buffer) {
|
|
1435
|
+
const reader = new BinaryReader(buffer);
|
|
1436
|
+
const dc = new DeviceContext();
|
|
1437
|
+
const objects = new ObjectTable();
|
|
1438
|
+
const svg = new SvgWriter();
|
|
1439
|
+
const pathBuilder = new PathBuilder();
|
|
1440
|
+
const state = {
|
|
1441
|
+
pathRecording: false,
|
|
1442
|
+
worldTransformGroup: false,
|
|
1443
|
+
emittedClipPathIds: [],
|
|
1444
|
+
lastEmittedTransform: null,
|
|
1445
|
+
hadClipGroups: false,
|
|
1446
|
+
contentMinX: Infinity,
|
|
1447
|
+
contentMaxX: -Infinity,
|
|
1448
|
+
contentMinY: Infinity,
|
|
1449
|
+
contentMaxY: -Infinity,
|
|
1450
|
+
clipMinX: Infinity,
|
|
1451
|
+
clipMaxX: -Infinity,
|
|
1452
|
+
clipMinY: Infinity,
|
|
1453
|
+
clipMaxY: -Infinity,
|
|
1454
|
+
sourceCanvasBounds: void 0
|
|
1455
|
+
};
|
|
1456
|
+
const header = parseHeader(reader);
|
|
1457
|
+
if (!header) throw new Error("Invalid EMF file: could not parse header");
|
|
1458
|
+
const bounds = header.bounds;
|
|
1459
|
+
const viewX = bounds.left;
|
|
1460
|
+
const viewY = bounds.top;
|
|
1461
|
+
const viewW = bounds.right - bounds.left;
|
|
1462
|
+
const viewH = bounds.bottom - bounds.top;
|
|
1463
|
+
if (viewW <= 0 || viewH <= 0) throw new Error("Invalid EMF file: zero or negative bounds");
|
|
1464
|
+
dc.state.windowOrgX = viewX;
|
|
1465
|
+
dc.state.windowOrgY = viewY;
|
|
1466
|
+
dc.state.windowExtX = viewW;
|
|
1467
|
+
dc.state.windowExtY = viewH;
|
|
1468
|
+
dc.state.viewportOrgX = viewX;
|
|
1469
|
+
dc.state.viewportOrgY = viewY;
|
|
1470
|
+
dc.state.viewportExtX = viewW;
|
|
1471
|
+
dc.state.viewportExtY = viewH;
|
|
1472
|
+
svg.beginDocument(viewX, viewY, viewW, viewH);
|
|
1473
|
+
reader.seek(header.size);
|
|
1474
|
+
let recordCount = 0;
|
|
1475
|
+
while (reader.remaining >= 8 && recordCount < header.numRecords) {
|
|
1476
|
+
const recordStart = reader.position;
|
|
1477
|
+
const recordType = reader.readUint32();
|
|
1478
|
+
const recordSize = reader.readUint32();
|
|
1479
|
+
if (recordSize < 8 || recordStart + recordSize > reader.length) break;
|
|
1480
|
+
const dataSize = recordSize - 8;
|
|
1481
|
+
processRecord$1(recordType, reader, recordStart + 8, dataSize, dc, objects, svg, pathBuilder, state);
|
|
1482
|
+
if (recordType === 59) state.pathRecording = true;
|
|
1483
|
+
else if (recordType === 60 || recordType === 62 || recordType === 64 || recordType === 63 || recordType === 68) state.pathRecording = false;
|
|
1484
|
+
reader.seek(recordStart + recordSize);
|
|
1485
|
+
recordCount++;
|
|
1486
|
+
if (recordType === 14) break;
|
|
1487
|
+
}
|
|
1488
|
+
if (state.worldTransformGroup) svg.closeGroup();
|
|
1489
|
+
while (state.emittedClipPathIds.length > 0) {
|
|
1490
|
+
svg.closeGroup();
|
|
1491
|
+
state.emittedClipPathIds.pop();
|
|
1492
|
+
}
|
|
1493
|
+
const finalWindow = transformRectBounds(getPageTransform(dc), dc.state.windowOrgX, dc.state.windowOrgY, dc.state.windowExtX, dc.state.windowExtY);
|
|
1494
|
+
let finalX = finalWindow.left;
|
|
1495
|
+
let finalY = finalWindow.top;
|
|
1496
|
+
let finalW = finalWindow.right - finalWindow.left;
|
|
1497
|
+
let finalH = finalWindow.bottom - finalWindow.top;
|
|
1498
|
+
let usedSourceCanvasBounds = false;
|
|
1499
|
+
if (state.sourceCanvasBounds && state.contentMinX < state.contentMaxX) {
|
|
1500
|
+
finalX = state.contentMinX;
|
|
1501
|
+
finalY = state.sourceCanvasBounds.top;
|
|
1502
|
+
finalW = state.contentMaxX - state.contentMinX;
|
|
1503
|
+
finalH = state.sourceCanvasBounds.bottom - state.sourceCanvasBounds.top;
|
|
1504
|
+
usedSourceCanvasBounds = true;
|
|
1505
|
+
} else if (state.sourceCanvasBounds) {
|
|
1506
|
+
finalX = state.sourceCanvasBounds.left;
|
|
1507
|
+
finalY = state.sourceCanvasBounds.top;
|
|
1508
|
+
finalW = state.sourceCanvasBounds.right - state.sourceCanvasBounds.left;
|
|
1509
|
+
finalH = state.sourceCanvasBounds.bottom - state.sourceCanvasBounds.top;
|
|
1510
|
+
usedSourceCanvasBounds = true;
|
|
1511
|
+
} else if (state.contentMinX < state.contentMaxX && state.contentMinY < state.contentMaxY) {
|
|
1512
|
+
if (state.hadClipGroups) {
|
|
1513
|
+
if (!(state.clipMinX < state.clipMaxX && state.clipMinY < state.clipMaxY) || rectsOverlap(state.clipMinX, state.clipMinY, state.clipMaxX - state.clipMinX, state.clipMaxY - state.clipMinY, finalX, finalY, finalW, finalH)) {
|
|
1514
|
+
const winRight = finalX + finalW;
|
|
1515
|
+
const winBottom = finalY + finalH;
|
|
1516
|
+
const clampedX = Math.max(state.contentMinX, finalX);
|
|
1517
|
+
const clampedY = Math.max(state.contentMinY, finalY);
|
|
1518
|
+
const clampedRight = Math.min(state.contentMaxX, winRight);
|
|
1519
|
+
const clampedBottom = Math.min(state.contentMaxY, winBottom);
|
|
1520
|
+
if (clampedRight > clampedX && clampedBottom > clampedY) {
|
|
1521
|
+
finalX = clampedX;
|
|
1522
|
+
finalY = clampedY;
|
|
1523
|
+
finalW = clampedRight - clampedX;
|
|
1524
|
+
finalH = clampedBottom - clampedY;
|
|
1525
|
+
}
|
|
1526
|
+
} else {
|
|
1527
|
+
finalX = state.contentMinX;
|
|
1528
|
+
finalY = state.contentMinY;
|
|
1529
|
+
finalW = state.contentMaxX - state.contentMinX;
|
|
1530
|
+
finalH = state.contentMaxY - state.contentMinY;
|
|
1531
|
+
}
|
|
1532
|
+
} else {
|
|
1533
|
+
finalX = state.contentMinX;
|
|
1534
|
+
finalY = state.contentMinY;
|
|
1535
|
+
finalW = state.contentMaxX - state.contentMinX;
|
|
1536
|
+
finalH = state.contentMaxY - state.contentMinY;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if (finalW > 0 && finalH > 0 && (finalW !== viewW || finalH !== viewH || finalX !== viewX || finalY !== viewY)) svg.updateViewBox(finalX, finalY, finalW, finalH);
|
|
1540
|
+
if (usedSourceCanvasBounds) {
|
|
1541
|
+
svg.setRootDataAttribute("data-s8fy-preserves-src-rect", "true");
|
|
1542
|
+
svg.preserveAspectRatioNone();
|
|
1543
|
+
}
|
|
1544
|
+
return svg.endDocument();
|
|
1545
|
+
}
|
|
1546
|
+
function processRecord$1(type, reader, dataStart, dataSize, dc, objects, svg, pathBuilder, state) {
|
|
1547
|
+
reader.seek(dataStart);
|
|
1548
|
+
switch (type) {
|
|
1549
|
+
case 1:
|
|
1550
|
+
case 14: break;
|
|
1551
|
+
case 70: {
|
|
1552
|
+
const cbData = reader.readUint32();
|
|
1553
|
+
if (cbData >= 28 && reader.remaining >= cbData) {
|
|
1554
|
+
const data = reader.readBytes(cbData);
|
|
1555
|
+
if (data[0] === 71 && data[1] === 68 && data[2] === 73 && data[3] === 67) {
|
|
1556
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
1557
|
+
const width = view.getInt32(16, true);
|
|
1558
|
+
const height = view.getInt32(20, true);
|
|
1559
|
+
if (width > 0 && height > 0) state.sourceCanvasBounds = transformRectBounds(getEffectiveTransform(dc), 0, 0, width, height);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
break;
|
|
1563
|
+
}
|
|
1564
|
+
case 33:
|
|
1565
|
+
dc.save();
|
|
1566
|
+
break;
|
|
1567
|
+
case 34: {
|
|
1568
|
+
const savedDC = reader.readInt32();
|
|
1569
|
+
dc.restore(savedDC);
|
|
1570
|
+
syncClipGroups(svg, dc, state);
|
|
1571
|
+
break;
|
|
1572
|
+
}
|
|
1573
|
+
case 9:
|
|
1574
|
+
dc.state.windowExtX = reader.readInt32();
|
|
1575
|
+
dc.state.windowExtY = reader.readInt32();
|
|
1576
|
+
dc.state.hasExplicitWindowExt = true;
|
|
1577
|
+
break;
|
|
1578
|
+
case 10:
|
|
1579
|
+
dc.state.windowOrgX = reader.readInt32();
|
|
1580
|
+
dc.state.windowOrgY = reader.readInt32();
|
|
1581
|
+
break;
|
|
1582
|
+
case 11:
|
|
1583
|
+
dc.state.viewportExtX = reader.readInt32();
|
|
1584
|
+
dc.state.viewportExtY = reader.readInt32();
|
|
1585
|
+
dc.state.hasExplicitViewportExt = true;
|
|
1586
|
+
break;
|
|
1587
|
+
case 12:
|
|
1588
|
+
dc.state.viewportOrgX = reader.readInt32();
|
|
1589
|
+
dc.state.viewportOrgY = reader.readInt32();
|
|
1590
|
+
break;
|
|
1591
|
+
case 24:
|
|
1592
|
+
dc.state.textColor = readColorRef(reader);
|
|
1593
|
+
break;
|
|
1594
|
+
case 22:
|
|
1595
|
+
dc.state.textAlign = reader.readUint32();
|
|
1596
|
+
break;
|
|
1597
|
+
case 18:
|
|
1598
|
+
dc.state.bkMode = reader.readUint32();
|
|
1599
|
+
break;
|
|
1600
|
+
case 25:
|
|
1601
|
+
dc.state.bkColor = readColorRef(reader);
|
|
1602
|
+
break;
|
|
1603
|
+
case 19:
|
|
1604
|
+
dc.state.polyFillMode = reader.readUint32();
|
|
1605
|
+
break;
|
|
1606
|
+
case 58:
|
|
1607
|
+
dc.state.miterLimit = reader.readUint32();
|
|
1608
|
+
break;
|
|
1609
|
+
case 17:
|
|
1610
|
+
dc.state.mapMode = reader.readUint32();
|
|
1611
|
+
break;
|
|
1612
|
+
case 20:
|
|
1613
|
+
case 21:
|
|
1614
|
+
case 13:
|
|
1615
|
+
case 16:
|
|
1616
|
+
case 98:
|
|
1617
|
+
case 115:
|
|
1618
|
+
case 23: break;
|
|
1619
|
+
case 57:
|
|
1620
|
+
dc.state.arcDirection = reader.readUint32();
|
|
1621
|
+
break;
|
|
1622
|
+
case 35:
|
|
1623
|
+
dc.state.worldTransform = readXForm(reader);
|
|
1624
|
+
break;
|
|
1625
|
+
case 36: {
|
|
1626
|
+
const xform = readXForm(reader);
|
|
1627
|
+
switch (reader.readUint32()) {
|
|
1628
|
+
case 1:
|
|
1629
|
+
dc.state.worldTransform = identity();
|
|
1630
|
+
break;
|
|
1631
|
+
case 2:
|
|
1632
|
+
dc.state.worldTransform = multiply(xform, dc.state.worldTransform);
|
|
1633
|
+
break;
|
|
1634
|
+
case 3:
|
|
1635
|
+
dc.state.worldTransform = multiply(dc.state.worldTransform, xform);
|
|
1636
|
+
break;
|
|
1637
|
+
case 4: dc.state.worldTransform = xform;
|
|
1638
|
+
}
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
case 37: {
|
|
1642
|
+
const index = reader.readUint32();
|
|
1643
|
+
objects.select(index, dc);
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
case 40: {
|
|
1647
|
+
const index = reader.readUint32();
|
|
1648
|
+
objects.delete(index);
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
case 39: {
|
|
1652
|
+
const ihBrush = reader.readUint32();
|
|
1653
|
+
const style = reader.readUint32();
|
|
1654
|
+
const color = readColorRef(reader);
|
|
1655
|
+
const hatch = reader.readUint32();
|
|
1656
|
+
objects.create(ihBrush, {
|
|
1657
|
+
type: "brush",
|
|
1658
|
+
style,
|
|
1659
|
+
color,
|
|
1660
|
+
hatch
|
|
1661
|
+
});
|
|
1662
|
+
break;
|
|
1663
|
+
}
|
|
1664
|
+
case 38: {
|
|
1665
|
+
const ihPen = reader.readUint32();
|
|
1666
|
+
const style = reader.readUint32();
|
|
1667
|
+
const widthX = reader.readInt32();
|
|
1668
|
+
reader.readInt32();
|
|
1669
|
+
const color = readColorRef(reader);
|
|
1670
|
+
objects.create(ihPen, {
|
|
1671
|
+
type: "pen",
|
|
1672
|
+
style,
|
|
1673
|
+
width: Math.max(widthX, 1),
|
|
1674
|
+
color,
|
|
1675
|
+
endCap: 0,
|
|
1676
|
+
lineJoin: 0
|
|
1677
|
+
});
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
case 95: {
|
|
1681
|
+
const ihPen = reader.readUint32();
|
|
1682
|
+
reader.readUint32();
|
|
1683
|
+
reader.readUint32();
|
|
1684
|
+
reader.readUint32();
|
|
1685
|
+
reader.readUint32();
|
|
1686
|
+
const style = reader.readUint32();
|
|
1687
|
+
const width = reader.readUint32();
|
|
1688
|
+
reader.readUint32();
|
|
1689
|
+
const color = readColorRef(reader);
|
|
1690
|
+
const endCap = style & PenStyle.PS_ENDCAP_MASK;
|
|
1691
|
+
const lineJoin = style & PenStyle.PS_JOIN_MASK;
|
|
1692
|
+
objects.create(ihPen, {
|
|
1693
|
+
type: "pen",
|
|
1694
|
+
style: style & PenStyle.PS_STYLE_MASK,
|
|
1695
|
+
width: Math.max(width, 1),
|
|
1696
|
+
color,
|
|
1697
|
+
endCap,
|
|
1698
|
+
lineJoin
|
|
1699
|
+
});
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
case 82: {
|
|
1703
|
+
const ihFont = reader.readUint32();
|
|
1704
|
+
const height = reader.readInt32();
|
|
1705
|
+
const width = reader.readInt32();
|
|
1706
|
+
const escapement = reader.readInt32();
|
|
1707
|
+
const orientation = reader.readInt32();
|
|
1708
|
+
const weight = reader.readInt32();
|
|
1709
|
+
const italic = reader.readUint8() !== 0;
|
|
1710
|
+
const underline = reader.readUint8() !== 0;
|
|
1711
|
+
const strikeOut = reader.readUint8() !== 0;
|
|
1712
|
+
const charSet = reader.readUint8();
|
|
1713
|
+
reader.skip(4);
|
|
1714
|
+
const faceName = reader.readUtf16String(32);
|
|
1715
|
+
objects.create(ihFont, {
|
|
1716
|
+
type: "font",
|
|
1717
|
+
height: Math.abs(height),
|
|
1718
|
+
width,
|
|
1719
|
+
weight,
|
|
1720
|
+
italic,
|
|
1721
|
+
underline,
|
|
1722
|
+
strikeOut,
|
|
1723
|
+
charSet,
|
|
1724
|
+
faceName,
|
|
1725
|
+
escapement,
|
|
1726
|
+
orientation
|
|
1727
|
+
});
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
case 48:
|
|
1731
|
+
case 49:
|
|
1732
|
+
case 50:
|
|
1733
|
+
case 51:
|
|
1734
|
+
case 52: break;
|
|
1735
|
+
case 27: {
|
|
1736
|
+
const x = reader.readInt32();
|
|
1737
|
+
const y = reader.readInt32();
|
|
1738
|
+
dc.state.currentPosX = x;
|
|
1739
|
+
dc.state.currentPosY = y;
|
|
1740
|
+
if (state.pathRecording) pathBuilder.moveTo(x, y);
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
case 54: {
|
|
1744
|
+
const x = reader.readInt32();
|
|
1745
|
+
const y = reader.readInt32();
|
|
1746
|
+
if (state.pathRecording) {
|
|
1747
|
+
if (!pathBuilder.hasContent) pathBuilder.moveTo(dc.state.currentPosX, dc.state.currentPosY);
|
|
1748
|
+
pathBuilder.lineTo(x, y);
|
|
1749
|
+
} else {
|
|
1750
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1751
|
+
trackPoint(state, dc, dc.state.currentPosX, dc.state.currentPosY);
|
|
1752
|
+
trackPoint(state, dc, x, y);
|
|
1753
|
+
emitLine(svg, dc, dc.state.currentPosX, dc.state.currentPosY, x, y);
|
|
1754
|
+
}
|
|
1755
|
+
dc.state.currentPosX = x;
|
|
1756
|
+
dc.state.currentPosY = y;
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1759
|
+
case 43: {
|
|
1760
|
+
const left = reader.readInt32();
|
|
1761
|
+
const top = reader.readInt32();
|
|
1762
|
+
const right = reader.readInt32();
|
|
1763
|
+
const bottom = reader.readInt32();
|
|
1764
|
+
if (state.pathRecording) {
|
|
1765
|
+
pathBuilder.moveTo(left, top);
|
|
1766
|
+
pathBuilder.lineTo(right, top);
|
|
1767
|
+
pathBuilder.lineTo(right, bottom);
|
|
1768
|
+
pathBuilder.lineTo(left, bottom);
|
|
1769
|
+
pathBuilder.closePath();
|
|
1770
|
+
} else {
|
|
1771
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1772
|
+
trackRect(state, dc, left, top, right - left, bottom - top);
|
|
1773
|
+
svg.rect(left, top, right - left, bottom - top, dc.getFillStyle(), dc.getStrokeStyle());
|
|
1774
|
+
}
|
|
1775
|
+
break;
|
|
1776
|
+
}
|
|
1777
|
+
case 42: {
|
|
1778
|
+
const left = reader.readInt32();
|
|
1779
|
+
const top = reader.readInt32();
|
|
1780
|
+
const right = reader.readInt32();
|
|
1781
|
+
const bottom = reader.readInt32();
|
|
1782
|
+
const cx = (left + right) / 2;
|
|
1783
|
+
const cy = (top + bottom) / 2;
|
|
1784
|
+
const rx = (right - left) / 2;
|
|
1785
|
+
const ry = (bottom - top) / 2;
|
|
1786
|
+
if (state.pathRecording) approximateEllipse(pathBuilder, cx, cy, rx, ry);
|
|
1787
|
+
else {
|
|
1788
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1789
|
+
trackRect(state, dc, left, top, right - left, bottom - top);
|
|
1790
|
+
svg.ellipse(cx, cy, rx, ry, dc.getFillStyle(), dc.getStrokeStyle());
|
|
1791
|
+
}
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
case 44: {
|
|
1795
|
+
const left = reader.readInt32();
|
|
1796
|
+
const top = reader.readInt32();
|
|
1797
|
+
const right = reader.readInt32();
|
|
1798
|
+
const bottom = reader.readInt32();
|
|
1799
|
+
const cornerW = reader.readInt32();
|
|
1800
|
+
const cornerH = reader.readInt32();
|
|
1801
|
+
if (!state.pathRecording) {
|
|
1802
|
+
const rx = cornerW / 2;
|
|
1803
|
+
const ry = cornerH / 2;
|
|
1804
|
+
const w = right - left;
|
|
1805
|
+
const h = bottom - top;
|
|
1806
|
+
const d = roundedRectPath(left, top, w, h, rx, ry);
|
|
1807
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1808
|
+
trackRect(state, dc, left, top, w, h);
|
|
1809
|
+
svg.path(d, dc.getFillStyle(), dc.getStrokeStyle());
|
|
1810
|
+
}
|
|
1811
|
+
break;
|
|
1812
|
+
}
|
|
1813
|
+
case 45:
|
|
1814
|
+
case 46:
|
|
1815
|
+
case 47: {
|
|
1816
|
+
const left = reader.readInt32();
|
|
1817
|
+
const top = reader.readInt32();
|
|
1818
|
+
const right = reader.readInt32();
|
|
1819
|
+
const bottom = reader.readInt32();
|
|
1820
|
+
const startPtX = reader.readInt32();
|
|
1821
|
+
const startPtY = reader.readInt32();
|
|
1822
|
+
const endPtX = reader.readInt32();
|
|
1823
|
+
const endPtY = reader.readInt32();
|
|
1824
|
+
const kind = type === 45 ? "arc" : type === 46 ? "chord" : "pie";
|
|
1825
|
+
const arc = buildArcPath(left, top, right, bottom, startPtX, startPtY, endPtX, endPtY, dc.state.arcDirection, kind);
|
|
1826
|
+
if (arc) {
|
|
1827
|
+
if (state.pathRecording) addArcToPathBuilder(pathBuilder, arc, dc.state.arcDirection, kind);
|
|
1828
|
+
else {
|
|
1829
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1830
|
+
trackRect(state, dc, left, top, right - left, bottom - top);
|
|
1831
|
+
if (kind === "arc") svg.path(arc.d, {
|
|
1832
|
+
enabled: false,
|
|
1833
|
+
color: {
|
|
1834
|
+
r: 0,
|
|
1835
|
+
g: 0,
|
|
1836
|
+
b: 0,
|
|
1837
|
+
a: 0
|
|
1838
|
+
},
|
|
1839
|
+
brushStyle: 1
|
|
1840
|
+
}, dc.getStrokeStyle());
|
|
1841
|
+
else svg.path(arc.d, dc.getFillStyle(), dc.getStrokeStyle());
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
case 55: {
|
|
1847
|
+
const arc = buildArcPath(reader.readInt32(), reader.readInt32(), reader.readInt32(), reader.readInt32(), reader.readInt32(), reader.readInt32(), reader.readInt32(), reader.readInt32(), dc.state.arcDirection, "arc");
|
|
1848
|
+
if (arc && state.pathRecording) {
|
|
1849
|
+
if (!pathBuilder.hasContent) pathBuilder.moveTo(dc.state.currentPosX, dc.state.currentPosY);
|
|
1850
|
+
pathBuilder.lineTo(arc.startX, arc.startY);
|
|
1851
|
+
pathBuilder.arcTo(arc.rx, arc.ry, 0, arc.largeArc, arc.sweep, arc.endX, arc.endY);
|
|
1852
|
+
dc.state.currentPosX = arc.endX;
|
|
1853
|
+
dc.state.currentPosY = arc.endY;
|
|
1854
|
+
}
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
case 41: {
|
|
1858
|
+
const cx = reader.readInt32();
|
|
1859
|
+
const cy = reader.readInt32();
|
|
1860
|
+
const radius = reader.readUint32();
|
|
1861
|
+
const startAngleDeg = reader.readFloat32();
|
|
1862
|
+
const sweepAngleDeg = reader.readFloat32();
|
|
1863
|
+
if (radius > 0 && sweepAngleDeg !== 0) {
|
|
1864
|
+
const startAngle = startAngleDeg * Math.PI / 180;
|
|
1865
|
+
const endAngle = (startAngleDeg + sweepAngleDeg) * Math.PI / 180;
|
|
1866
|
+
const sx = cx + radius * Math.cos(startAngle);
|
|
1867
|
+
const sy = cy + radius * Math.sin(startAngle);
|
|
1868
|
+
const ex = cx + radius * Math.cos(endAngle);
|
|
1869
|
+
const ey = cy + radius * Math.sin(endAngle);
|
|
1870
|
+
const sweepFlag = sweepAngleDeg > 0 ? 1 : 0;
|
|
1871
|
+
const largeArc = Math.abs(sweepAngleDeg) > 180 ? 1 : 0;
|
|
1872
|
+
if (state.pathRecording) {
|
|
1873
|
+
if (!pathBuilder.hasContent) pathBuilder.moveTo(dc.state.currentPosX, dc.state.currentPosY);
|
|
1874
|
+
pathBuilder.lineTo(sx, sy);
|
|
1875
|
+
pathBuilder.arcTo(radius, radius, 0, largeArc, sweepFlag, ex, ey);
|
|
1876
|
+
dc.state.currentPosX = ex;
|
|
1877
|
+
dc.state.currentPosY = ey;
|
|
1878
|
+
} else {
|
|
1879
|
+
const d = `M${fmtN(sx)},${fmtN(sy)}A${radius},${radius},0,${largeArc},${sweepFlag},${fmtN(ex)},${fmtN(ey)}`;
|
|
1880
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1881
|
+
trackPoint(state, dc, cx - radius, cy - radius);
|
|
1882
|
+
trackPoint(state, dc, cx + radius, cy + radius);
|
|
1883
|
+
svg.path(d, {
|
|
1884
|
+
enabled: false,
|
|
1885
|
+
color: {
|
|
1886
|
+
r: 0,
|
|
1887
|
+
g: 0,
|
|
1888
|
+
b: 0,
|
|
1889
|
+
a: 0
|
|
1890
|
+
},
|
|
1891
|
+
brushStyle: 1
|
|
1892
|
+
}, dc.getStrokeStyle());
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
case 86: {
|
|
1898
|
+
readBoundsRect(reader);
|
|
1899
|
+
const points = readPoints16(reader, reader.readUint32());
|
|
1900
|
+
if (state.pathRecording) addPolygonToPath(pathBuilder, points);
|
|
1901
|
+
else {
|
|
1902
|
+
const d = polygonToPathData(points);
|
|
1903
|
+
const fillRule = dc.state.polyFillMode === 1 ? "evenodd" : "nonzero";
|
|
1904
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1905
|
+
trackPoints(state, dc, points);
|
|
1906
|
+
svg.path(d, dc.getFillStyle(), dc.getStrokeStyle(), fillRule);
|
|
1907
|
+
}
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
case 87: {
|
|
1911
|
+
readBoundsRect(reader);
|
|
1912
|
+
const points = readPoints16(reader, reader.readUint32());
|
|
1913
|
+
if (state.pathRecording) addPolylineToPath(pathBuilder, points);
|
|
1914
|
+
else {
|
|
1915
|
+
const d = polylineToPathData(points);
|
|
1916
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1917
|
+
trackPoints(state, dc, points);
|
|
1918
|
+
svg.path(d, {
|
|
1919
|
+
enabled: false,
|
|
1920
|
+
color: {
|
|
1921
|
+
r: 0,
|
|
1922
|
+
g: 0,
|
|
1923
|
+
b: 0,
|
|
1924
|
+
a: 0
|
|
1925
|
+
},
|
|
1926
|
+
brushStyle: 1
|
|
1927
|
+
}, dc.getStrokeStyle());
|
|
1928
|
+
}
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
case 85: {
|
|
1932
|
+
readBoundsRect(reader);
|
|
1933
|
+
const points = readPoints16(reader, reader.readUint32());
|
|
1934
|
+
if (points.length >= 4) {
|
|
1935
|
+
if (state.pathRecording) addPolyBezierToPath(pathBuilder, points);
|
|
1936
|
+
else {
|
|
1937
|
+
const d = polyBezierToPathData(points);
|
|
1938
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1939
|
+
trackPoints(state, dc, points);
|
|
1940
|
+
svg.path(d, {
|
|
1941
|
+
enabled: false,
|
|
1942
|
+
color: {
|
|
1943
|
+
r: 0,
|
|
1944
|
+
g: 0,
|
|
1945
|
+
b: 0,
|
|
1946
|
+
a: 0
|
|
1947
|
+
},
|
|
1948
|
+
brushStyle: 1
|
|
1949
|
+
}, dc.getStrokeStyle());
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
case 88: {
|
|
1955
|
+
readBoundsRect(reader);
|
|
1956
|
+
const points = readPoints16(reader, reader.readUint32());
|
|
1957
|
+
if (state.pathRecording && points.length >= 3) {
|
|
1958
|
+
if (!pathBuilder.hasContent) pathBuilder.moveTo(dc.state.currentPosX, dc.state.currentPosY);
|
|
1959
|
+
for (let i = 0; i + 2 < points.length; i += 3) pathBuilder.curveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, points[i + 2].x, points[i + 2].y);
|
|
1960
|
+
const last = points[points.length - 1];
|
|
1961
|
+
dc.state.currentPosX = last.x;
|
|
1962
|
+
dc.state.currentPosY = last.y;
|
|
1963
|
+
}
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
case 89: {
|
|
1967
|
+
readBoundsRect(reader);
|
|
1968
|
+
const points = readPoints16(reader, reader.readUint32());
|
|
1969
|
+
if (state.pathRecording) {
|
|
1970
|
+
if (!pathBuilder.hasContent) pathBuilder.moveTo(dc.state.currentPosX, dc.state.currentPosY);
|
|
1971
|
+
for (const p of points) pathBuilder.lineTo(p.x, p.y);
|
|
1972
|
+
if (points.length > 0) {
|
|
1973
|
+
const last = points[points.length - 1];
|
|
1974
|
+
dc.state.currentPosX = last.x;
|
|
1975
|
+
dc.state.currentPosY = last.y;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
case 91: {
|
|
1981
|
+
readBoundsRect(reader);
|
|
1982
|
+
const numPolygons = reader.readUint32();
|
|
1983
|
+
const totalPoints = reader.readUint32();
|
|
1984
|
+
const polygonCounts = [];
|
|
1985
|
+
for (let i = 0; i < numPolygons; i++) polygonCounts.push(reader.readUint32());
|
|
1986
|
+
const allPoints = readPoints16(reader, totalPoints);
|
|
1987
|
+
let offset = 0;
|
|
1988
|
+
let d = "";
|
|
1989
|
+
for (let i = 0; i < numPolygons; i++) {
|
|
1990
|
+
const count = polygonCounts[i];
|
|
1991
|
+
const polyPoints = allPoints.slice(offset, offset + count);
|
|
1992
|
+
d += polygonToPathData(polyPoints);
|
|
1993
|
+
offset += count;
|
|
1994
|
+
}
|
|
1995
|
+
if (state.pathRecording) {} else {
|
|
1996
|
+
const fillRule = dc.state.polyFillMode === 1 ? "evenodd" : "nonzero";
|
|
1997
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
1998
|
+
trackPoints(state, dc, allPoints);
|
|
1999
|
+
svg.path(d, dc.getFillStyle(), dc.getStrokeStyle(), fillRule);
|
|
2000
|
+
}
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
case 3: {
|
|
2004
|
+
readBoundsRect(reader);
|
|
2005
|
+
const points = readPoints32(reader, reader.readUint32());
|
|
2006
|
+
if (!state.pathRecording) {
|
|
2007
|
+
const d = polygonToPathData(points);
|
|
2008
|
+
const fillRule = dc.state.polyFillMode === 1 ? "evenodd" : "nonzero";
|
|
2009
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2010
|
+
trackPoints(state, dc, points);
|
|
2011
|
+
svg.path(d, dc.getFillStyle(), dc.getStrokeStyle(), fillRule);
|
|
2012
|
+
}
|
|
2013
|
+
break;
|
|
2014
|
+
}
|
|
2015
|
+
case 4: {
|
|
2016
|
+
readBoundsRect(reader);
|
|
2017
|
+
const points = readPoints32(reader, reader.readUint32());
|
|
2018
|
+
if (!state.pathRecording) {
|
|
2019
|
+
const d = polylineToPathData(points);
|
|
2020
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2021
|
+
trackPoints(state, dc, points);
|
|
2022
|
+
svg.path(d, {
|
|
2023
|
+
enabled: false,
|
|
2024
|
+
color: {
|
|
2025
|
+
r: 0,
|
|
2026
|
+
g: 0,
|
|
2027
|
+
b: 0,
|
|
2028
|
+
a: 0
|
|
2029
|
+
},
|
|
2030
|
+
brushStyle: 1
|
|
2031
|
+
}, dc.getStrokeStyle());
|
|
2032
|
+
}
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
case 2: {
|
|
2036
|
+
readBoundsRect(reader);
|
|
2037
|
+
const points = readPoints32(reader, reader.readUint32());
|
|
2038
|
+
if (points.length >= 4 && !state.pathRecording) {
|
|
2039
|
+
const d = polyBezierToPathData(points);
|
|
2040
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2041
|
+
trackPoints(state, dc, points);
|
|
2042
|
+
svg.path(d, {
|
|
2043
|
+
enabled: false,
|
|
2044
|
+
color: {
|
|
2045
|
+
r: 0,
|
|
2046
|
+
g: 0,
|
|
2047
|
+
b: 0,
|
|
2048
|
+
a: 0
|
|
2049
|
+
},
|
|
2050
|
+
brushStyle: 1
|
|
2051
|
+
}, dc.getStrokeStyle());
|
|
2052
|
+
}
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
case 5: {
|
|
2056
|
+
readBoundsRect(reader);
|
|
2057
|
+
const points = readPoints32(reader, reader.readUint32());
|
|
2058
|
+
if (state.pathRecording && points.length >= 3) {
|
|
2059
|
+
if (!pathBuilder.hasContent) pathBuilder.moveTo(dc.state.currentPosX, dc.state.currentPosY);
|
|
2060
|
+
for (let i = 0; i + 2 < points.length; i += 3) pathBuilder.curveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, points[i + 2].x, points[i + 2].y);
|
|
2061
|
+
const last = points[points.length - 1];
|
|
2062
|
+
dc.state.currentPosX = last.x;
|
|
2063
|
+
dc.state.currentPosY = last.y;
|
|
2064
|
+
}
|
|
2065
|
+
break;
|
|
2066
|
+
}
|
|
2067
|
+
case 6: {
|
|
2068
|
+
readBoundsRect(reader);
|
|
2069
|
+
const points = readPoints32(reader, reader.readUint32());
|
|
2070
|
+
if (state.pathRecording) {
|
|
2071
|
+
if (!pathBuilder.hasContent) pathBuilder.moveTo(dc.state.currentPosX, dc.state.currentPosY);
|
|
2072
|
+
for (const p of points) pathBuilder.lineTo(p.x, p.y);
|
|
2073
|
+
if (points.length > 0) {
|
|
2074
|
+
const last = points[points.length - 1];
|
|
2075
|
+
dc.state.currentPosX = last.x;
|
|
2076
|
+
dc.state.currentPosY = last.y;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
break;
|
|
2080
|
+
}
|
|
2081
|
+
case 59:
|
|
2082
|
+
pathBuilder.clear();
|
|
2083
|
+
break;
|
|
2084
|
+
case 60: break;
|
|
2085
|
+
case 61:
|
|
2086
|
+
if (state.pathRecording) pathBuilder.closePath();
|
|
2087
|
+
break;
|
|
2088
|
+
case 62: {
|
|
2089
|
+
const d = pathBuilder.toPathData();
|
|
2090
|
+
if (d) {
|
|
2091
|
+
const fillRule = dc.state.polyFillMode === 1 ? "evenodd" : "nonzero";
|
|
2092
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2093
|
+
trackPathBounds(state, dc, pathBuilder);
|
|
2094
|
+
svg.path(d, dc.getFillStyle(), {
|
|
2095
|
+
enabled: false,
|
|
2096
|
+
color: {
|
|
2097
|
+
r: 0,
|
|
2098
|
+
g: 0,
|
|
2099
|
+
b: 0,
|
|
2100
|
+
a: 0
|
|
2101
|
+
},
|
|
2102
|
+
width: 0,
|
|
2103
|
+
penStyle: PenStyle.PS_NULL
|
|
2104
|
+
}, fillRule);
|
|
2105
|
+
}
|
|
2106
|
+
pathBuilder.clear();
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
case 64: {
|
|
2110
|
+
const d = pathBuilder.toPathData();
|
|
2111
|
+
if (d) {
|
|
2112
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2113
|
+
trackPathBounds(state, dc, pathBuilder);
|
|
2114
|
+
svg.path(d, {
|
|
2115
|
+
enabled: false,
|
|
2116
|
+
color: {
|
|
2117
|
+
r: 0,
|
|
2118
|
+
g: 0,
|
|
2119
|
+
b: 0,
|
|
2120
|
+
a: 0
|
|
2121
|
+
},
|
|
2122
|
+
brushStyle: 1
|
|
2123
|
+
}, dc.getStrokeStyle());
|
|
2124
|
+
}
|
|
2125
|
+
pathBuilder.clear();
|
|
2126
|
+
break;
|
|
2127
|
+
}
|
|
2128
|
+
case 63: {
|
|
2129
|
+
const d = pathBuilder.toPathData();
|
|
2130
|
+
if (d) {
|
|
2131
|
+
const fillRule = dc.state.polyFillMode === 1 ? "evenodd" : "nonzero";
|
|
2132
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2133
|
+
trackPathBounds(state, dc, pathBuilder);
|
|
2134
|
+
svg.path(d, dc.getFillStyle(), dc.getStrokeStyle(), fillRule);
|
|
2135
|
+
}
|
|
2136
|
+
pathBuilder.clear();
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
case 68:
|
|
2140
|
+
pathBuilder.clear();
|
|
2141
|
+
break;
|
|
2142
|
+
case 65:
|
|
2143
|
+
case 66: break;
|
|
2144
|
+
case 67: {
|
|
2145
|
+
const d = pathBuilder.toPathData();
|
|
2146
|
+
const regionMode = reader.readUint32();
|
|
2147
|
+
if (d) {
|
|
2148
|
+
const fillRule = dc.state.polyFillMode === 1 ? "evenodd" : "nonzero";
|
|
2149
|
+
const clipId = svg.addClipPath(d, fillRule, getOptionalTransformString(dc));
|
|
2150
|
+
trackClipPathBounds(state, dc, pathBuilder);
|
|
2151
|
+
if (regionMode === 1) dc.state.clipPathIds = [...dc.state.clipPathIds, clipId];
|
|
2152
|
+
else dc.state.clipPathIds = [clipId];
|
|
2153
|
+
syncClipGroups(svg, dc, state);
|
|
2154
|
+
}
|
|
2155
|
+
pathBuilder.clear();
|
|
2156
|
+
break;
|
|
2157
|
+
}
|
|
2158
|
+
case 30: {
|
|
2159
|
+
const left = reader.readInt32();
|
|
2160
|
+
const top = reader.readInt32();
|
|
2161
|
+
const right = reader.readInt32();
|
|
2162
|
+
const bottom = reader.readInt32();
|
|
2163
|
+
const w = right - left;
|
|
2164
|
+
const h = bottom - top;
|
|
2165
|
+
if (w > 0 && h > 0) {
|
|
2166
|
+
const d = `M${left},${top}L${right},${top}L${right},${bottom}L${left},${bottom}Z`;
|
|
2167
|
+
const clipId = svg.addClipPath(d, void 0, getOptionalTransformString(dc));
|
|
2168
|
+
trackClipRect(state, dc, left, top, w, h);
|
|
2169
|
+
dc.state.clipPathIds = [...dc.state.clipPathIds, clipId];
|
|
2170
|
+
syncClipGroups(svg, dc, state);
|
|
2171
|
+
}
|
|
2172
|
+
break;
|
|
2173
|
+
}
|
|
2174
|
+
case 84: {
|
|
2175
|
+
readBoundsRect(reader);
|
|
2176
|
+
reader.skip(4);
|
|
2177
|
+
reader.skip(4);
|
|
2178
|
+
reader.skip(4);
|
|
2179
|
+
const refX = reader.readInt32();
|
|
2180
|
+
const refY = reader.readInt32();
|
|
2181
|
+
const nChars = reader.readUint32();
|
|
2182
|
+
const offString = reader.readUint32();
|
|
2183
|
+
const options = reader.readUint32();
|
|
2184
|
+
reader.skip(16);
|
|
2185
|
+
const offDx = reader.readUint32();
|
|
2186
|
+
if (nChars > 0 && offString > 0) {
|
|
2187
|
+
const recordStart = dataStart - 8;
|
|
2188
|
+
reader.seek(recordStart + offString);
|
|
2189
|
+
const text = decodePrivateUseMathText(reader.readUtf16String(nChars), dc.state.font);
|
|
2190
|
+
if (text.length > 0) {
|
|
2191
|
+
const font = dc.state.font;
|
|
2192
|
+
const fontSize = font.height > 0 ? font.height : 12;
|
|
2193
|
+
const fontWeight = font.weight;
|
|
2194
|
+
const fontStyle = font.italic ? "italic" : "normal";
|
|
2195
|
+
const fontFamily = svgFontFamilyForEmfText(font);
|
|
2196
|
+
const point = textOriginFromCurrentPosition(dc, refX, refY);
|
|
2197
|
+
let anchor = "start";
|
|
2198
|
+
const hAlign = dc.state.textAlign & 6;
|
|
2199
|
+
if (hAlign === 6) anchor = "middle";
|
|
2200
|
+
else if (hAlign === 2) anchor = "end";
|
|
2201
|
+
let baseline = "auto";
|
|
2202
|
+
const vAlign = dc.state.textAlign & 24;
|
|
2203
|
+
if (vAlign === 24) baseline = "auto";
|
|
2204
|
+
else if (vAlign === 8) baseline = "text-after-edge";
|
|
2205
|
+
else baseline = "text-before-edge";
|
|
2206
|
+
const textY = baseline === "auto" && point.y === 0 ? point.y + fontSize : point.y;
|
|
2207
|
+
const dx = readTextDx$1(reader, recordStart, offDx, nChars, options);
|
|
2208
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2209
|
+
emitEmfText(svg, dc, state, point.x, textY, text, fontFamily, fontSize, fontWeight, fontStyle, anchor, baseline, dx);
|
|
2210
|
+
updateCurrentTextPosition(dc, point.x, textY, text, fontSize, font.width ?? 0, dx);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
break;
|
|
2214
|
+
}
|
|
2215
|
+
case 81: {
|
|
2216
|
+
readBoundsRect(reader);
|
|
2217
|
+
const xDest = reader.readInt32();
|
|
2218
|
+
const yDest = reader.readInt32();
|
|
2219
|
+
reader.skip(4);
|
|
2220
|
+
reader.skip(4);
|
|
2221
|
+
reader.skip(4);
|
|
2222
|
+
reader.skip(4);
|
|
2223
|
+
const offBmiSrc = reader.readUint32();
|
|
2224
|
+
const cbBmiSrc = reader.readUint32();
|
|
2225
|
+
const offBitsSrc = reader.readUint32();
|
|
2226
|
+
const cbBitsSrc = reader.readUint32();
|
|
2227
|
+
reader.skip(4);
|
|
2228
|
+
reader.skip(4);
|
|
2229
|
+
const cxDest = reader.readInt32();
|
|
2230
|
+
const cyDest = reader.readInt32();
|
|
2231
|
+
if (cbBmiSrc > 0 && cbBitsSrc > 0) {
|
|
2232
|
+
const recordStart = dataStart - 8;
|
|
2233
|
+
reader.seek(recordStart + offBmiSrc);
|
|
2234
|
+
const dibHeader = reader.readSlice(cbBmiSrc);
|
|
2235
|
+
reader.seek(recordStart + offBitsSrc);
|
|
2236
|
+
const bitmapBits = reader.readSlice(cbBitsSrc);
|
|
2237
|
+
const format = detectEmbeddedFormat(bitmapBits);
|
|
2238
|
+
let dataUri;
|
|
2239
|
+
if (format === "png" || format === "jpeg") dataUri = rawImageToDataUri(bitmapBits);
|
|
2240
|
+
else dataUri = dibToDataUri(dibHeader, bitmapBits);
|
|
2241
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2242
|
+
trackRect(state, dc, xDest, yDest, Math.abs(cxDest), Math.abs(cyDest));
|
|
2243
|
+
svg.image(xDest, yDest, Math.abs(cxDest), Math.abs(cyDest), dataUri);
|
|
2244
|
+
}
|
|
2245
|
+
break;
|
|
2246
|
+
}
|
|
2247
|
+
case 76: {
|
|
2248
|
+
readBoundsRect(reader);
|
|
2249
|
+
const xDest = reader.readInt32();
|
|
2250
|
+
const yDest = reader.readInt32();
|
|
2251
|
+
const cxDest = reader.readInt32();
|
|
2252
|
+
const cyDest = reader.readInt32();
|
|
2253
|
+
const dwRop = reader.readUint32();
|
|
2254
|
+
reader.skip(4);
|
|
2255
|
+
reader.skip(4);
|
|
2256
|
+
reader.skip(24);
|
|
2257
|
+
reader.skip(4);
|
|
2258
|
+
reader.skip(4);
|
|
2259
|
+
const offBmiSrc = reader.readUint32();
|
|
2260
|
+
const cbBmiSrc = reader.readUint32();
|
|
2261
|
+
const offBitsSrc = reader.readUint32();
|
|
2262
|
+
const cbBitsSrc = reader.readUint32();
|
|
2263
|
+
if (cbBmiSrc > 0 && cbBitsSrc > 0) {
|
|
2264
|
+
const recordStart = dataStart - 8;
|
|
2265
|
+
reader.seek(recordStart + offBmiSrc);
|
|
2266
|
+
const dibHeader = reader.readSlice(cbBmiSrc);
|
|
2267
|
+
reader.seek(recordStart + offBitsSrc);
|
|
2268
|
+
const bitmapBits = reader.readSlice(cbBitsSrc);
|
|
2269
|
+
const format = detectEmbeddedFormat(bitmapBits);
|
|
2270
|
+
let dataUri;
|
|
2271
|
+
if (format === "png" || format === "jpeg") dataUri = rawImageToDataUri(bitmapBits);
|
|
2272
|
+
else dataUri = dibToDataUri(dibHeader, bitmapBits);
|
|
2273
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2274
|
+
svg.image(xDest, yDest, Math.abs(cxDest), Math.abs(cyDest), dataUri);
|
|
2275
|
+
} else if (dwRop === ROP_PATCOPY$1 && cxDest !== 0 && cyDest !== 0) {
|
|
2276
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2277
|
+
trackRect(state, dc, xDest, yDest, Math.abs(cxDest), Math.abs(cyDest));
|
|
2278
|
+
svg.rect(xDest, yDest, Math.abs(cxDest), Math.abs(cyDest), dc.getFillStyle(), {
|
|
2279
|
+
enabled: false,
|
|
2280
|
+
color: dc.state.pen.color,
|
|
2281
|
+
penStyle: PenStyle.PS_NULL,
|
|
2282
|
+
width: 0
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
break;
|
|
2286
|
+
}
|
|
2287
|
+
case 15:
|
|
2288
|
+
case 26:
|
|
2289
|
+
case 28:
|
|
2290
|
+
case 29:
|
|
2291
|
+
case 31:
|
|
2292
|
+
case 32:
|
|
2293
|
+
case 75:
|
|
2294
|
+
case 71:
|
|
2295
|
+
case 72:
|
|
2296
|
+
case 74:
|
|
2297
|
+
case 73:
|
|
2298
|
+
case 53:
|
|
2299
|
+
case 56:
|
|
2300
|
+
case 92:
|
|
2301
|
+
case 7:
|
|
2302
|
+
case 90:
|
|
2303
|
+
case 77:
|
|
2304
|
+
case 78:
|
|
2305
|
+
case 79:
|
|
2306
|
+
case 80:
|
|
2307
|
+
case 83:
|
|
2308
|
+
case 93:
|
|
2309
|
+
case 94:
|
|
2310
|
+
case 96:
|
|
2311
|
+
case 97:
|
|
2312
|
+
case 108: break;
|
|
2313
|
+
case 118: {
|
|
2314
|
+
readBoundsRect(reader);
|
|
2315
|
+
const nTriVert = reader.readUint32();
|
|
2316
|
+
const nGradObj = reader.readUint32();
|
|
2317
|
+
const ulMode = reader.readUint32();
|
|
2318
|
+
const vertices = [];
|
|
2319
|
+
for (let i = 0; i < nTriVert; i++) {
|
|
2320
|
+
const x = reader.readInt32();
|
|
2321
|
+
const y = reader.readInt32();
|
|
2322
|
+
const r = reader.readUint16() >> 8;
|
|
2323
|
+
const g = reader.readUint16() >> 8;
|
|
2324
|
+
const b = reader.readUint16() >> 8;
|
|
2325
|
+
const a = reader.readUint16() >> 8;
|
|
2326
|
+
vertices.push({
|
|
2327
|
+
x,
|
|
2328
|
+
y,
|
|
2329
|
+
r,
|
|
2330
|
+
g,
|
|
2331
|
+
b,
|
|
2332
|
+
a
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2336
|
+
if (ulMode === 0 || ulMode === 1) for (let i = 0; i < nGradObj; i++) {
|
|
2337
|
+
const ul = reader.readUint32();
|
|
2338
|
+
const lr = reader.readUint32();
|
|
2339
|
+
const v0 = vertices[ul];
|
|
2340
|
+
const v1 = vertices[lr];
|
|
2341
|
+
if (!v0 || !v1) continue;
|
|
2342
|
+
const c0 = `rgb(${v0.r},${v0.g},${v0.b})`;
|
|
2343
|
+
const c1 = `rgb(${v1.r},${v1.g},${v1.b})`;
|
|
2344
|
+
const x1 = v0.x;
|
|
2345
|
+
const y1 = v0.y;
|
|
2346
|
+
const x2 = ulMode === 0 ? v1.x : v0.x;
|
|
2347
|
+
const y2 = ulMode === 0 ? v0.y : v1.y;
|
|
2348
|
+
const gradId = svg.addLinearGradient(x1, y1, x2, y2, [{
|
|
2349
|
+
offset: 0,
|
|
2350
|
+
color: c0
|
|
2351
|
+
}, {
|
|
2352
|
+
offset: 1,
|
|
2353
|
+
color: c1
|
|
2354
|
+
}]);
|
|
2355
|
+
const rx = Math.min(v0.x, v1.x);
|
|
2356
|
+
const ry = Math.min(v0.y, v1.y);
|
|
2357
|
+
const rw = Math.abs(v1.x - v0.x);
|
|
2358
|
+
const rh = Math.abs(v1.y - v0.y);
|
|
2359
|
+
trackRect(state, dc, rx, ry, rw, rh);
|
|
2360
|
+
svg.gradientRect(rx, ry, rw, rh, gradId);
|
|
2361
|
+
}
|
|
2362
|
+
else if (ulMode === 2) for (let i = 0; i < nGradObj; i++) {
|
|
2363
|
+
const i0 = reader.readUint32();
|
|
2364
|
+
const i1 = reader.readUint32();
|
|
2365
|
+
const i2 = reader.readUint32();
|
|
2366
|
+
const v0 = vertices[i0];
|
|
2367
|
+
const v1 = vertices[i1];
|
|
2368
|
+
const v2 = vertices[i2];
|
|
2369
|
+
if (!v0 || !v1 || !v2) continue;
|
|
2370
|
+
const ar = Math.round((v0.r + v1.r + v2.r) / 3);
|
|
2371
|
+
const ag = Math.round((v0.g + v1.g + v2.g) / 3);
|
|
2372
|
+
const ab = Math.round((v0.b + v1.b + v2.b) / 3);
|
|
2373
|
+
const pts = `${v0.x},${v0.y} ${v1.x},${v1.y} ${v2.x},${v2.y}`;
|
|
2374
|
+
trackPoint(state, dc, v0.x, v0.y);
|
|
2375
|
+
trackPoint(state, dc, v1.x, v1.y);
|
|
2376
|
+
trackPoint(state, dc, v2.x, v2.y);
|
|
2377
|
+
svg.gradientPolygon(pts, `rgb(${ar},${ag},${ab})`);
|
|
2378
|
+
}
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
case 114: {
|
|
2382
|
+
readBoundsRect(reader);
|
|
2383
|
+
const xDest = reader.readInt32();
|
|
2384
|
+
const yDest = reader.readInt32();
|
|
2385
|
+
const cxDest = reader.readInt32();
|
|
2386
|
+
const cyDest = reader.readInt32();
|
|
2387
|
+
reader.readUint8();
|
|
2388
|
+
reader.readUint8();
|
|
2389
|
+
const srcConstantAlpha = reader.readUint8();
|
|
2390
|
+
reader.readUint8();
|
|
2391
|
+
reader.skip(4);
|
|
2392
|
+
reader.skip(4);
|
|
2393
|
+
reader.skip(24);
|
|
2394
|
+
reader.skip(4);
|
|
2395
|
+
reader.skip(4);
|
|
2396
|
+
const offBmiSrc = reader.readUint32();
|
|
2397
|
+
const cbBmiSrc = reader.readUint32();
|
|
2398
|
+
const offBitsSrc = reader.readUint32();
|
|
2399
|
+
const cbBitsSrc = reader.readUint32();
|
|
2400
|
+
if (cbBmiSrc > 0 && cbBitsSrc > 0) {
|
|
2401
|
+
const recordStart = dataStart - 8;
|
|
2402
|
+
reader.seek(recordStart + offBmiSrc);
|
|
2403
|
+
const dibHeader = reader.readSlice(cbBmiSrc);
|
|
2404
|
+
reader.seek(recordStart + offBitsSrc);
|
|
2405
|
+
const bitmapBits = reader.readSlice(cbBitsSrc);
|
|
2406
|
+
const format = detectEmbeddedFormat(bitmapBits);
|
|
2407
|
+
let dataUri;
|
|
2408
|
+
if (format === "png" || format === "jpeg") dataUri = rawImageToDataUri(bitmapBits);
|
|
2409
|
+
else dataUri = dibToDataUri(dibHeader, bitmapBits);
|
|
2410
|
+
const opacity = srcConstantAlpha / 255;
|
|
2411
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2412
|
+
trackRect(state, dc, xDest, yDest, Math.abs(cxDest), Math.abs(cyDest));
|
|
2413
|
+
svg.imageWithOpacity(xDest, yDest, Math.abs(cxDest), Math.abs(cyDest), dataUri, opacity);
|
|
2414
|
+
}
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
case 116: {
|
|
2418
|
+
readBoundsRect(reader);
|
|
2419
|
+
const xDest = reader.readInt32();
|
|
2420
|
+
const yDest = reader.readInt32();
|
|
2421
|
+
const cxDest = reader.readInt32();
|
|
2422
|
+
const cyDest = reader.readInt32();
|
|
2423
|
+
reader.skip(4);
|
|
2424
|
+
reader.skip(4);
|
|
2425
|
+
reader.skip(4);
|
|
2426
|
+
reader.skip(24);
|
|
2427
|
+
reader.skip(4);
|
|
2428
|
+
reader.skip(4);
|
|
2429
|
+
const offBmiSrc = reader.readUint32();
|
|
2430
|
+
const cbBmiSrc = reader.readUint32();
|
|
2431
|
+
const offBitsSrc = reader.readUint32();
|
|
2432
|
+
const cbBitsSrc = reader.readUint32();
|
|
2433
|
+
if (cbBmiSrc > 0 && cbBitsSrc > 0) {
|
|
2434
|
+
const recordStart = dataStart - 8;
|
|
2435
|
+
reader.seek(recordStart + offBmiSrc);
|
|
2436
|
+
const dibHeader = reader.readSlice(cbBmiSrc);
|
|
2437
|
+
reader.seek(recordStart + offBitsSrc);
|
|
2438
|
+
const bitmapBits = reader.readSlice(cbBitsSrc);
|
|
2439
|
+
const format = detectEmbeddedFormat(bitmapBits);
|
|
2440
|
+
let dataUri;
|
|
2441
|
+
if (format === "png" || format === "jpeg") dataUri = rawImageToDataUri(bitmapBits);
|
|
2442
|
+
else dataUri = dibToDataUri(dibHeader, bitmapBits);
|
|
2443
|
+
ensureWorldTransformGroup(svg, dc, state);
|
|
2444
|
+
trackRect(state, dc, xDest, yDest, Math.abs(cxDest), Math.abs(cyDest));
|
|
2445
|
+
svg.image(xDest, yDest, Math.abs(cxDest), Math.abs(cyDest), dataUri);
|
|
2446
|
+
}
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function parseHeader(reader) {
|
|
2452
|
+
if (reader.remaining < 88) return null;
|
|
2453
|
+
const startPos = reader.position;
|
|
2454
|
+
const recordType = reader.readUint32();
|
|
2455
|
+
const recordSize = reader.readUint32();
|
|
2456
|
+
if (recordType !== 1) return null;
|
|
2457
|
+
const boundsLeft = reader.readInt32();
|
|
2458
|
+
const boundsTop = reader.readInt32();
|
|
2459
|
+
const boundsRight = reader.readInt32();
|
|
2460
|
+
const boundsBottom = reader.readInt32();
|
|
2461
|
+
const frameLeft = reader.readInt32();
|
|
2462
|
+
const frameTop = reader.readInt32();
|
|
2463
|
+
const frameRight = reader.readInt32();
|
|
2464
|
+
const frameBottom = reader.readInt32();
|
|
2465
|
+
const signature = reader.readUint32();
|
|
2466
|
+
if (signature !== 1179469088) return null;
|
|
2467
|
+
const version = reader.readUint32();
|
|
2468
|
+
reader.readUint32();
|
|
2469
|
+
const numRecords = reader.readUint32();
|
|
2470
|
+
const numHandles = reader.readUint16();
|
|
2471
|
+
reader.readUint16();
|
|
2472
|
+
const descriptionLen = reader.readUint32();
|
|
2473
|
+
const descriptionOff = reader.readUint32();
|
|
2474
|
+
const nPalEntries = reader.readUint32();
|
|
2475
|
+
const deviceWidth = reader.readInt32();
|
|
2476
|
+
const deviceHeight = reader.readInt32();
|
|
2477
|
+
const deviceWidthMm = reader.readInt32();
|
|
2478
|
+
const deviceHeightMm = reader.readInt32();
|
|
2479
|
+
let description = "";
|
|
2480
|
+
if (descriptionLen > 0 && descriptionOff > 0) {
|
|
2481
|
+
const savedPos = reader.position;
|
|
2482
|
+
reader.seek(startPos + descriptionOff);
|
|
2483
|
+
description = reader.readUtf16String(descriptionLen);
|
|
2484
|
+
reader.seek(savedPos);
|
|
2485
|
+
}
|
|
2486
|
+
let pixelFormatSize = 0;
|
|
2487
|
+
let pixelFormatOff = 0;
|
|
2488
|
+
let hasOpenGl = false;
|
|
2489
|
+
let deviceWidthUm = 0;
|
|
2490
|
+
let deviceHeightUm = 0;
|
|
2491
|
+
if (recordSize >= 100) {
|
|
2492
|
+
reader.seek(startPos + 88);
|
|
2493
|
+
pixelFormatSize = reader.readUint32();
|
|
2494
|
+
pixelFormatOff = reader.readUint32();
|
|
2495
|
+
hasOpenGl = reader.readUint32() !== 0;
|
|
2496
|
+
}
|
|
2497
|
+
if (recordSize >= 108) {
|
|
2498
|
+
deviceWidthUm = reader.readUint32();
|
|
2499
|
+
deviceHeightUm = reader.readUint32();
|
|
2500
|
+
}
|
|
2501
|
+
return {
|
|
2502
|
+
bounds: {
|
|
2503
|
+
left: boundsLeft,
|
|
2504
|
+
top: boundsTop,
|
|
2505
|
+
right: boundsRight,
|
|
2506
|
+
bottom: boundsBottom
|
|
2507
|
+
},
|
|
2508
|
+
frame: {
|
|
2509
|
+
left: frameLeft,
|
|
2510
|
+
top: frameTop,
|
|
2511
|
+
right: frameRight,
|
|
2512
|
+
bottom: frameBottom
|
|
2513
|
+
},
|
|
2514
|
+
signature,
|
|
2515
|
+
version,
|
|
2516
|
+
size: recordSize,
|
|
2517
|
+
numRecords,
|
|
2518
|
+
numHandles,
|
|
2519
|
+
description,
|
|
2520
|
+
nPalEntries,
|
|
2521
|
+
deviceWidth,
|
|
2522
|
+
deviceHeight,
|
|
2523
|
+
deviceWidthMm,
|
|
2524
|
+
deviceHeightMm,
|
|
2525
|
+
pixelFormatSize,
|
|
2526
|
+
pixelFormatOff,
|
|
2527
|
+
hasOpenGl,
|
|
2528
|
+
deviceWidthUm,
|
|
2529
|
+
deviceHeightUm
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
function readColorRef(reader) {
|
|
2533
|
+
const r = reader.readUint8();
|
|
2534
|
+
const g = reader.readUint8();
|
|
2535
|
+
const b = reader.readUint8();
|
|
2536
|
+
const a = reader.readUint8();
|
|
2537
|
+
return {
|
|
2538
|
+
r,
|
|
2539
|
+
g,
|
|
2540
|
+
b,
|
|
2541
|
+
a: a === 0 ? 255 : a
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
function readXForm(reader) {
|
|
2545
|
+
return [
|
|
2546
|
+
reader.readFloat32(),
|
|
2547
|
+
reader.readFloat32(),
|
|
2548
|
+
reader.readFloat32(),
|
|
2549
|
+
reader.readFloat32(),
|
|
2550
|
+
reader.readFloat32(),
|
|
2551
|
+
reader.readFloat32()
|
|
2552
|
+
];
|
|
2553
|
+
}
|
|
2554
|
+
function readBoundsRect(reader) {
|
|
2555
|
+
return {
|
|
2556
|
+
left: reader.readInt32(),
|
|
2557
|
+
top: reader.readInt32(),
|
|
2558
|
+
right: reader.readInt32(),
|
|
2559
|
+
bottom: reader.readInt32()
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
function readPoints16(reader, count) {
|
|
2563
|
+
const points = [];
|
|
2564
|
+
for (let i = 0; i < count; i++) {
|
|
2565
|
+
const x = reader.readInt16();
|
|
2566
|
+
const y = reader.readInt16();
|
|
2567
|
+
points.push({
|
|
2568
|
+
x,
|
|
2569
|
+
y
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
return points;
|
|
2573
|
+
}
|
|
2574
|
+
function readPoints32(reader, count) {
|
|
2575
|
+
const points = [];
|
|
2576
|
+
for (let i = 0; i < count; i++) {
|
|
2577
|
+
const x = reader.readInt32();
|
|
2578
|
+
const y = reader.readInt32();
|
|
2579
|
+
points.push({
|
|
2580
|
+
x,
|
|
2581
|
+
y
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
return points;
|
|
2585
|
+
}
|
|
2586
|
+
function emitLine(svg, dc, x1, y1, x2, y2) {
|
|
2587
|
+
const d = `M${x1},${y1}L${x2},${y2}`;
|
|
2588
|
+
svg.path(d, {
|
|
2589
|
+
enabled: false,
|
|
2590
|
+
color: {
|
|
2591
|
+
r: 0,
|
|
2592
|
+
g: 0,
|
|
2593
|
+
b: 0,
|
|
2594
|
+
a: 0
|
|
2595
|
+
},
|
|
2596
|
+
brushStyle: 1
|
|
2597
|
+
}, dc.getStrokeStyle());
|
|
2598
|
+
}
|
|
2599
|
+
function polygonToPathData(points) {
|
|
2600
|
+
if (points.length === 0) return "";
|
|
2601
|
+
let d = `M${points[0].x},${points[0].y}`;
|
|
2602
|
+
for (let i = 1; i < points.length; i++) d += `L${points[i].x},${points[i].y}`;
|
|
2603
|
+
d += "Z";
|
|
2604
|
+
return d;
|
|
2605
|
+
}
|
|
2606
|
+
function polylineToPathData(points) {
|
|
2607
|
+
if (points.length === 0) return "";
|
|
2608
|
+
let d = `M${points[0].x},${points[0].y}`;
|
|
2609
|
+
for (let i = 1; i < points.length; i++) d += `L${points[i].x},${points[i].y}`;
|
|
2610
|
+
return d;
|
|
2611
|
+
}
|
|
2612
|
+
function polyBezierToPathData(points) {
|
|
2613
|
+
if (points.length < 4) return "";
|
|
2614
|
+
let d = `M${points[0].x},${points[0].y}`;
|
|
2615
|
+
for (let i = 1; i + 2 < points.length; i += 3) d += `C${points[i].x},${points[i].y},${points[i + 1].x},${points[i + 1].y},${points[i + 2].x},${points[i + 2].y}`;
|
|
2616
|
+
return d;
|
|
2617
|
+
}
|
|
2618
|
+
function addPolygonToPath(builder, points) {
|
|
2619
|
+
if (points.length === 0) return;
|
|
2620
|
+
builder.moveTo(points[0].x, points[0].y);
|
|
2621
|
+
for (let i = 1; i < points.length; i++) builder.lineTo(points[i].x, points[i].y);
|
|
2622
|
+
builder.closePath();
|
|
2623
|
+
}
|
|
2624
|
+
function addPolylineToPath(builder, points) {
|
|
2625
|
+
if (points.length === 0) return;
|
|
2626
|
+
builder.moveTo(points[0].x, points[0].y);
|
|
2627
|
+
for (let i = 1; i < points.length; i++) builder.lineTo(points[i].x, points[i].y);
|
|
2628
|
+
}
|
|
2629
|
+
function addPolyBezierToPath(builder, points) {
|
|
2630
|
+
if (points.length < 4) return;
|
|
2631
|
+
builder.moveTo(points[0].x, points[0].y);
|
|
2632
|
+
for (let i = 1; i + 2 < points.length; i += 3) builder.curveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y, points[i + 2].x, points[i + 2].y);
|
|
2633
|
+
}
|
|
2634
|
+
/** Approximate an ellipse with 4 cubic bezier curves */
|
|
2635
|
+
function approximateEllipse(builder, cx, cy, rx, ry) {
|
|
2636
|
+
const k = .5522847498;
|
|
2637
|
+
const kx = rx * k;
|
|
2638
|
+
const ky = ry * k;
|
|
2639
|
+
builder.moveTo(cx + rx, cy);
|
|
2640
|
+
builder.curveTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
|
|
2641
|
+
builder.curveTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
|
|
2642
|
+
builder.curveTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
|
|
2643
|
+
builder.curveTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
|
|
2644
|
+
builder.closePath();
|
|
2645
|
+
}
|
|
2646
|
+
/** Build SVG path data for a rounded rectangle */
|
|
2647
|
+
function roundedRectPath(x, y, w, h, rx, ry) {
|
|
2648
|
+
const r = Math.min(rx, w / 2);
|
|
2649
|
+
const rv = Math.min(ry, h / 2);
|
|
2650
|
+
return `M${x + r},${y}L${x + w - r},${y}Q${x + w},${y},${x + w},${y + rv}L${x + w},${y + h - rv}Q${x + w},${y + h},${x + w - r},${y + h}L${x + r},${y + h}Q${x},${y + h},${x},${y + h - rv}L${x},${y + rv}Q${x},${y},${x + r},${y}Z`;
|
|
2651
|
+
}
|
|
2652
|
+
/** Format a number for SVG: up to 2 decimal places, no trailing zeros */
|
|
2653
|
+
function fmtN(n) {
|
|
2654
|
+
return Number(n.toFixed(2)).toString();
|
|
2655
|
+
}
|
|
2656
|
+
/**
|
|
2657
|
+
* Compute the intersection of a ray from (cx,cy) through (px,py) with an ellipse
|
|
2658
|
+
* centered at (cx,cy) with semi-axes rx, ry.
|
|
2659
|
+
*/
|
|
2660
|
+
function intersectEllipseRay(cx, cy, rx, ry, px, py) {
|
|
2661
|
+
const dx = px - cx;
|
|
2662
|
+
const dy = py - cy;
|
|
2663
|
+
if (dx === 0 && dy === 0) return {
|
|
2664
|
+
x: cx + rx,
|
|
2665
|
+
y: cy,
|
|
2666
|
+
angle: 0
|
|
2667
|
+
};
|
|
2668
|
+
const t = 1 / Math.sqrt((dx / rx) ** 2 + (dy / ry) ** 2);
|
|
2669
|
+
const hitX = cx + dx * t;
|
|
2670
|
+
const hitY = cy + dy * t;
|
|
2671
|
+
return {
|
|
2672
|
+
x: hitX,
|
|
2673
|
+
y: hitY,
|
|
2674
|
+
angle: Math.atan2(hitY - cy, hitX - cx)
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Compute SVG arc flags from start/end angles and arc direction.
|
|
2679
|
+
* Y-down coordinate system: atan2 angle increasing = screen clockwise.
|
|
2680
|
+
*/
|
|
2681
|
+
function computeArcFlags(startAngle, endAngle, arcDir) {
|
|
2682
|
+
const sweepFlag = arcDir === 2 ? 1 : 0;
|
|
2683
|
+
let span;
|
|
2684
|
+
if (sweepFlag === 1) {
|
|
2685
|
+
span = endAngle - startAngle;
|
|
2686
|
+
if (span <= 0) span += 2 * Math.PI;
|
|
2687
|
+
} else {
|
|
2688
|
+
span = startAngle - endAngle;
|
|
2689
|
+
if (span <= 0) span += 2 * Math.PI;
|
|
2690
|
+
}
|
|
2691
|
+
return {
|
|
2692
|
+
largeArc: span > Math.PI ? 1 : 0,
|
|
2693
|
+
sweepFlag
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Build SVG path data for Arc/Chord/Pie shapes.
|
|
2698
|
+
* Returns null for degenerate (zero-size) cases.
|
|
2699
|
+
*/
|
|
2700
|
+
function buildArcPath(left, top, right, bottom, startPtX, startPtY, endPtX, endPtY, arcDir, kind) {
|
|
2701
|
+
const rx = (right - left) / 2;
|
|
2702
|
+
const ry = (bottom - top) / 2;
|
|
2703
|
+
if (rx <= 0 || ry <= 0) return null;
|
|
2704
|
+
const cx = left + rx;
|
|
2705
|
+
const cy = top + ry;
|
|
2706
|
+
const start = intersectEllipseRay(cx, cy, rx, ry, startPtX, startPtY);
|
|
2707
|
+
const end = intersectEllipseRay(cx, cy, rx, ry, endPtX, endPtY);
|
|
2708
|
+
const { largeArc, sweepFlag } = computeArcFlags(start.angle, end.angle, arcDir);
|
|
2709
|
+
let d;
|
|
2710
|
+
if (kind === "pie") d = `M${fmtN(cx)},${fmtN(cy)}L${fmtN(start.x)},${fmtN(start.y)}A${fmtN(rx)},${fmtN(ry)},0,${largeArc},${sweepFlag},${fmtN(end.x)},${fmtN(end.y)}Z`;
|
|
2711
|
+
else if (kind === "chord") d = `M${fmtN(start.x)},${fmtN(start.y)}A${fmtN(rx)},${fmtN(ry)},0,${largeArc},${sweepFlag},${fmtN(end.x)},${fmtN(end.y)}Z`;
|
|
2712
|
+
else d = `M${fmtN(start.x)},${fmtN(start.y)}A${fmtN(rx)},${fmtN(ry)},0,${largeArc},${sweepFlag},${fmtN(end.x)},${fmtN(end.y)}`;
|
|
2713
|
+
return {
|
|
2714
|
+
d,
|
|
2715
|
+
startX: start.x,
|
|
2716
|
+
startY: start.y,
|
|
2717
|
+
endX: end.x,
|
|
2718
|
+
endY: end.y,
|
|
2719
|
+
rx,
|
|
2720
|
+
ry,
|
|
2721
|
+
largeArc,
|
|
2722
|
+
sweep: sweepFlag
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
/** Add arc geometry to a PathBuilder (for path recording mode). */
|
|
2726
|
+
function addArcToPathBuilder(pb, arc, _arcDir, kind) {
|
|
2727
|
+
pb.moveTo(arc.startX, arc.startY);
|
|
2728
|
+
pb.arcTo(arc.rx, arc.ry, 0, arc.largeArc, arc.sweep, arc.endX, arc.endY);
|
|
2729
|
+
if (kind === "chord" || kind === "pie") pb.closePath();
|
|
2730
|
+
}
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/wmf-parser.ts
|
|
2733
|
+
const PLACEABLE_KEY = 2596720087;
|
|
2734
|
+
const META_EOF = 0;
|
|
2735
|
+
const META_SETTEXTCOLOR = 521;
|
|
2736
|
+
const META_SETWINDOWORG = 523;
|
|
2737
|
+
const META_SETWINDOWEXT = 524;
|
|
2738
|
+
const META_MOVETO = 532;
|
|
2739
|
+
const META_LINETO = 531;
|
|
2740
|
+
const META_POLYGON = 804;
|
|
2741
|
+
const META_POLYLINE = 805;
|
|
2742
|
+
const META_ELLIPSE = 1048;
|
|
2743
|
+
const META_RECTANGLE = 1051;
|
|
2744
|
+
const META_SELECTOBJECT = 301;
|
|
2745
|
+
const META_SETTEXTALIGN = 302;
|
|
2746
|
+
const META_ESCAPE = 1574;
|
|
2747
|
+
const META_TEXTOUT = 1313;
|
|
2748
|
+
const META_EXTTEXTOUT = 2610;
|
|
2749
|
+
const META_ROUNDRECT = 1564;
|
|
2750
|
+
const META_PATBLT = 1565;
|
|
2751
|
+
const META_POLYPOLYGON = 1336;
|
|
2752
|
+
const META_CREATEPENINDIRECT = 762;
|
|
2753
|
+
const META_CREATEFONTINDIRECT = 763;
|
|
2754
|
+
const META_CREATEBRUSHINDIRECT = 764;
|
|
2755
|
+
const META_DELETEOBJECT = 496;
|
|
2756
|
+
const ROP_PATCOPY = 15728673;
|
|
2757
|
+
const TA_UPDATECP = 1;
|
|
2758
|
+
var WmfReader = class {
|
|
2759
|
+
constructor(buffer) {
|
|
2760
|
+
this.pos = 0;
|
|
2761
|
+
this.view = buffer instanceof Uint8Array ? new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) : new DataView(buffer);
|
|
2762
|
+
}
|
|
2763
|
+
get length() {
|
|
2764
|
+
return this.view.byteLength;
|
|
2765
|
+
}
|
|
2766
|
+
get remaining() {
|
|
2767
|
+
return this.length - this.pos;
|
|
2768
|
+
}
|
|
2769
|
+
seek(pos) {
|
|
2770
|
+
if (pos < 0 || pos > this.length) throw new Error("Invalid WMF seek");
|
|
2771
|
+
this.pos = pos;
|
|
2772
|
+
}
|
|
2773
|
+
readUint16() {
|
|
2774
|
+
const value = this.view.getUint16(this.pos, true);
|
|
2775
|
+
this.pos += 2;
|
|
2776
|
+
return value;
|
|
2777
|
+
}
|
|
2778
|
+
readInt16() {
|
|
2779
|
+
const value = this.view.getInt16(this.pos, true);
|
|
2780
|
+
this.pos += 2;
|
|
2781
|
+
return value;
|
|
2782
|
+
}
|
|
2783
|
+
readUint32() {
|
|
2784
|
+
const value = this.view.getUint32(this.pos, true);
|
|
2785
|
+
this.pos += 4;
|
|
2786
|
+
return value;
|
|
2787
|
+
}
|
|
2788
|
+
uint32At(pos) {
|
|
2789
|
+
return pos + 4 <= this.length ? this.view.getUint32(pos, true) : 0;
|
|
2790
|
+
}
|
|
2791
|
+
bytesAt(start, end) {
|
|
2792
|
+
const safeStart = Math.max(0, Math.min(start, this.length));
|
|
2793
|
+
const safeEnd = Math.max(safeStart, Math.min(end, this.length));
|
|
2794
|
+
const bytes = new Uint8Array(safeEnd - safeStart);
|
|
2795
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = this.view.getUint8(safeStart + i);
|
|
2796
|
+
return bytes;
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
function isWmf$1(buffer) {
|
|
2800
|
+
const reader = new WmfReader(buffer);
|
|
2801
|
+
if (reader.length < 18) return false;
|
|
2802
|
+
const start = reader.uint32At(0) === PLACEABLE_KEY ? 22 : 0;
|
|
2803
|
+
if (reader.length < start + 18) return false;
|
|
2804
|
+
reader.seek(start);
|
|
2805
|
+
const fileType = reader.readUint16();
|
|
2806
|
+
const headerSizeWords = reader.readUint16();
|
|
2807
|
+
const version = reader.readUint16();
|
|
2808
|
+
return (fileType === 1 || fileType === 2) && headerSizeWords === 9 && version >= 256;
|
|
2809
|
+
}
|
|
2810
|
+
function wmf2svg$1(buffer) {
|
|
2811
|
+
const reader = new WmfReader(buffer);
|
|
2812
|
+
if (!isWmf$1(buffer)) throw new Error("Invalid WMF file");
|
|
2813
|
+
const hasPlaceable = reader.uint32At(0) === PLACEABLE_KEY;
|
|
2814
|
+
const placeable = hasPlaceable ? readPlaceableHeader(reader) : void 0;
|
|
2815
|
+
const headerStart = hasPlaceable ? 22 : 0;
|
|
2816
|
+
reader.seek(headerStart);
|
|
2817
|
+
reader.seek(headerStart + 18);
|
|
2818
|
+
const state = {
|
|
2819
|
+
windowX: placeable?.left ?? 0,
|
|
2820
|
+
windowY: placeable?.top ?? 0,
|
|
2821
|
+
windowW: Math.max((placeable?.right ?? 1e3) - (placeable?.left ?? 0), 1),
|
|
2822
|
+
windowH: Math.max((placeable?.bottom ?? 1e3) - (placeable?.top ?? 0), 1),
|
|
2823
|
+
canvasX: placeable?.left ?? 0,
|
|
2824
|
+
canvasY: placeable?.top ?? 0,
|
|
2825
|
+
canvasW: Math.max((placeable?.right ?? 1e3) - (placeable?.left ?? 0), 1),
|
|
2826
|
+
canvasH: Math.max((placeable?.bottom ?? 1e3) - (placeable?.top ?? 0), 1),
|
|
2827
|
+
drawingStarted: false,
|
|
2828
|
+
currentX: 0,
|
|
2829
|
+
currentY: 0,
|
|
2830
|
+
pen: {
|
|
2831
|
+
type: "pen",
|
|
2832
|
+
style: PenStyle.PS_SOLID,
|
|
2833
|
+
color: {
|
|
2834
|
+
r: 0,
|
|
2835
|
+
g: 0,
|
|
2836
|
+
b: 0,
|
|
2837
|
+
a: 255
|
|
2838
|
+
},
|
|
2839
|
+
width: 1
|
|
2840
|
+
},
|
|
2841
|
+
brush: {
|
|
2842
|
+
type: "brush",
|
|
2843
|
+
style: 0,
|
|
2844
|
+
color: {
|
|
2845
|
+
r: 255,
|
|
2846
|
+
g: 255,
|
|
2847
|
+
b: 255,
|
|
2848
|
+
a: 255
|
|
2849
|
+
}
|
|
2850
|
+
},
|
|
2851
|
+
font: {
|
|
2852
|
+
type: "font",
|
|
2853
|
+
style: 0,
|
|
2854
|
+
color: {
|
|
2855
|
+
r: 0,
|
|
2856
|
+
g: 0,
|
|
2857
|
+
b: 0,
|
|
2858
|
+
a: 255
|
|
2859
|
+
}
|
|
2860
|
+
},
|
|
2861
|
+
textColor: {
|
|
2862
|
+
r: 0,
|
|
2863
|
+
g: 0,
|
|
2864
|
+
b: 0,
|
|
2865
|
+
a: 255
|
|
2866
|
+
},
|
|
2867
|
+
textAlignUpdateCP: false,
|
|
2868
|
+
textAlignBaseline: false,
|
|
2869
|
+
mathTypeLeftInset: 0,
|
|
2870
|
+
objects: [],
|
|
2871
|
+
parts: []
|
|
2872
|
+
};
|
|
2873
|
+
while (reader.remaining >= 6) {
|
|
2874
|
+
const recordStart = reader.pos;
|
|
2875
|
+
const sizeWords = reader.readUint32();
|
|
2876
|
+
const fn = reader.readUint16();
|
|
2877
|
+
const recordBytes = sizeWords * 2;
|
|
2878
|
+
if (recordBytes < 6 || recordStart + recordBytes > reader.length) break;
|
|
2879
|
+
const record = reader.bytesAt(recordStart, recordStart + recordBytes);
|
|
2880
|
+
processRecord(fn, readParams(reader, recordStart + recordBytes), state, record);
|
|
2881
|
+
reader.seek(recordStart + recordBytes);
|
|
2882
|
+
if (fn === META_EOF) break;
|
|
2883
|
+
}
|
|
2884
|
+
const viewTop = Math.min(state.canvasY, state.textMinY ?? state.canvasY);
|
|
2885
|
+
const viewBottom = state.canvasY + state.canvasH;
|
|
2886
|
+
const viewX = state.canvasX - state.mathTypeLeftInset;
|
|
2887
|
+
const viewW = state.canvasW + state.mathTypeLeftInset;
|
|
2888
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${fmt(viewX)} ${fmt(viewTop)} ${fmt(viewW)} ${fmt(viewBottom - viewTop)}">${state.parts.join("")}</svg>`;
|
|
2889
|
+
}
|
|
2890
|
+
function readPlaceableHeader(reader) {
|
|
2891
|
+
reader.seek(6);
|
|
2892
|
+
return {
|
|
2893
|
+
left: reader.readInt16(),
|
|
2894
|
+
top: reader.readInt16(),
|
|
2895
|
+
right: reader.readInt16(),
|
|
2896
|
+
bottom: reader.readInt16()
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
function readParams(reader, end) {
|
|
2900
|
+
const params = [];
|
|
2901
|
+
while (reader.pos + 2 <= end) params.push(reader.readInt16());
|
|
2902
|
+
return params;
|
|
2903
|
+
}
|
|
2904
|
+
function processRecord(fn, p, state, record) {
|
|
2905
|
+
switch (fn) {
|
|
2906
|
+
case META_SETTEXTCOLOR:
|
|
2907
|
+
if (p.length >= 2) state.textColor = colorRef(p[0], p[1]);
|
|
2908
|
+
break;
|
|
2909
|
+
case META_SETWINDOWORG:
|
|
2910
|
+
if (p.length >= 2) {
|
|
2911
|
+
state.windowX = p[1];
|
|
2912
|
+
state.windowY = p[0];
|
|
2913
|
+
syncCanvasWindow(state);
|
|
2914
|
+
}
|
|
2915
|
+
break;
|
|
2916
|
+
case META_SETWINDOWEXT:
|
|
2917
|
+
if (p.length >= 2) {
|
|
2918
|
+
state.windowW = Math.max(Math.abs(p[1]), 1);
|
|
2919
|
+
state.windowH = Math.max(Math.abs(p[0]), 1);
|
|
2920
|
+
syncCanvasWindow(state);
|
|
2921
|
+
}
|
|
2922
|
+
break;
|
|
2923
|
+
case META_MOVETO:
|
|
2924
|
+
if (p.length >= 2) {
|
|
2925
|
+
state.currentX = p[1];
|
|
2926
|
+
state.currentY = p[0];
|
|
2927
|
+
}
|
|
2928
|
+
break;
|
|
2929
|
+
case META_SETTEXTALIGN:
|
|
2930
|
+
if (p.length >= 1) {
|
|
2931
|
+
state.textAlignUpdateCP = (p[0] & TA_UPDATECP) === TA_UPDATECP;
|
|
2932
|
+
state.textAlignBaseline = (p[0] & 24) === 24;
|
|
2933
|
+
}
|
|
2934
|
+
break;
|
|
2935
|
+
case META_ESCAPE:
|
|
2936
|
+
readEscapeRecord(p, state);
|
|
2937
|
+
break;
|
|
2938
|
+
case META_LINETO:
|
|
2939
|
+
if (p.length >= 2) {
|
|
2940
|
+
const x = p[1];
|
|
2941
|
+
const y = p[0];
|
|
2942
|
+
beginDrawing(state);
|
|
2943
|
+
state.parts.push(`<path d="M${fmt(transformX(state, state.currentX))} ${fmt(transformY(state, state.currentY))} L${fmt(transformX(state, x))} ${fmt(transformY(state, y))}"${strokeAttr(state.pen, state)} fill="none"/>`);
|
|
2944
|
+
state.currentX = x;
|
|
2945
|
+
state.currentY = y;
|
|
2946
|
+
}
|
|
2947
|
+
break;
|
|
2948
|
+
case META_RECTANGLE:
|
|
2949
|
+
case META_ELLIPSE:
|
|
2950
|
+
if (p.length >= 4) {
|
|
2951
|
+
const { left, top, right, bottom } = rectParams(p);
|
|
2952
|
+
beginDrawing(state);
|
|
2953
|
+
if (fn === META_RECTANGLE) state.parts.push(`<rect x="${fmt(transformX(state, left))}" y="${fmt(transformY(state, top))}" width="${fmt(transformWidth(state, right - left))}" height="${fmt(transformHeight(state, bottom - top))}"${fillAttr(state.brush)}${strokeAttr(state.pen, state)}/>`);
|
|
2954
|
+
else state.parts.push(`<ellipse cx="${fmt(transformX(state, (left + right) / 2))}" cy="${fmt(transformY(state, (top + bottom) / 2))}" rx="${fmt(transformWidth(state, (right - left) / 2))}" ry="${fmt(transformHeight(state, (bottom - top) / 2))}"${fillAttr(state.brush)}${strokeAttr(state.pen, state)}/>`);
|
|
2955
|
+
}
|
|
2956
|
+
break;
|
|
2957
|
+
case META_ROUNDRECT:
|
|
2958
|
+
if (p.length >= 6) {
|
|
2959
|
+
const { left, top, right, bottom } = rectParams(p.slice(2));
|
|
2960
|
+
beginDrawing(state);
|
|
2961
|
+
state.parts.push(`<rect x="${fmt(transformX(state, left))}" y="${fmt(transformY(state, top))}" width="${fmt(transformWidth(state, right - left))}" height="${fmt(transformHeight(state, bottom - top))}" rx="${fmt(transformWidth(state, Math.abs(p[1]) / 2))}" ry="${fmt(transformHeight(state, Math.abs(p[0]) / 2))}"${fillAttr(state.brush)}${strokeAttr(state.pen, state)}/>`);
|
|
2962
|
+
}
|
|
2963
|
+
break;
|
|
2964
|
+
case META_PATBLT:
|
|
2965
|
+
drawPatBlt(p, state);
|
|
2966
|
+
break;
|
|
2967
|
+
case META_POLYGON:
|
|
2968
|
+
case META_POLYLINE:
|
|
2969
|
+
drawPoly(fn, p, state);
|
|
2970
|
+
break;
|
|
2971
|
+
case META_POLYPOLYGON:
|
|
2972
|
+
drawPolyPolygon(p, state);
|
|
2973
|
+
break;
|
|
2974
|
+
case META_TEXTOUT:
|
|
2975
|
+
drawTextOut(record, state);
|
|
2976
|
+
break;
|
|
2977
|
+
case META_EXTTEXTOUT:
|
|
2978
|
+
drawExtTextOut(record, state);
|
|
2979
|
+
break;
|
|
2980
|
+
case META_CREATEPENINDIRECT:
|
|
2981
|
+
if (p.length >= 5) addWmfObject(state, {
|
|
2982
|
+
type: "pen",
|
|
2983
|
+
style: p[0],
|
|
2984
|
+
width: Math.max(Math.abs(p[2]), Math.abs(p[1]), 1),
|
|
2985
|
+
color: colorRef(p[3], p[4])
|
|
2986
|
+
});
|
|
2987
|
+
break;
|
|
2988
|
+
case META_CREATEFONTINDIRECT:
|
|
2989
|
+
addWmfObject(state, readFontObject(record));
|
|
2990
|
+
break;
|
|
2991
|
+
case META_CREATEBRUSHINDIRECT:
|
|
2992
|
+
if (p.length >= 4) addWmfObject(state, {
|
|
2993
|
+
type: "brush",
|
|
2994
|
+
style: p[0],
|
|
2995
|
+
color: colorRef(p[1], p[2])
|
|
2996
|
+
});
|
|
2997
|
+
break;
|
|
2998
|
+
case META_SELECTOBJECT: {
|
|
2999
|
+
const obj = state.objects[p[0]];
|
|
3000
|
+
if (obj?.type === "pen") state.pen = obj;
|
|
3001
|
+
if (obj?.type === "brush") state.brush = obj;
|
|
3002
|
+
if (obj?.type === "font") state.font = obj;
|
|
3003
|
+
break;
|
|
3004
|
+
}
|
|
3005
|
+
case META_DELETEOBJECT: state.objects[p[0]] = void 0;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
function syncCanvasWindow(state) {
|
|
3009
|
+
if (state.drawingStarted) return;
|
|
3010
|
+
state.canvasX = state.windowX;
|
|
3011
|
+
state.canvasY = state.windowY;
|
|
3012
|
+
state.canvasW = state.windowW;
|
|
3013
|
+
state.canvasH = state.windowH;
|
|
3014
|
+
}
|
|
3015
|
+
function beginDrawing(state) {
|
|
3016
|
+
state.drawingStarted = true;
|
|
3017
|
+
}
|
|
3018
|
+
function transformX(state, x) {
|
|
3019
|
+
const transform = state.objectTransform;
|
|
3020
|
+
return transform ? transform.dst.left + (x - transform.src.left) * transform.scaleX : x;
|
|
3021
|
+
}
|
|
3022
|
+
function transformY(state, y) {
|
|
3023
|
+
const transform = state.objectTransform;
|
|
3024
|
+
return transform ? transform.dst.top + (y - transform.src.top) * transform.scaleY : y;
|
|
3025
|
+
}
|
|
3026
|
+
function transformWidth(state, width) {
|
|
3027
|
+
return Math.abs(width * (state.objectTransform?.scaleX ?? 1));
|
|
3028
|
+
}
|
|
3029
|
+
function transformHeight(state, height) {
|
|
3030
|
+
return Math.abs(height * (state.objectTransform?.scaleY ?? 1));
|
|
3031
|
+
}
|
|
3032
|
+
function transformScale(state) {
|
|
3033
|
+
const transform = state.objectTransform;
|
|
3034
|
+
return transform ? (Math.abs(transform.scaleX) + Math.abs(transform.scaleY)) / 2 : 1;
|
|
3035
|
+
}
|
|
3036
|
+
function transformFontSize(state, fontSize) {
|
|
3037
|
+
return fontSize * transformScale(state);
|
|
3038
|
+
}
|
|
3039
|
+
function includeTextVerticalBounds(state, y, fontSize) {
|
|
3040
|
+
const top = transformY(state, y) - transformFontSize(state, fontSize);
|
|
3041
|
+
const bottom = transformY(state, y) + transformFontSize(state, fontSize) * .25;
|
|
3042
|
+
state.textMinY = state.textMinY === void 0 ? top : Math.min(state.textMinY, top);
|
|
3043
|
+
state.textMaxY = state.textMaxY === void 0 ? bottom : Math.max(state.textMaxY, bottom);
|
|
3044
|
+
}
|
|
3045
|
+
function readEscapeRecord(p, state) {
|
|
3046
|
+
if (isOfficeObjectEndEscape(p)) {
|
|
3047
|
+
state.objectTransform = void 0;
|
|
3048
|
+
state.pendingObjectDest = void 0;
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
if (p.length < 8) return;
|
|
3052
|
+
const marker = wordsToAscii(p.slice(2, 6));
|
|
3053
|
+
if (p[0] === 15 && marker === "MathType" && !state.objectTransform) {
|
|
3054
|
+
state.mathTypeLeftInset = Math.max(state.mathTypeLeftInset, p[7] ?? 0);
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
if (isOfficeObjectDestEscape(p)) {
|
|
3058
|
+
state.pendingObjectDest = readOfficeEscapeRect(p, 5);
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
if (isOfficeObjectSourceEscape(p) && state.pendingObjectDest) {
|
|
3062
|
+
const src = readOfficeEscapeRect(p, 7);
|
|
3063
|
+
const dst = state.pendingObjectDest;
|
|
3064
|
+
const srcW = src.right - src.left;
|
|
3065
|
+
const srcH = src.bottom - src.top;
|
|
3066
|
+
if (srcW !== 0 && srcH !== 0) state.objectTransform = {
|
|
3067
|
+
src,
|
|
3068
|
+
dst,
|
|
3069
|
+
scaleX: (dst.right - dst.left) / srcW,
|
|
3070
|
+
scaleY: (dst.bottom - dst.top) / srcH
|
|
3071
|
+
};
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function isOfficeObjectDestEscape(p) {
|
|
3076
|
+
return p.length >= 13 && p[0] === 15 && p[1] === 22 && p[2] === -1 && p[3] === -1 && p[4] === 0;
|
|
3077
|
+
}
|
|
3078
|
+
function isOfficeObjectSourceEscape(p) {
|
|
3079
|
+
return p.length >= 15 && p[0] === 15 && p[1] === 26 && p[2] === -1 && p[3] === -1 && p[4] === 0;
|
|
3080
|
+
}
|
|
3081
|
+
function isOfficeObjectEndEscape(p) {
|
|
3082
|
+
return p[0] === 15 && (p[1] === 10 || p[1] === 6) && p[2] === -1 && p[3] === -1 && p[4] === 1;
|
|
3083
|
+
}
|
|
3084
|
+
function readOfficeEscapeRect(p, start) {
|
|
3085
|
+
return {
|
|
3086
|
+
left: signedLongFromWords(p[start], p[start + 1]),
|
|
3087
|
+
top: signedLongFromWords(p[start + 2], p[start + 3]),
|
|
3088
|
+
right: signedLongFromWords(p[start + 4], p[start + 5]),
|
|
3089
|
+
bottom: signedLongFromWords(p[start + 6], p[start + 7])
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
function signedLongFromWords(lowWord, highWord) {
|
|
3093
|
+
const value = lowWord & 65535 | (highWord & 65535) << 16;
|
|
3094
|
+
return value >= 2147483648 ? value - 4294967296 : value;
|
|
3095
|
+
}
|
|
3096
|
+
function wordsToAscii(words) {
|
|
3097
|
+
let value = "";
|
|
3098
|
+
for (const word of words) {
|
|
3099
|
+
value += String.fromCharCode(word & 255);
|
|
3100
|
+
const high = word >> 8 & 255;
|
|
3101
|
+
if (high) value += String.fromCharCode(high);
|
|
3102
|
+
}
|
|
3103
|
+
return value;
|
|
3104
|
+
}
|
|
3105
|
+
function addWmfObject(state, object) {
|
|
3106
|
+
const index = state.objects.findIndex((entry) => entry === void 0);
|
|
3107
|
+
if (index >= 0) state.objects[index] = object;
|
|
3108
|
+
else state.objects.push(object);
|
|
3109
|
+
}
|
|
3110
|
+
function rectParams(p) {
|
|
3111
|
+
const bottom = p[0];
|
|
3112
|
+
const right = p[1];
|
|
3113
|
+
const top = p[2];
|
|
3114
|
+
const left = p[3];
|
|
3115
|
+
return {
|
|
3116
|
+
left: Math.min(left, right),
|
|
3117
|
+
top: Math.min(top, bottom),
|
|
3118
|
+
right: Math.max(left, right),
|
|
3119
|
+
bottom: Math.max(top, bottom)
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
function drawPoly(fn, p, state) {
|
|
3123
|
+
const count = p[0] ?? 0;
|
|
3124
|
+
if (count <= 0 || p.length < 1 + count * 2) return;
|
|
3125
|
+
const points = [];
|
|
3126
|
+
for (let i = 0; i < count; i++) points.push(`${fmt(transformX(state, p[1 + i * 2]))} ${fmt(transformY(state, p[2 + i * 2]))}`);
|
|
3127
|
+
if (fn === META_POLYGON) {
|
|
3128
|
+
beginDrawing(state);
|
|
3129
|
+
state.parts.push(`<polygon points="${points.join(" ")}"${fillAttr(state.brush)}${strokeAttr(state.pen, state)}/>`);
|
|
3130
|
+
} else {
|
|
3131
|
+
beginDrawing(state);
|
|
3132
|
+
state.parts.push(`<polyline points="${points.join(" ")}"${strokeAttr(state.pen, state)} fill="none"/>`);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
function drawPatBlt(p, state) {
|
|
3136
|
+
if (p.length < 6) return;
|
|
3137
|
+
if (unsignedLongFromWords(p[0], p[1]) !== ROP_PATCOPY) return;
|
|
3138
|
+
const height = Math.abs(p[2]);
|
|
3139
|
+
const width = Math.abs(p[3]);
|
|
3140
|
+
const y = p[4];
|
|
3141
|
+
const x = p[5];
|
|
3142
|
+
if (width === 0 || height === 0) return;
|
|
3143
|
+
beginDrawing(state);
|
|
3144
|
+
state.parts.push(`<rect x="${fmt(transformX(state, x))}" y="${fmt(transformY(state, y))}" width="${fmt(transformWidth(state, width))}" height="${fmt(transformHeight(state, height))}"${fillAttr(state.brush)}/>`);
|
|
3145
|
+
}
|
|
3146
|
+
function unsignedLongFromWords(lowWord, highWord) {
|
|
3147
|
+
return (lowWord & 65535 | (highWord & 65535) << 16) >>> 0;
|
|
3148
|
+
}
|
|
3149
|
+
function drawPolyPolygon(p, state) {
|
|
3150
|
+
const polygonCount = p[0] ?? 0;
|
|
3151
|
+
if (polygonCount <= 0 || p.length < 1 + polygonCount) return;
|
|
3152
|
+
let offset = 1 + polygonCount;
|
|
3153
|
+
for (let polygonIndex = 0; polygonIndex < polygonCount; polygonIndex++) {
|
|
3154
|
+
const pointCount = p[1 + polygonIndex] ?? 0;
|
|
3155
|
+
if (pointCount <= 0 || p.length < offset + pointCount * 2) return;
|
|
3156
|
+
const points = [];
|
|
3157
|
+
for (let i = 0; i < pointCount; i++) points.push(`${fmt(transformX(state, p[offset + i * 2]))} ${fmt(transformY(state, p[offset + i * 2 + 1]))}`);
|
|
3158
|
+
beginDrawing(state);
|
|
3159
|
+
state.parts.push(`<polygon points="${points.join(" ")}"${fillAttr(state.brush)}${strokeAttr(state.pen, state)}/>`);
|
|
3160
|
+
offset += pointCount * 2;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
function drawTextOut(record, state) {
|
|
3164
|
+
if (record.length < 12) return;
|
|
3165
|
+
const length = int16At(record, 6);
|
|
3166
|
+
if (length <= 0) return;
|
|
3167
|
+
const textStart = 8;
|
|
3168
|
+
const textEnd = Math.min(textStart + length, record.length);
|
|
3169
|
+
const coordOffset = textStart + length + length % 2;
|
|
3170
|
+
if (coordOffset + 4 > record.length) return;
|
|
3171
|
+
const y = int16At(record, coordOffset);
|
|
3172
|
+
const point = resolveTextPoint(state, int16At(record, coordOffset + 2), y);
|
|
3173
|
+
const textInfo = decodeWmfText(record.subarray(textStart, textEnd), state.font);
|
|
3174
|
+
drawText(state, point.x, point.y, textInfo.text);
|
|
3175
|
+
updateTextCurrentPosition(state, point.x, point.y, textInfo.text);
|
|
3176
|
+
}
|
|
3177
|
+
function drawExtTextOut(record, state) {
|
|
3178
|
+
if (record.length < 14) return;
|
|
3179
|
+
const y = int16At(record, 6);
|
|
3180
|
+
const x = int16At(record, 8);
|
|
3181
|
+
const length = int16At(record, 10);
|
|
3182
|
+
const opts = uint16At(record, 12);
|
|
3183
|
+
if (length <= 0) return;
|
|
3184
|
+
const textStart = 14 + (opts & 6 ? 8 : 0);
|
|
3185
|
+
const textEnd = Math.min(textStart + length, record.length);
|
|
3186
|
+
if (textStart >= record.length) return;
|
|
3187
|
+
const point = resolveTextPoint(state, x, y);
|
|
3188
|
+
const textInfo = decodeWmfText(record.subarray(textStart, textEnd), state.font);
|
|
3189
|
+
const dx = readTextDx(record, textStart + length + length % 2, textInfo);
|
|
3190
|
+
drawText(state, point.x, point.y, textInfo.text, dx);
|
|
3191
|
+
updateTextCurrentPosition(state, point.x, point.y, textInfo.text, dx);
|
|
3192
|
+
}
|
|
3193
|
+
function resolveTextPoint(state, x, y) {
|
|
3194
|
+
if (state.textAlignUpdateCP && x === 0 && y === 0) return {
|
|
3195
|
+
x: state.currentX,
|
|
3196
|
+
y: state.currentY
|
|
3197
|
+
};
|
|
3198
|
+
return {
|
|
3199
|
+
x,
|
|
3200
|
+
y
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
function readTextDx(record, offset, textInfo) {
|
|
3204
|
+
const charCount = textInfo.byteLengths.length;
|
|
3205
|
+
if (charCount <= 0) return void 0;
|
|
3206
|
+
const byteCount = textInfo.byteLengths.reduce((sum, length) => sum + length, 0);
|
|
3207
|
+
if (byteCount > charCount && offset + byteCount * 2 <= record.length) {
|
|
3208
|
+
const rawDx = [];
|
|
3209
|
+
for (let i = 0; i < byteCount; i++) rawDx.push(uint16At(record, offset + i * 2));
|
|
3210
|
+
const dx = [];
|
|
3211
|
+
let rawIndex = 0;
|
|
3212
|
+
for (const byteLength of textInfo.byteLengths) {
|
|
3213
|
+
let advance = 0;
|
|
3214
|
+
for (let i = 0; i < byteLength; i++) advance += rawDx[rawIndex++] ?? 0;
|
|
3215
|
+
dx.push(advance);
|
|
3216
|
+
}
|
|
3217
|
+
return dx;
|
|
3218
|
+
}
|
|
3219
|
+
if (offset + charCount * 2 > record.length) return void 0;
|
|
3220
|
+
const dx = [];
|
|
3221
|
+
for (let i = 0; i < charCount; i++) dx.push(uint16At(record, offset + i * 2));
|
|
3222
|
+
return dx;
|
|
3223
|
+
}
|
|
3224
|
+
function updateTextCurrentPosition(state, x, y, value, dx) {
|
|
3225
|
+
if (!state.textAlignUpdateCP) return;
|
|
3226
|
+
const chars = Array.from(value);
|
|
3227
|
+
state.currentX = x + (dx?.length ? dx.slice(0, chars.length).reduce((sum, item) => sum + item, 0) : estimateWmfTextWidth(state, chars));
|
|
3228
|
+
state.currentY = y;
|
|
3229
|
+
}
|
|
3230
|
+
function estimateWmfTextWidth(state, chars) {
|
|
3231
|
+
const fontSize = state.font.height || Math.max(Math.min(state.windowH, state.windowW) * .04, 1);
|
|
3232
|
+
return chars.reduce((sum, char) => sum + (char.codePointAt(0) > 255 ? fontSize : fontSize * .62), 0);
|
|
3233
|
+
}
|
|
3234
|
+
function drawText(state, x, y, value, dx) {
|
|
3235
|
+
const text = escapeXml(value);
|
|
3236
|
+
if (!text) return;
|
|
3237
|
+
const fontSize = state.font.height || Math.max(Math.min(state.windowH, state.windowW) * .04, 1);
|
|
3238
|
+
const fontAttrs = textFontAttrs(state.font);
|
|
3239
|
+
const baselineAttrs = textBaselineAttrs(state);
|
|
3240
|
+
const chars = Array.from(value);
|
|
3241
|
+
const vectorizeMathDelimiters = isSymbolFont(state.font);
|
|
3242
|
+
if (dx && chars.length > 1 || vectorizeMathDelimiters && chars.some(isVectorMathDelimiterPiece)) {
|
|
3243
|
+
let cursorX = x;
|
|
3244
|
+
for (let i = 0; i < chars.length; i++) {
|
|
3245
|
+
drawTextChar(state, cursorX, y, chars[i], fontSize, fontAttrs, vectorizeMathDelimiters);
|
|
3246
|
+
cursorX += dx?.[i] ?? estimateWmfTextWidth(state, [chars[i]]);
|
|
3247
|
+
}
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
beginDrawing(state);
|
|
3251
|
+
includeTextVerticalBounds(state, y, fontSize);
|
|
3252
|
+
state.parts.push(`<text x="${fmt(transformX(state, x))}" y="${fmt(transformY(state, y))}" fill="${color(state.textColor)}" font-size="${fmt(transformFontSize(state, fontSize))}"${fontAttrs}${baselineAttrs}>${text}</text>`);
|
|
3253
|
+
}
|
|
3254
|
+
function drawTextChar(state, x, y, char, fontSize, fontAttrs, vectorizeMathDelimiters) {
|
|
3255
|
+
if (vectorizeMathDelimiters && drawVectorMathDelimiterPiece(state, x, y, char, fontSize)) return;
|
|
3256
|
+
const text = escapeXml(char);
|
|
3257
|
+
if (!text) return;
|
|
3258
|
+
beginDrawing(state);
|
|
3259
|
+
includeTextVerticalBounds(state, y, fontSize);
|
|
3260
|
+
state.parts.push(`<text x="${fmt(transformX(state, x))}" y="${fmt(transformY(state, y))}" fill="${color(state.textColor)}" font-size="${fmt(transformFontSize(state, fontSize))}"${fontAttrs}${textBaselineAttrs(state)}>${text}</text>`);
|
|
3261
|
+
}
|
|
3262
|
+
function isVectorMathDelimiterPiece(char) {
|
|
3263
|
+
return "⎛⎜⎝⎞⎟⎠⎡⎢⎣⎤⎥⎦⎧⎨⎩⎪⎫⎬⎭".includes(char);
|
|
3264
|
+
}
|
|
3265
|
+
function drawVectorMathDelimiterPiece(state, x, y, char, fontSize) {
|
|
3266
|
+
if (!isVectorMathDelimiterPiece(char)) return false;
|
|
3267
|
+
const transformedX = transformX(state, x);
|
|
3268
|
+
const transformedY = transformY(state, y);
|
|
3269
|
+
const transformedFontSize = transformFontSize(state, fontSize);
|
|
3270
|
+
const top = transformedY - transformedFontSize * .84;
|
|
3271
|
+
const bottom = transformedY + transformedFontSize * .12;
|
|
3272
|
+
const mid = (top + bottom) / 2;
|
|
3273
|
+
const width = transformedFontSize * (char === "⎧" || char === "⎨" || char === "⎩" || char === "⎫" || char === "⎬" || char === "⎭" ? .34 : .28);
|
|
3274
|
+
const strokeWidth = Math.max(transformedFontSize * .052, 1);
|
|
3275
|
+
const path = mathDelimiterPiecePath(char, transformedX, top, mid, bottom, width);
|
|
3276
|
+
if (!path) return false;
|
|
3277
|
+
beginDrawing(state);
|
|
3278
|
+
state.textMinY = state.textMinY === void 0 ? top : Math.min(state.textMinY, top);
|
|
3279
|
+
state.textMaxY = state.textMaxY === void 0 ? bottom : Math.max(state.textMaxY, bottom);
|
|
3280
|
+
state.parts.push(`<path d="${path}" fill="none" stroke="${color(state.textColor)}" stroke-width="${fmt(strokeWidth)}" stroke-linecap="round" stroke-linejoin="round"/>`);
|
|
3281
|
+
return true;
|
|
3282
|
+
}
|
|
3283
|
+
function mathDelimiterPiecePath(char, x, top, mid, bottom, width) {
|
|
3284
|
+
const left = x;
|
|
3285
|
+
const right = x + width;
|
|
3286
|
+
const nearLeft = x + width * .08;
|
|
3287
|
+
const nearRight = x + width * .92;
|
|
3288
|
+
const center = x + width * .5;
|
|
3289
|
+
switch (char) {
|
|
3290
|
+
case "⎛": return pathData([
|
|
3291
|
+
"M",
|
|
3292
|
+
right,
|
|
3293
|
+
top,
|
|
3294
|
+
"C",
|
|
3295
|
+
center,
|
|
3296
|
+
top,
|
|
3297
|
+
nearLeft,
|
|
3298
|
+
mid,
|
|
3299
|
+
nearLeft,
|
|
3300
|
+
bottom
|
|
3301
|
+
]);
|
|
3302
|
+
case "⎜": return pathData([
|
|
3303
|
+
"M",
|
|
3304
|
+
nearLeft,
|
|
3305
|
+
top,
|
|
3306
|
+
"L",
|
|
3307
|
+
nearLeft,
|
|
3308
|
+
bottom
|
|
3309
|
+
]);
|
|
3310
|
+
case "⎝": return pathData([
|
|
3311
|
+
"M",
|
|
3312
|
+
nearLeft,
|
|
3313
|
+
top,
|
|
3314
|
+
"C",
|
|
3315
|
+
nearLeft,
|
|
3316
|
+
mid,
|
|
3317
|
+
center,
|
|
3318
|
+
bottom,
|
|
3319
|
+
right,
|
|
3320
|
+
bottom
|
|
3321
|
+
]);
|
|
3322
|
+
case "⎞": return pathData([
|
|
3323
|
+
"M",
|
|
3324
|
+
left,
|
|
3325
|
+
top,
|
|
3326
|
+
"C",
|
|
3327
|
+
center,
|
|
3328
|
+
top,
|
|
3329
|
+
nearRight,
|
|
3330
|
+
mid,
|
|
3331
|
+
nearRight,
|
|
3332
|
+
bottom
|
|
3333
|
+
]);
|
|
3334
|
+
case "⎟": return pathData([
|
|
3335
|
+
"M",
|
|
3336
|
+
nearRight,
|
|
3337
|
+
top,
|
|
3338
|
+
"L",
|
|
3339
|
+
nearRight,
|
|
3340
|
+
bottom
|
|
3341
|
+
]);
|
|
3342
|
+
case "⎠": return pathData([
|
|
3343
|
+
"M",
|
|
3344
|
+
nearRight,
|
|
3345
|
+
top,
|
|
3346
|
+
"C",
|
|
3347
|
+
nearRight,
|
|
3348
|
+
mid,
|
|
3349
|
+
center,
|
|
3350
|
+
bottom,
|
|
3351
|
+
left,
|
|
3352
|
+
bottom
|
|
3353
|
+
]);
|
|
3354
|
+
case "⎡": return pathData([
|
|
3355
|
+
"M",
|
|
3356
|
+
right,
|
|
3357
|
+
top,
|
|
3358
|
+
"L",
|
|
3359
|
+
nearLeft,
|
|
3360
|
+
top,
|
|
3361
|
+
"L",
|
|
3362
|
+
nearLeft,
|
|
3363
|
+
bottom
|
|
3364
|
+
]);
|
|
3365
|
+
case "⎢": return pathData([
|
|
3366
|
+
"M",
|
|
3367
|
+
nearLeft,
|
|
3368
|
+
top,
|
|
3369
|
+
"L",
|
|
3370
|
+
nearLeft,
|
|
3371
|
+
bottom
|
|
3372
|
+
]);
|
|
3373
|
+
case "⎣": return pathData([
|
|
3374
|
+
"M",
|
|
3375
|
+
nearLeft,
|
|
3376
|
+
top,
|
|
3377
|
+
"L",
|
|
3378
|
+
nearLeft,
|
|
3379
|
+
bottom,
|
|
3380
|
+
"L",
|
|
3381
|
+
right,
|
|
3382
|
+
bottom
|
|
3383
|
+
]);
|
|
3384
|
+
case "⎤": return pathData([
|
|
3385
|
+
"M",
|
|
3386
|
+
left,
|
|
3387
|
+
top,
|
|
3388
|
+
"L",
|
|
3389
|
+
nearRight,
|
|
3390
|
+
top,
|
|
3391
|
+
"L",
|
|
3392
|
+
nearRight,
|
|
3393
|
+
bottom
|
|
3394
|
+
]);
|
|
3395
|
+
case "⎥": return pathData([
|
|
3396
|
+
"M",
|
|
3397
|
+
nearRight,
|
|
3398
|
+
top,
|
|
3399
|
+
"L",
|
|
3400
|
+
nearRight,
|
|
3401
|
+
bottom
|
|
3402
|
+
]);
|
|
3403
|
+
case "⎦": return pathData([
|
|
3404
|
+
"M",
|
|
3405
|
+
nearRight,
|
|
3406
|
+
top,
|
|
3407
|
+
"L",
|
|
3408
|
+
nearRight,
|
|
3409
|
+
bottom,
|
|
3410
|
+
"L",
|
|
3411
|
+
left,
|
|
3412
|
+
bottom
|
|
3413
|
+
]);
|
|
3414
|
+
case "⎧": return pathData([
|
|
3415
|
+
"M",
|
|
3416
|
+
right,
|
|
3417
|
+
top,
|
|
3418
|
+
"C",
|
|
3419
|
+
center,
|
|
3420
|
+
top,
|
|
3421
|
+
center,
|
|
3422
|
+
mid,
|
|
3423
|
+
nearLeft,
|
|
3424
|
+
mid,
|
|
3425
|
+
"C",
|
|
3426
|
+
center,
|
|
3427
|
+
mid,
|
|
3428
|
+
center,
|
|
3429
|
+
bottom,
|
|
3430
|
+
right,
|
|
3431
|
+
bottom
|
|
3432
|
+
]);
|
|
3433
|
+
case "⎨": return pathData([
|
|
3434
|
+
"M",
|
|
3435
|
+
right,
|
|
3436
|
+
top,
|
|
3437
|
+
"C",
|
|
3438
|
+
center,
|
|
3439
|
+
top,
|
|
3440
|
+
center,
|
|
3441
|
+
mid,
|
|
3442
|
+
nearLeft,
|
|
3443
|
+
mid,
|
|
3444
|
+
"C",
|
|
3445
|
+
center,
|
|
3446
|
+
mid,
|
|
3447
|
+
center,
|
|
3448
|
+
bottom,
|
|
3449
|
+
right,
|
|
3450
|
+
bottom
|
|
3451
|
+
]);
|
|
3452
|
+
case "⎩": return pathData([
|
|
3453
|
+
"M",
|
|
3454
|
+
right,
|
|
3455
|
+
top,
|
|
3456
|
+
"C",
|
|
3457
|
+
center,
|
|
3458
|
+
top,
|
|
3459
|
+
center,
|
|
3460
|
+
mid,
|
|
3461
|
+
nearLeft,
|
|
3462
|
+
mid,
|
|
3463
|
+
"C",
|
|
3464
|
+
center,
|
|
3465
|
+
mid,
|
|
3466
|
+
center,
|
|
3467
|
+
bottom,
|
|
3468
|
+
right,
|
|
3469
|
+
bottom
|
|
3470
|
+
]);
|
|
3471
|
+
case "⎪": return pathData([
|
|
3472
|
+
"M",
|
|
3473
|
+
nearLeft,
|
|
3474
|
+
top,
|
|
3475
|
+
"L",
|
|
3476
|
+
nearLeft,
|
|
3477
|
+
bottom
|
|
3478
|
+
]);
|
|
3479
|
+
case "⎫": return pathData([
|
|
3480
|
+
"M",
|
|
3481
|
+
left,
|
|
3482
|
+
top,
|
|
3483
|
+
"C",
|
|
3484
|
+
center,
|
|
3485
|
+
top,
|
|
3486
|
+
center,
|
|
3487
|
+
mid,
|
|
3488
|
+
nearRight,
|
|
3489
|
+
mid,
|
|
3490
|
+
"C",
|
|
3491
|
+
center,
|
|
3492
|
+
mid,
|
|
3493
|
+
center,
|
|
3494
|
+
bottom,
|
|
3495
|
+
left,
|
|
3496
|
+
bottom
|
|
3497
|
+
]);
|
|
3498
|
+
case "⎬": return pathData([
|
|
3499
|
+
"M",
|
|
3500
|
+
left,
|
|
3501
|
+
top,
|
|
3502
|
+
"C",
|
|
3503
|
+
center,
|
|
3504
|
+
top,
|
|
3505
|
+
center,
|
|
3506
|
+
mid,
|
|
3507
|
+
nearRight,
|
|
3508
|
+
mid,
|
|
3509
|
+
"C",
|
|
3510
|
+
center,
|
|
3511
|
+
mid,
|
|
3512
|
+
center,
|
|
3513
|
+
bottom,
|
|
3514
|
+
left,
|
|
3515
|
+
bottom
|
|
3516
|
+
]);
|
|
3517
|
+
case "⎭": return pathData([
|
|
3518
|
+
"M",
|
|
3519
|
+
left,
|
|
3520
|
+
top,
|
|
3521
|
+
"C",
|
|
3522
|
+
center,
|
|
3523
|
+
top,
|
|
3524
|
+
center,
|
|
3525
|
+
mid,
|
|
3526
|
+
nearRight,
|
|
3527
|
+
mid,
|
|
3528
|
+
"C",
|
|
3529
|
+
center,
|
|
3530
|
+
mid,
|
|
3531
|
+
center,
|
|
3532
|
+
bottom,
|
|
3533
|
+
left,
|
|
3534
|
+
bottom
|
|
3535
|
+
]);
|
|
3536
|
+
default: return;
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
function pathData(parts) {
|
|
3540
|
+
return parts.map((part) => typeof part === "number" ? fmt(part) : part).join(" ");
|
|
3541
|
+
}
|
|
3542
|
+
function readFontObject(record) {
|
|
3543
|
+
const charset = record[19];
|
|
3544
|
+
return {
|
|
3545
|
+
type: "font",
|
|
3546
|
+
style: 0,
|
|
3547
|
+
color: {
|
|
3548
|
+
r: 0,
|
|
3549
|
+
g: 0,
|
|
3550
|
+
b: 0,
|
|
3551
|
+
a: 255
|
|
3552
|
+
},
|
|
3553
|
+
height: Math.abs(int16At(record, 6)),
|
|
3554
|
+
weight: int16At(record, 14),
|
|
3555
|
+
italic: record[16] !== 0,
|
|
3556
|
+
charset,
|
|
3557
|
+
faceName: readNullTerminatedAnsi(record, 24, charset)
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
function int16At(bytes, offset) {
|
|
3561
|
+
const value = uint16At(bytes, offset);
|
|
3562
|
+
return value >= 32768 ? value - 65536 : value;
|
|
3563
|
+
}
|
|
3564
|
+
function uint16At(bytes, offset) {
|
|
3565
|
+
if (offset + 2 > bytes.length) return 0;
|
|
3566
|
+
return bytes[offset] | bytes[offset + 1] << 8;
|
|
3567
|
+
}
|
|
3568
|
+
function decodeWmfText(bytes, font) {
|
|
3569
|
+
if (!isSymbolFont(font)) return decodeAnsiText(stripNullBytes(bytes), font?.charset);
|
|
3570
|
+
let text = "";
|
|
3571
|
+
const byteLengths = [];
|
|
3572
|
+
for (const byte of bytes) if (byte !== 0) {
|
|
3573
|
+
text += SYMBOL_CHAR_MAP[byte] ?? String.fromCharCode(byte);
|
|
3574
|
+
byteLengths.push(1);
|
|
3575
|
+
}
|
|
3576
|
+
return {
|
|
3577
|
+
text,
|
|
3578
|
+
byteLengths
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
function isSymbolFont(font) {
|
|
3582
|
+
const faceName = font?.faceName?.toLowerCase();
|
|
3583
|
+
return font?.charset === 2 || faceName === "symbol";
|
|
3584
|
+
}
|
|
3585
|
+
function stripNullBytes(bytes) {
|
|
3586
|
+
const out = [];
|
|
3587
|
+
for (const byte of bytes) if (byte !== 0) out.push(byte);
|
|
3588
|
+
return new Uint8Array(out);
|
|
3589
|
+
}
|
|
3590
|
+
function textDecoderLabel(charset) {
|
|
3591
|
+
switch (charset) {
|
|
3592
|
+
case 128: return "shift_jis";
|
|
3593
|
+
case 129: return "ks_c_5601-1987";
|
|
3594
|
+
case 134: return "gb18030";
|
|
3595
|
+
case 136: return "big5";
|
|
3596
|
+
case 161: return "windows-1253";
|
|
3597
|
+
case 162: return "windows-1254";
|
|
3598
|
+
case 163: return "windows-1258";
|
|
3599
|
+
case 177: return "windows-1255";
|
|
3600
|
+
case 178: return "windows-1256";
|
|
3601
|
+
case 186: return "windows-1257";
|
|
3602
|
+
case 204: return "windows-1251";
|
|
3603
|
+
case 222: return "windows-874";
|
|
3604
|
+
case 238: return "windows-1250";
|
|
3605
|
+
default: return;
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
function decodeAnsiBytes(bytes, charset) {
|
|
3609
|
+
const label = textDecoderLabel(charset);
|
|
3610
|
+
if (label && typeof TextDecoder !== "undefined") try {
|
|
3611
|
+
return new TextDecoder(label).decode(bytes);
|
|
3612
|
+
} catch {}
|
|
3613
|
+
let text = "";
|
|
3614
|
+
for (const byte of bytes) text += String.fromCharCode(byte);
|
|
3615
|
+
return text;
|
|
3616
|
+
}
|
|
3617
|
+
function decodeAnsiText(bytes, charset) {
|
|
3618
|
+
const chunks = splitAnsiTextBytes(bytes, charset);
|
|
3619
|
+
return {
|
|
3620
|
+
text: chunks.map((chunk) => decodeAnsiBytes(chunk, charset)).join(""),
|
|
3621
|
+
byteLengths: chunks.map((chunk) => chunk.length)
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
function splitAnsiTextBytes(bytes, charset) {
|
|
3625
|
+
const chunks = [];
|
|
3626
|
+
for (let i = 0; i < bytes.length;) {
|
|
3627
|
+
const length = ansiCharByteLength(bytes, i, charset);
|
|
3628
|
+
chunks.push(bytes.subarray(i, i + length));
|
|
3629
|
+
i += length;
|
|
3630
|
+
}
|
|
3631
|
+
return chunks;
|
|
3632
|
+
}
|
|
3633
|
+
function ansiCharByteLength(bytes, offset, charset) {
|
|
3634
|
+
const first = bytes[offset] ?? 0;
|
|
3635
|
+
if (first < 128) return 1;
|
|
3636
|
+
switch (charset) {
|
|
3637
|
+
case 128: return isShiftJisLead(first) && offset + 1 < bytes.length ? 2 : 1;
|
|
3638
|
+
case 129: return isDoubleByteLead(first) && offset + 1 < bytes.length ? 2 : 1;
|
|
3639
|
+
case 134: return gb18030CharByteLength(bytes, offset);
|
|
3640
|
+
case 136: return isBig5Lead(first) && offset + 1 < bytes.length ? 2 : 1;
|
|
3641
|
+
default: return 1;
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
function gb18030CharByteLength(bytes, offset) {
|
|
3645
|
+
const b0 = bytes[offset] ?? 0;
|
|
3646
|
+
const b1 = bytes[offset + 1] ?? 0;
|
|
3647
|
+
const b2 = bytes[offset + 2] ?? 0;
|
|
3648
|
+
const b3 = bytes[offset + 3] ?? 0;
|
|
3649
|
+
if (b0 >= 129 && b0 <= 254 && b1 >= 48 && b1 <= 57 && b2 >= 129 && b2 <= 254 && b3 >= 48 && b3 <= 57) return 4;
|
|
3650
|
+
return b0 >= 129 && b0 <= 254 && offset + 1 < bytes.length ? 2 : 1;
|
|
3651
|
+
}
|
|
3652
|
+
function isShiftJisLead(byte) {
|
|
3653
|
+
return byte >= 129 && byte <= 159 || byte >= 224 && byte <= 252;
|
|
3654
|
+
}
|
|
3655
|
+
function isBig5Lead(byte) {
|
|
3656
|
+
return byte >= 129 && byte <= 254;
|
|
3657
|
+
}
|
|
3658
|
+
function isDoubleByteLead(byte) {
|
|
3659
|
+
return byte >= 129 && byte <= 254;
|
|
3660
|
+
}
|
|
3661
|
+
function readNullTerminatedAnsi(bytes, offset, charset) {
|
|
3662
|
+
const out = [];
|
|
3663
|
+
for (let i = offset; i < bytes.length; i++) {
|
|
3664
|
+
const byte = bytes[i];
|
|
3665
|
+
if (byte === 0) break;
|
|
3666
|
+
out.push(byte);
|
|
3667
|
+
}
|
|
3668
|
+
return decodeAnsiBytes(new Uint8Array(out), charset);
|
|
3669
|
+
}
|
|
3670
|
+
function textFontAttrs(font) {
|
|
3671
|
+
const attrs = [` font-family="${escapeXml(isSymbolFont(font) ? "serif" : font.faceName || "sans-serif")}"`];
|
|
3672
|
+
if (font.italic) attrs.push(" font-style=\"italic\"");
|
|
3673
|
+
if (font.weight && font.weight >= 700) attrs.push(" font-weight=\"700\"");
|
|
3674
|
+
return attrs.join("");
|
|
3675
|
+
}
|
|
3676
|
+
function textBaselineAttrs(state) {
|
|
3677
|
+
if (state.objectTransform && state.mathTypeLeftInset === 0) return " dominant-baseline=\"text-after-edge\"";
|
|
3678
|
+
if (!state.textAlignBaseline || state.objectTransform || isSymbolFont(state.font)) return "";
|
|
3679
|
+
if (state.mathTypeLeftInset > 0) return "";
|
|
3680
|
+
if (state.canvasW < 1e3 || state.canvasH < 500) return "";
|
|
3681
|
+
return " dominant-baseline=\"text-before-edge\"";
|
|
3682
|
+
}
|
|
3683
|
+
function escapeXml(value) {
|
|
3684
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3685
|
+
}
|
|
3686
|
+
function colorRef(low, high) {
|
|
3687
|
+
const lo = low & 65535;
|
|
3688
|
+
const hi = high & 65535;
|
|
3689
|
+
return {
|
|
3690
|
+
r: lo & 255,
|
|
3691
|
+
g: lo >> 8 & 255,
|
|
3692
|
+
b: hi & 255,
|
|
3693
|
+
a: 255
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
function fillAttr(brush) {
|
|
3697
|
+
return brush.style === 1 ? " fill=\"none\"" : ` fill="${color(brush.color)}"`;
|
|
3698
|
+
}
|
|
3699
|
+
function strokeAttr(pen, state) {
|
|
3700
|
+
if (pen.style === PenStyle.PS_NULL) return "";
|
|
3701
|
+
const scaledWidth = pen.width && state ? pen.width * transformScale(state) : pen.width;
|
|
3702
|
+
const width = scaledWidth && scaledWidth > 0 ? ` stroke-width="${fmt(scaledWidth)}"` : "";
|
|
3703
|
+
return ` stroke="${color(pen.color)}"${width}`;
|
|
3704
|
+
}
|
|
3705
|
+
function color(c) {
|
|
3706
|
+
return `rgb(${c.r},${c.g},${c.b})`;
|
|
3707
|
+
}
|
|
3708
|
+
function fmt(n) {
|
|
3709
|
+
return Number(n.toFixed(2)).toString();
|
|
3710
|
+
}
|
|
3711
|
+
//#endregion
|
|
3712
|
+
//#region src/index.ts
|
|
3713
|
+
/**
|
|
3714
|
+
* Convert an EMF (Enhanced Metafile) buffer to SVG string.
|
|
3715
|
+
*
|
|
3716
|
+
* @param buffer - EMF file data as ArrayBuffer or Uint8Array
|
|
3717
|
+
* @returns Complete SVG XML string
|
|
3718
|
+
* @throws Error if the buffer is not a valid EMF file
|
|
3719
|
+
*/
|
|
3720
|
+
function emf2svg(buffer) {
|
|
3721
|
+
return parseEmf(buffer);
|
|
3722
|
+
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Convert a WMF (Windows Metafile) buffer to SVG string.
|
|
3725
|
+
*
|
|
3726
|
+
* This lightweight converter covers common vector records used in legacy
|
|
3727
|
+
* Office clipart/templates: window extents, lines, rectangles, ellipses,
|
|
3728
|
+
* polygons, polylines, solid pens, and solid brushes.
|
|
3729
|
+
*/
|
|
3730
|
+
function wmf2svg(buffer) {
|
|
3731
|
+
return wmf2svg$1(buffer);
|
|
3732
|
+
}
|
|
3733
|
+
/**
|
|
3734
|
+
* Check if a buffer is a valid EMF file by verifying the magic number.
|
|
3735
|
+
* EMF files have signature 0x464D4520 (' EMF') at offset 40 in the header.
|
|
3736
|
+
*
|
|
3737
|
+
* @param buffer - Data to check
|
|
3738
|
+
* @returns true if the buffer starts with a valid EMF header
|
|
3739
|
+
*/
|
|
3740
|
+
function isEmf(buffer) {
|
|
3741
|
+
const view = buffer instanceof Uint8Array ? new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) : new DataView(buffer);
|
|
3742
|
+
if (view.byteLength < 88) return false;
|
|
3743
|
+
if (view.getUint32(0, true) !== 1) return false;
|
|
3744
|
+
return view.getUint32(40, true) === EMF_SIGNATURE;
|
|
3745
|
+
}
|
|
3746
|
+
/**
|
|
3747
|
+
* Check if a buffer looks like a WMF file, including Aldus placeable WMF.
|
|
3748
|
+
*/
|
|
3749
|
+
function isWmf(buffer) {
|
|
3750
|
+
return isWmf$1(buffer);
|
|
3751
|
+
}
|
|
3752
|
+
//#endregion
|
|
3753
|
+
exports.emf2svg = emf2svg;
|
|
3754
|
+
exports.isEmf = isEmf;
|
|
3755
|
+
exports.isWmf = isWmf;
|
|
3756
|
+
exports.wmf2svg = wmf2svg;
|