@emasoft/svg-matrix 1.0.30 → 1.0.31
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/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
package/src/convert-path-data.js
CHANGED
|
@@ -16,8 +16,6 @@ import Decimal from "decimal.js";
|
|
|
16
16
|
|
|
17
17
|
Decimal.set({ precision: 80 });
|
|
18
18
|
|
|
19
|
-
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
20
|
-
|
|
21
19
|
// SVG path command parameters count
|
|
22
20
|
const COMMAND_PARAMS = {
|
|
23
21
|
M: 2,
|
|
@@ -52,6 +50,13 @@ const COMMAND_PARAMS = {
|
|
|
52
50
|
* @returns {Array<number>|null} Parsed arc parameters or null if invalid
|
|
53
51
|
*/
|
|
54
52
|
function parseArcArgs(argsStr) {
|
|
53
|
+
// Parameter validation: argsStr must be a string
|
|
54
|
+
if (typeof argsStr !== "string") {
|
|
55
|
+
throw new TypeError(
|
|
56
|
+
`parseArcArgs: argsStr must be a string, got ${typeof argsStr}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
const args = [];
|
|
56
61
|
// Regex to match: number, then optionally flags and more numbers
|
|
57
62
|
// Arc has: rx ry rotation flag flag x y (7 params per arc)
|
|
@@ -170,6 +175,14 @@ export function parsePath(d) {
|
|
|
170
175
|
|
|
171
176
|
const paramCount = COMMAND_PARAMS[cmd];
|
|
172
177
|
|
|
178
|
+
// BUG FIX #6: Validate command exists in COMMAND_PARAMS
|
|
179
|
+
if (paramCount === undefined) {
|
|
180
|
+
console.warn(
|
|
181
|
+
`Unknown SVG path command '${cmd}' - skipping`,
|
|
182
|
+
);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
173
186
|
if (paramCount === 0 || nums.length === 0) {
|
|
174
187
|
commands.push({ command: cmd, args: [] });
|
|
175
188
|
} else {
|
|
@@ -203,6 +216,21 @@ export function parsePath(d) {
|
|
|
203
216
|
* @returns {string} Formatted number
|
|
204
217
|
*/
|
|
205
218
|
export function formatNumber(num, precision = 3) {
|
|
219
|
+
// Parameter validation: num must be a number
|
|
220
|
+
if (typeof num !== "number") {
|
|
221
|
+
throw new TypeError(`formatNumber: num must be a number, got ${typeof num}`);
|
|
222
|
+
}
|
|
223
|
+
// Parameter validation: precision must be a non-negative integer
|
|
224
|
+
if (
|
|
225
|
+
typeof precision !== "number" ||
|
|
226
|
+
!Number.isInteger(precision) ||
|
|
227
|
+
precision < 0
|
|
228
|
+
) {
|
|
229
|
+
throw new TypeError(
|
|
230
|
+
`formatNumber: precision must be a non-negative integer, got ${precision}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
206
234
|
// BUG FIX #3: Handle NaN, Infinity, -Infinity
|
|
207
235
|
if (!isFinite(num)) return "0";
|
|
208
236
|
if (num === 0) return "0";
|
|
@@ -233,9 +261,35 @@ export function formatNumber(num, precision = 3) {
|
|
|
233
261
|
* Convert a command to absolute form.
|
|
234
262
|
*/
|
|
235
263
|
export function toAbsolute(cmd, cx, cy) {
|
|
264
|
+
// Parameter validation: cmd must be an object with command and args
|
|
265
|
+
if (!cmd || typeof cmd !== "object") {
|
|
266
|
+
throw new TypeError(`toAbsolute: cmd must be an object, got ${typeof cmd}`);
|
|
267
|
+
}
|
|
268
|
+
if (typeof cmd.command !== "string") {
|
|
269
|
+
throw new TypeError(
|
|
270
|
+
`toAbsolute: cmd.command must be a string, got ${typeof cmd.command}`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (!Array.isArray(cmd.args)) {
|
|
274
|
+
throw new TypeError(`toAbsolute: cmd.args must be an array`);
|
|
275
|
+
}
|
|
276
|
+
// Parameter validation: cx and cy must be numbers
|
|
277
|
+
if (typeof cx !== "number" || !isFinite(cx)) {
|
|
278
|
+
throw new TypeError(`toAbsolute: cx must be a finite number, got ${cx}`);
|
|
279
|
+
}
|
|
280
|
+
if (typeof cy !== "number" || !isFinite(cy)) {
|
|
281
|
+
throw new TypeError(`toAbsolute: cy must be a finite number, got ${cy}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
236
284
|
const { command, args } = cmd;
|
|
237
285
|
|
|
238
|
-
|
|
286
|
+
// BUG FIX #7: Validate command is known before early return
|
|
287
|
+
if (command === command.toUpperCase()) {
|
|
288
|
+
if (COMMAND_PARAMS[command] === undefined) {
|
|
289
|
+
throw new TypeError(`toAbsolute: unknown command '${command}'`);
|
|
290
|
+
}
|
|
291
|
+
return cmd;
|
|
292
|
+
}
|
|
239
293
|
|
|
240
294
|
const absCmd = command.toUpperCase();
|
|
241
295
|
const absArgs = [...args];
|
|
@@ -244,16 +298,40 @@ export function toAbsolute(cmd, cx, cy) {
|
|
|
244
298
|
case "m":
|
|
245
299
|
case "l":
|
|
246
300
|
case "t":
|
|
301
|
+
// Bounds check: need at least 2 args
|
|
302
|
+
if (absArgs.length < 2) {
|
|
303
|
+
throw new RangeError(
|
|
304
|
+
`toAbsolute: command ${command} requires at least 2 args, got ${absArgs.length}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
247
307
|
absArgs[0] += cx;
|
|
248
308
|
absArgs[1] += cy;
|
|
249
309
|
break;
|
|
250
310
|
case "h":
|
|
311
|
+
// Bounds check: need at least 1 arg
|
|
312
|
+
if (absArgs.length < 1) {
|
|
313
|
+
throw new RangeError(
|
|
314
|
+
`toAbsolute: command ${command} requires at least 1 arg, got ${absArgs.length}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
251
317
|
absArgs[0] += cx;
|
|
252
318
|
break;
|
|
253
319
|
case "v":
|
|
320
|
+
// Bounds check: need at least 1 arg
|
|
321
|
+
if (absArgs.length < 1) {
|
|
322
|
+
throw new RangeError(
|
|
323
|
+
`toAbsolute: command ${command} requires at least 1 arg, got ${absArgs.length}`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
254
326
|
absArgs[0] += cy;
|
|
255
327
|
break;
|
|
256
328
|
case "c":
|
|
329
|
+
// Bounds check: need at least 6 args
|
|
330
|
+
if (absArgs.length < 6) {
|
|
331
|
+
throw new RangeError(
|
|
332
|
+
`toAbsolute: command ${command} requires at least 6 args, got ${absArgs.length}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
257
335
|
absArgs[0] += cx;
|
|
258
336
|
absArgs[1] += cy;
|
|
259
337
|
absArgs[2] += cx;
|
|
@@ -263,15 +341,32 @@ export function toAbsolute(cmd, cx, cy) {
|
|
|
263
341
|
break;
|
|
264
342
|
case "s":
|
|
265
343
|
case "q":
|
|
344
|
+
// Bounds check: need at least 4 args
|
|
345
|
+
if (absArgs.length < 4) {
|
|
346
|
+
throw new RangeError(
|
|
347
|
+
`toAbsolute: command ${command} requires at least 4 args, got ${absArgs.length}`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
266
350
|
absArgs[0] += cx;
|
|
267
351
|
absArgs[1] += cy;
|
|
268
352
|
absArgs[2] += cx;
|
|
269
353
|
absArgs[3] += cy;
|
|
270
354
|
break;
|
|
271
355
|
case "a":
|
|
356
|
+
// Bounds check: need at least 7 args
|
|
357
|
+
if (absArgs.length < 7) {
|
|
358
|
+
throw new RangeError(
|
|
359
|
+
`toAbsolute: command ${command} requires at least 7 args, got ${absArgs.length}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
272
362
|
absArgs[5] += cx;
|
|
273
363
|
absArgs[6] += cy;
|
|
274
364
|
break;
|
|
365
|
+
default:
|
|
366
|
+
// BUG FIX #8: Handle unknown commands in default case
|
|
367
|
+
throw new TypeError(
|
|
368
|
+
`toAbsolute: unsupported command '${command}' for conversion`,
|
|
369
|
+
);
|
|
275
370
|
}
|
|
276
371
|
|
|
277
372
|
return { command: absCmd, args: absArgs };
|
|
@@ -281,9 +376,35 @@ export function toAbsolute(cmd, cx, cy) {
|
|
|
281
376
|
* Convert a command to relative form.
|
|
282
377
|
*/
|
|
283
378
|
export function toRelative(cmd, cx, cy) {
|
|
379
|
+
// Parameter validation: cmd must be an object with command and args
|
|
380
|
+
if (!cmd || typeof cmd !== "object") {
|
|
381
|
+
throw new TypeError(`toRelative: cmd must be an object, got ${typeof cmd}`);
|
|
382
|
+
}
|
|
383
|
+
if (typeof cmd.command !== "string") {
|
|
384
|
+
throw new TypeError(
|
|
385
|
+
`toRelative: cmd.command must be a string, got ${typeof cmd.command}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (!Array.isArray(cmd.args)) {
|
|
389
|
+
throw new TypeError(`toRelative: cmd.args must be an array`);
|
|
390
|
+
}
|
|
391
|
+
// Parameter validation: cx and cy must be numbers
|
|
392
|
+
if (typeof cx !== "number" || !isFinite(cx)) {
|
|
393
|
+
throw new TypeError(`toRelative: cx must be a finite number, got ${cx}`);
|
|
394
|
+
}
|
|
395
|
+
if (typeof cy !== "number" || !isFinite(cy)) {
|
|
396
|
+
throw new TypeError(`toRelative: cy must be a finite number, got ${cy}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
284
399
|
const { command, args } = cmd;
|
|
285
400
|
|
|
286
|
-
|
|
401
|
+
// BUG FIX #9: Validate command is known before early return
|
|
402
|
+
if (command === command.toLowerCase() && command !== "z") {
|
|
403
|
+
if (COMMAND_PARAMS[command] === undefined) {
|
|
404
|
+
throw new TypeError(`toRelative: unknown command '${command}'`);
|
|
405
|
+
}
|
|
406
|
+
return cmd;
|
|
407
|
+
}
|
|
287
408
|
if (command === "Z" || command === "z") return { command: "z", args: [] };
|
|
288
409
|
|
|
289
410
|
const relCmd = command.toLowerCase();
|
|
@@ -293,16 +414,40 @@ export function toRelative(cmd, cx, cy) {
|
|
|
293
414
|
case "M":
|
|
294
415
|
case "L":
|
|
295
416
|
case "T":
|
|
417
|
+
// Bounds check: need at least 2 args
|
|
418
|
+
if (relArgs.length < 2) {
|
|
419
|
+
throw new RangeError(
|
|
420
|
+
`toRelative: command ${command} requires at least 2 args, got ${relArgs.length}`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
296
423
|
relArgs[0] -= cx;
|
|
297
424
|
relArgs[1] -= cy;
|
|
298
425
|
break;
|
|
299
426
|
case "H":
|
|
427
|
+
// Bounds check: need at least 1 arg
|
|
428
|
+
if (relArgs.length < 1) {
|
|
429
|
+
throw new RangeError(
|
|
430
|
+
`toRelative: command ${command} requires at least 1 arg, got ${relArgs.length}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
300
433
|
relArgs[0] -= cx;
|
|
301
434
|
break;
|
|
302
435
|
case "V":
|
|
436
|
+
// Bounds check: need at least 1 arg
|
|
437
|
+
if (relArgs.length < 1) {
|
|
438
|
+
throw new RangeError(
|
|
439
|
+
`toRelative: command ${command} requires at least 1 arg, got ${relArgs.length}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
303
442
|
relArgs[0] -= cy;
|
|
304
443
|
break;
|
|
305
444
|
case "C":
|
|
445
|
+
// Bounds check: need at least 6 args
|
|
446
|
+
if (relArgs.length < 6) {
|
|
447
|
+
throw new RangeError(
|
|
448
|
+
`toRelative: command ${command} requires at least 6 args, got ${relArgs.length}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
306
451
|
relArgs[0] -= cx;
|
|
307
452
|
relArgs[1] -= cy;
|
|
308
453
|
relArgs[2] -= cx;
|
|
@@ -312,15 +457,32 @@ export function toRelative(cmd, cx, cy) {
|
|
|
312
457
|
break;
|
|
313
458
|
case "S":
|
|
314
459
|
case "Q":
|
|
460
|
+
// Bounds check: need at least 4 args
|
|
461
|
+
if (relArgs.length < 4) {
|
|
462
|
+
throw new RangeError(
|
|
463
|
+
`toRelative: command ${command} requires at least 4 args, got ${relArgs.length}`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
315
466
|
relArgs[0] -= cx;
|
|
316
467
|
relArgs[1] -= cy;
|
|
317
468
|
relArgs[2] -= cx;
|
|
318
469
|
relArgs[3] -= cy;
|
|
319
470
|
break;
|
|
320
471
|
case "A":
|
|
472
|
+
// Bounds check: need at least 7 args
|
|
473
|
+
if (relArgs.length < 7) {
|
|
474
|
+
throw new RangeError(
|
|
475
|
+
`toRelative: command ${command} requires at least 7 args, got ${relArgs.length}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
321
478
|
relArgs[5] -= cx;
|
|
322
479
|
relArgs[6] -= cy;
|
|
323
480
|
break;
|
|
481
|
+
default:
|
|
482
|
+
// BUG FIX #10: Handle unknown commands in default case
|
|
483
|
+
throw new TypeError(
|
|
484
|
+
`toRelative: unsupported command '${command}' for conversion`,
|
|
485
|
+
);
|
|
324
486
|
}
|
|
325
487
|
|
|
326
488
|
return { command: relCmd, args: relArgs };
|
|
@@ -330,9 +492,41 @@ export function toRelative(cmd, cx, cy) {
|
|
|
330
492
|
* Convert L command to H or V when applicable.
|
|
331
493
|
*/
|
|
332
494
|
export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
|
|
495
|
+
// Parameter validation: cmd must be an object with command and args
|
|
496
|
+
if (!cmd || typeof cmd !== "object") {
|
|
497
|
+
throw new TypeError(`lineToHV: cmd must be an object, got ${typeof cmd}`);
|
|
498
|
+
}
|
|
499
|
+
if (typeof cmd.command !== "string") {
|
|
500
|
+
throw new TypeError(
|
|
501
|
+
`lineToHV: cmd.command must be a string, got ${typeof cmd.command}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (!Array.isArray(cmd.args)) {
|
|
505
|
+
throw new TypeError(`lineToHV: cmd.args must be an array`);
|
|
506
|
+
}
|
|
507
|
+
// Parameter validation: cx, cy, and tolerance must be numbers
|
|
508
|
+
if (typeof cx !== "number" || !isFinite(cx)) {
|
|
509
|
+
throw new TypeError(`lineToHV: cx must be a finite number, got ${cx}`);
|
|
510
|
+
}
|
|
511
|
+
if (typeof cy !== "number" || !isFinite(cy)) {
|
|
512
|
+
throw new TypeError(`lineToHV: cy must be a finite number, got ${cy}`);
|
|
513
|
+
}
|
|
514
|
+
if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
|
|
515
|
+
throw new TypeError(
|
|
516
|
+
`lineToHV: tolerance must be a non-negative finite number, got ${tolerance}`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
333
520
|
const { command, args } = cmd;
|
|
334
521
|
if (command !== "L" && command !== "l") return null;
|
|
335
522
|
|
|
523
|
+
// Bounds check: need at least 2 args
|
|
524
|
+
if (args.length < 2) {
|
|
525
|
+
throw new RangeError(
|
|
526
|
+
`lineToHV: command ${command} requires at least 2 args, got ${args.length}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
336
530
|
const isAbs = command === "L";
|
|
337
531
|
const endX = isAbs ? args[0] : cx + args[0];
|
|
338
532
|
const endY = isAbs ? args[1] : cy + args[1];
|
|
@@ -354,9 +548,47 @@ export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
|
|
|
354
548
|
* Check if a line command returns to subpath start (can use Z).
|
|
355
549
|
*/
|
|
356
550
|
export function lineToZ(cmd, cx, cy, startX, startY, tolerance = 1e-6) {
|
|
551
|
+
// Parameter validation: cmd must be an object with command and args
|
|
552
|
+
if (!cmd || typeof cmd !== "object") {
|
|
553
|
+
throw new TypeError(`lineToZ: cmd must be an object, got ${typeof cmd}`);
|
|
554
|
+
}
|
|
555
|
+
if (typeof cmd.command !== "string") {
|
|
556
|
+
throw new TypeError(
|
|
557
|
+
`lineToZ: cmd.command must be a string, got ${typeof cmd.command}`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (!Array.isArray(cmd.args)) {
|
|
561
|
+
throw new TypeError(`lineToZ: cmd.args must be an array`);
|
|
562
|
+
}
|
|
563
|
+
// Parameter validation: cx, cy, startX, startY, and tolerance must be numbers
|
|
564
|
+
if (typeof cx !== "number" || !isFinite(cx)) {
|
|
565
|
+
throw new TypeError(`lineToZ: cx must be a finite number, got ${cx}`);
|
|
566
|
+
}
|
|
567
|
+
if (typeof cy !== "number" || !isFinite(cy)) {
|
|
568
|
+
throw new TypeError(`lineToZ: cy must be a finite number, got ${cy}`);
|
|
569
|
+
}
|
|
570
|
+
if (typeof startX !== "number" || !isFinite(startX)) {
|
|
571
|
+
throw new TypeError(`lineToZ: startX must be a finite number, got ${startX}`);
|
|
572
|
+
}
|
|
573
|
+
if (typeof startY !== "number" || !isFinite(startY)) {
|
|
574
|
+
throw new TypeError(`lineToZ: startY must be a finite number, got ${startY}`);
|
|
575
|
+
}
|
|
576
|
+
if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
|
|
577
|
+
throw new TypeError(
|
|
578
|
+
`lineToZ: tolerance must be a non-negative finite number, got ${tolerance}`,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
357
582
|
const { command, args } = cmd;
|
|
358
583
|
if (command !== "L" && command !== "l") return false;
|
|
359
584
|
|
|
585
|
+
// Bounds check: need at least 2 args
|
|
586
|
+
if (args.length < 2) {
|
|
587
|
+
throw new RangeError(
|
|
588
|
+
`lineToZ: command ${command} requires at least 2 args, got ${args.length}`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
360
592
|
const isAbs = command === "L";
|
|
361
593
|
const endX = isAbs ? args[0] : cx + args[0];
|
|
362
594
|
const endY = isAbs ? args[1] : cy + args[1];
|
|
@@ -380,6 +612,21 @@ export function isCurveStraight(
|
|
|
380
612
|
y3,
|
|
381
613
|
tolerance = 0.5,
|
|
382
614
|
) {
|
|
615
|
+
// Parameter validation: all parameters must be finite numbers
|
|
616
|
+
const params = { x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, tolerance };
|
|
617
|
+
for (const [name, value] of Object.entries(params)) {
|
|
618
|
+
if (typeof value !== "number" || !isFinite(value)) {
|
|
619
|
+
throw new TypeError(
|
|
620
|
+
`isCurveStraight: ${name} must be a finite number, got ${value}`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (tolerance < 0) {
|
|
625
|
+
throw new TypeError(
|
|
626
|
+
`isCurveStraight: tolerance must be non-negative, got ${tolerance}`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
383
630
|
const chordLengthSq = (x3 - x0) ** 2 + (y3 - y0) ** 2;
|
|
384
631
|
|
|
385
632
|
if (chordLengthSq < 1e-10) {
|
|
@@ -403,9 +650,47 @@ export function isCurveStraight(
|
|
|
403
650
|
* Convert a straight cubic bezier to a line command.
|
|
404
651
|
*/
|
|
405
652
|
export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
|
|
653
|
+
// Parameter validation: cmd must be an object with command and args
|
|
654
|
+
if (!cmd || typeof cmd !== "object") {
|
|
655
|
+
throw new TypeError(
|
|
656
|
+
`straightCurveToLine: cmd must be an object, got ${typeof cmd}`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (typeof cmd.command !== "string") {
|
|
660
|
+
throw new TypeError(
|
|
661
|
+
`straightCurveToLine: cmd.command must be a string, got ${typeof cmd.command}`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (!Array.isArray(cmd.args)) {
|
|
665
|
+
throw new TypeError(`straightCurveToLine: cmd.args must be an array`);
|
|
666
|
+
}
|
|
667
|
+
// Parameter validation: cx, cy, and tolerance must be numbers
|
|
668
|
+
if (typeof cx !== "number" || !isFinite(cx)) {
|
|
669
|
+
throw new TypeError(
|
|
670
|
+
`straightCurveToLine: cx must be a finite number, got ${cx}`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
if (typeof cy !== "number" || !isFinite(cy)) {
|
|
674
|
+
throw new TypeError(
|
|
675
|
+
`straightCurveToLine: cy must be a finite number, got ${cy}`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
if (typeof tolerance !== "number" || !isFinite(tolerance) || tolerance < 0) {
|
|
679
|
+
throw new TypeError(
|
|
680
|
+
`straightCurveToLine: tolerance must be a non-negative finite number, got ${tolerance}`,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
406
684
|
const { command, args } = cmd;
|
|
407
685
|
if (command !== "C" && command !== "c") return null;
|
|
408
686
|
|
|
687
|
+
// Bounds check: need at least 6 args
|
|
688
|
+
if (args.length < 6) {
|
|
689
|
+
throw new RangeError(
|
|
690
|
+
`straightCurveToLine: command ${command} requires at least 6 args, got ${args.length}`,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
409
694
|
const isAbs = command === "C";
|
|
410
695
|
const cp1x = isAbs ? args[0] : cx + args[0];
|
|
411
696
|
const cp1y = isAbs ? args[1] : cy + args[1];
|
|
@@ -436,6 +721,43 @@ export function serializeCommand(
|
|
|
436
721
|
precision = 3,
|
|
437
722
|
prevLastArgHadDecimal = false,
|
|
438
723
|
) {
|
|
724
|
+
// Parameter validation: cmd must be an object with command and args
|
|
725
|
+
if (!cmd || typeof cmd !== "object") {
|
|
726
|
+
throw new TypeError(
|
|
727
|
+
`serializeCommand: cmd must be an object, got ${typeof cmd}`,
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
if (typeof cmd.command !== "string") {
|
|
731
|
+
throw new TypeError(
|
|
732
|
+
`serializeCommand: cmd.command must be a string, got ${typeof cmd.command}`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (!Array.isArray(cmd.args)) {
|
|
736
|
+
throw new TypeError(`serializeCommand: cmd.args must be an array`);
|
|
737
|
+
}
|
|
738
|
+
// Parameter validation: prevCommand must be string or null
|
|
739
|
+
if (prevCommand !== null && typeof prevCommand !== "string") {
|
|
740
|
+
throw new TypeError(
|
|
741
|
+
`serializeCommand: prevCommand must be a string or null, got ${typeof prevCommand}`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
// Parameter validation: precision must be non-negative integer
|
|
745
|
+
if (
|
|
746
|
+
typeof precision !== "number" ||
|
|
747
|
+
!Number.isInteger(precision) ||
|
|
748
|
+
precision < 0
|
|
749
|
+
) {
|
|
750
|
+
throw new TypeError(
|
|
751
|
+
`serializeCommand: precision must be a non-negative integer, got ${precision}`,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
// Parameter validation: prevLastArgHadDecimal must be boolean
|
|
755
|
+
if (typeof prevLastArgHadDecimal !== "boolean") {
|
|
756
|
+
throw new TypeError(
|
|
757
|
+
`serializeCommand: prevLastArgHadDecimal must be a boolean, got ${typeof prevLastArgHadDecimal}`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
439
761
|
const { command, args } = cmd;
|
|
440
762
|
|
|
441
763
|
if (command === "Z" || command === "z") {
|
|
@@ -446,6 +768,20 @@ export function serializeCommand(
|
|
|
446
768
|
// Arc format: rx ry rotation large-arc-flag sweep-flag x y
|
|
447
769
|
// Per SVG spec: flags MUST be exactly 0 or 1, arc commands CANNOT be implicitly repeated
|
|
448
770
|
if (command === "A" || command === "a") {
|
|
771
|
+
// Bounds check: arc commands need at least 7 args
|
|
772
|
+
if (args.length < 7) {
|
|
773
|
+
throw new RangeError(
|
|
774
|
+
`serializeCommand: arc command requires at least 7 args, got ${args.length}`,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// BUG FIX #11: Warn if args.length > 7 (multiple arcs should be separate commands)
|
|
779
|
+
if (args.length > 7) {
|
|
780
|
+
console.warn(
|
|
781
|
+
`serializeCommand: arc command has ${args.length} args (expected 7) - only first 7 will be used`,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
449
785
|
const arcArgs = [
|
|
450
786
|
formatNumber(args[0], precision), // rx
|
|
451
787
|
formatNumber(args[1], precision), // ry
|
|
@@ -463,6 +799,14 @@ export function serializeCommand(
|
|
|
463
799
|
};
|
|
464
800
|
}
|
|
465
801
|
|
|
802
|
+
// Edge case: handle empty args array (should not happen for valid commands except Z)
|
|
803
|
+
if (args.length === 0) {
|
|
804
|
+
// Only Z/z should have empty args, but we already handled that above
|
|
805
|
+
throw new RangeError(
|
|
806
|
+
`serializeCommand: command ${command} has empty args array (only Z/z should have no args)`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
466
810
|
const formattedArgs = args.map((n) => formatNumber(n, precision));
|
|
467
811
|
|
|
468
812
|
let argsStr = "";
|
|
@@ -525,6 +869,23 @@ export function serializeCommand(
|
|
|
525
869
|
* Serialize a complete path to minimal string.
|
|
526
870
|
*/
|
|
527
871
|
export function serializePath(commands, precision = 3) {
|
|
872
|
+
// Parameter validation: commands must be an array
|
|
873
|
+
if (!Array.isArray(commands)) {
|
|
874
|
+
throw new TypeError(
|
|
875
|
+
`serializePath: commands must be an array, got ${typeof commands}`,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
// Parameter validation: precision must be non-negative integer
|
|
879
|
+
if (
|
|
880
|
+
typeof precision !== "number" ||
|
|
881
|
+
!Number.isInteger(precision) ||
|
|
882
|
+
precision < 0
|
|
883
|
+
) {
|
|
884
|
+
throw new TypeError(
|
|
885
|
+
`serializePath: precision must be a non-negative integer, got ${precision}`,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
528
889
|
if (commands.length === 0) return "";
|
|
529
890
|
|
|
530
891
|
let result = "";
|
|
@@ -553,6 +914,17 @@ export function serializePath(commands, precision = 3) {
|
|
|
553
914
|
* @returns {{d: string, originalLength: number, optimizedLength: number, savings: number}}
|
|
554
915
|
*/
|
|
555
916
|
export function convertPathData(d, options = {}) {
|
|
917
|
+
// Parameter validation: d must be a string
|
|
918
|
+
if (typeof d !== "string") {
|
|
919
|
+
throw new TypeError(`convertPathData: d must be a string, got ${typeof d}`);
|
|
920
|
+
}
|
|
921
|
+
// Parameter validation: options must be an object
|
|
922
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
923
|
+
throw new TypeError(
|
|
924
|
+
`convertPathData: options must be an object, got ${typeof options}`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
556
928
|
const {
|
|
557
929
|
floatPrecision = 3,
|
|
558
930
|
straightCurves = true,
|
|
@@ -562,6 +934,46 @@ export function convertPathData(d, options = {}) {
|
|
|
562
934
|
straightTolerance = 0.5,
|
|
563
935
|
} = options;
|
|
564
936
|
|
|
937
|
+
// Validate option types
|
|
938
|
+
if (
|
|
939
|
+
typeof floatPrecision !== "number" ||
|
|
940
|
+
!Number.isInteger(floatPrecision) ||
|
|
941
|
+
floatPrecision < 0
|
|
942
|
+
) {
|
|
943
|
+
throw new TypeError(
|
|
944
|
+
`convertPathData: floatPrecision must be a non-negative integer, got ${floatPrecision}`,
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
if (typeof straightCurves !== "boolean") {
|
|
948
|
+
throw new TypeError(
|
|
949
|
+
`convertPathData: straightCurves must be a boolean, got ${typeof straightCurves}`,
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (typeof lineShorthands !== "boolean") {
|
|
953
|
+
throw new TypeError(
|
|
954
|
+
`convertPathData: lineShorthands must be a boolean, got ${typeof lineShorthands}`,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (typeof convertToZ !== "boolean") {
|
|
958
|
+
throw new TypeError(
|
|
959
|
+
`convertPathData: convertToZ must be a boolean, got ${typeof convertToZ}`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
if (typeof utilizeAbsolute !== "boolean") {
|
|
963
|
+
throw new TypeError(
|
|
964
|
+
`convertPathData: utilizeAbsolute must be a boolean, got ${typeof utilizeAbsolute}`,
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (
|
|
968
|
+
typeof straightTolerance !== "number" ||
|
|
969
|
+
!isFinite(straightTolerance) ||
|
|
970
|
+
straightTolerance < 0
|
|
971
|
+
) {
|
|
972
|
+
throw new TypeError(
|
|
973
|
+
`convertPathData: straightTolerance must be a non-negative finite number, got ${straightTolerance}`,
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
565
977
|
const originalLength = d.length;
|
|
566
978
|
const commands = parsePath(d);
|
|
567
979
|
|
|
@@ -580,16 +992,20 @@ export function convertPathData(d, options = {}) {
|
|
|
580
992
|
|
|
581
993
|
// BUG FIX #3: Convert zero arc radii to line per SVG spec Section 8.3.4
|
|
582
994
|
// When rx or ry is 0, the arc degenerates to a straight line
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
(cmd.args
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
995
|
+
// BUG FIX #12: Add bounds check before accessing arc args
|
|
996
|
+
if (cmd.command === "A" || cmd.command === "a") {
|
|
997
|
+
if (cmd.args.length < 7) {
|
|
998
|
+
console.warn(
|
|
999
|
+
`Arc command has insufficient args (${cmd.args.length} < 7) - skipping optimization`,
|
|
1000
|
+
);
|
|
1001
|
+
} else if (cmd.args[0] === 0 || cmd.args[1] === 0) {
|
|
1002
|
+
const isAbs = cmd.command === "A";
|
|
1003
|
+
const endX = cmd.args[5];
|
|
1004
|
+
const endY = cmd.args[6];
|
|
1005
|
+
cmd = isAbs
|
|
1006
|
+
? { command: "L", args: [endX, endY] }
|
|
1007
|
+
: { command: "l", args: [endX, endY] };
|
|
1008
|
+
}
|
|
593
1009
|
}
|
|
594
1010
|
|
|
595
1011
|
// 1. Straight curve to line
|
|
@@ -626,41 +1042,59 @@ export function convertPathData(d, options = {}) {
|
|
|
626
1042
|
|
|
627
1043
|
// Update position
|
|
628
1044
|
const finalCmd = toAbsolute(cmd, cx, cy);
|
|
1045
|
+
// BUG FIX #13: Add bounds checks before accessing finalCmd.args
|
|
629
1046
|
switch (finalCmd.command) {
|
|
630
1047
|
case "M":
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1048
|
+
if (finalCmd.args.length >= 2) {
|
|
1049
|
+
cx = finalCmd.args[0];
|
|
1050
|
+
cy = finalCmd.args[1];
|
|
1051
|
+
startX = cx;
|
|
1052
|
+
startY = cy;
|
|
1053
|
+
}
|
|
635
1054
|
break;
|
|
636
1055
|
case "L":
|
|
637
1056
|
case "T":
|
|
638
|
-
|
|
639
|
-
|
|
1057
|
+
if (finalCmd.args.length >= 2) {
|
|
1058
|
+
cx = finalCmd.args[0];
|
|
1059
|
+
cy = finalCmd.args[1];
|
|
1060
|
+
}
|
|
640
1061
|
break;
|
|
641
1062
|
case "H":
|
|
642
|
-
|
|
1063
|
+
if (finalCmd.args.length >= 1) {
|
|
1064
|
+
cx = finalCmd.args[0];
|
|
1065
|
+
}
|
|
643
1066
|
break;
|
|
644
1067
|
case "V":
|
|
645
|
-
|
|
1068
|
+
if (finalCmd.args.length >= 1) {
|
|
1069
|
+
cy = finalCmd.args[0];
|
|
1070
|
+
}
|
|
646
1071
|
break;
|
|
647
1072
|
case "C":
|
|
648
|
-
|
|
649
|
-
|
|
1073
|
+
if (finalCmd.args.length >= 6) {
|
|
1074
|
+
cx = finalCmd.args[4];
|
|
1075
|
+
cy = finalCmd.args[5];
|
|
1076
|
+
}
|
|
650
1077
|
break;
|
|
651
1078
|
case "S":
|
|
652
1079
|
case "Q":
|
|
653
|
-
|
|
654
|
-
|
|
1080
|
+
if (finalCmd.args.length >= 4) {
|
|
1081
|
+
cx = finalCmd.args[2];
|
|
1082
|
+
cy = finalCmd.args[3];
|
|
1083
|
+
}
|
|
655
1084
|
break;
|
|
656
1085
|
case "A":
|
|
657
|
-
|
|
658
|
-
|
|
1086
|
+
if (finalCmd.args.length >= 7) {
|
|
1087
|
+
cx = finalCmd.args[5];
|
|
1088
|
+
cy = finalCmd.args[6];
|
|
1089
|
+
}
|
|
659
1090
|
break;
|
|
660
1091
|
case "Z":
|
|
661
1092
|
cx = startX;
|
|
662
1093
|
cy = startY;
|
|
663
1094
|
break;
|
|
1095
|
+
default:
|
|
1096
|
+
// Unknown command - don't update position
|
|
1097
|
+
break;
|
|
664
1098
|
}
|
|
665
1099
|
}
|
|
666
1100
|
|
|
@@ -678,11 +1112,28 @@ export function convertPathData(d, options = {}) {
|
|
|
678
1112
|
* Optimize all path elements in an SVG document.
|
|
679
1113
|
*/
|
|
680
1114
|
export function optimizeDocumentPaths(root, options = {}) {
|
|
1115
|
+
// Parameter validation: root must be an object (DOM element)
|
|
1116
|
+
if (!root || typeof root !== "object") {
|
|
1117
|
+
throw new TypeError(
|
|
1118
|
+
`optimizeDocumentPaths: root must be a DOM element object, got ${typeof root}`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
// Parameter validation: options must be an object
|
|
1122
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
1123
|
+
throw new TypeError(
|
|
1124
|
+
`optimizeDocumentPaths: options must be an object, got ${typeof options}`,
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
681
1128
|
let pathsOptimized = 0;
|
|
682
1129
|
let totalSavings = 0;
|
|
683
1130
|
const details = [];
|
|
684
1131
|
|
|
685
1132
|
const processElement = (el) => {
|
|
1133
|
+
// Validate element is an object
|
|
1134
|
+
if (!el || typeof el !== "object") {
|
|
1135
|
+
return; // Skip invalid elements silently
|
|
1136
|
+
}
|
|
686
1137
|
const tagName = el.tagName?.toLowerCase();
|
|
687
1138
|
|
|
688
1139
|
if (tagName === "path") {
|