@emasoft/svg-matrix 1.2.1 → 1.3.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/README.md +75 -0
- package/bin/svgfonts.js +1412 -0
- package/bin/svgm.js +1 -1
- package/dist/svg-matrix.global.min.js +8 -0
- package/dist/svg-matrix.min.js +2 -2
- package/dist/svg-toolbox.global.min.js +493 -0
- package/dist/svg-toolbox.min.js +16 -16
- package/dist/svgm.min.js +60 -60
- package/dist/version.json +44 -16
- package/package.json +11 -3
- package/src/bezier-intersections.js +1 -1
- package/src/browser-verify.js +0 -1
- package/src/clip-path-resolver.js +3 -1
- package/src/font-manager.js +1013 -0
- package/src/index.js +2 -2
- package/src/inkscape-support.js +2 -2
- package/src/mask-resolver.js +14 -6
- package/src/mesh-gradient.js +0 -2
- package/src/off-canvas-detection.js +14 -22
- package/src/svg-boolean-ops.js +0 -5
- package/src/svg-collections.js +11 -0
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-parser.js +0 -24
- package/src/svg-rendering-context.js +2 -4
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svgm-lib.js +2 -2
- package/src/transform-optimization.js +93 -142
- package/src/verification.js +0 -2
- package/templates/svgm_replacement_map.yml +53 -0
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* SVG path conversion, and 2D/3D affine transformations using Decimal.js.
|
|
6
6
|
*
|
|
7
7
|
* @module @emasoft/svg-matrix
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.3.0
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
@@ -135,7 +135,7 @@ Decimal.set({ precision: 80 });
|
|
|
135
135
|
* Library version
|
|
136
136
|
* @constant {string}
|
|
137
137
|
*/
|
|
138
|
-
export const VERSION = '1.
|
|
138
|
+
export const VERSION = '1.3.0';
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Default precision for path output (decimal places)
|
package/src/inkscape-support.js
CHANGED
|
@@ -215,8 +215,8 @@ export function getNodeTypes(element) {
|
|
|
215
215
|
const nodeTypes = element.getAttribute("sodipodi:nodetypes");
|
|
216
216
|
if (!nodeTypes) return null;
|
|
217
217
|
|
|
218
|
-
// Validate format: should only contain c, s, z, a characters
|
|
219
|
-
if (!/^[csza]
|
|
218
|
+
// Validate format: should only contain c, s, z, a characters (case-sensitive)
|
|
219
|
+
if (!/^[csza]+$/.test(nodeTypes)) return null;
|
|
220
220
|
|
|
221
221
|
return nodeTypes;
|
|
222
222
|
}
|
package/src/mask-resolver.js
CHANGED
|
@@ -44,8 +44,6 @@
|
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
import Decimal from "decimal.js";
|
|
47
|
-
import { Matrix as _Matrix } from "./matrix.js";
|
|
48
|
-
import * as _Transforms2D from "./transforms2d.js";
|
|
49
47
|
import * as PolygonClip from "./polygon-clip.js";
|
|
50
48
|
import * as ClipPathResolver from "./clip-path-resolver.js";
|
|
51
49
|
import * as MeshGradient from "./mesh-gradient.js";
|
|
@@ -1721,10 +1719,15 @@ export function clipWithMeshGradientShape(
|
|
|
1721
1719
|
}
|
|
1722
1720
|
|
|
1723
1721
|
// Union all patch polygons to get the complete mesh shape
|
|
1724
|
-
|
|
1725
|
-
if (
|
|
1722
|
+
// Validate first polygon before use
|
|
1723
|
+
if (
|
|
1724
|
+
!meshPolygons[0] ||
|
|
1725
|
+
!meshPolygons[0].polygon ||
|
|
1726
|
+
meshPolygons[0].polygon.length < 3
|
|
1727
|
+
) {
|
|
1726
1728
|
return [];
|
|
1727
1729
|
}
|
|
1730
|
+
let meshShape = meshPolygons[0].polygon;
|
|
1728
1731
|
|
|
1729
1732
|
for (let i = 1; i < meshPolygons.length; i++) {
|
|
1730
1733
|
if (
|
|
@@ -1817,10 +1820,15 @@ export function meshGradientToClipPath(meshData, options = {}) {
|
|
|
1817
1820
|
}
|
|
1818
1821
|
|
|
1819
1822
|
// Union all patches into one shape
|
|
1820
|
-
|
|
1821
|
-
if (
|
|
1823
|
+
// Validate first polygon before use
|
|
1824
|
+
if (
|
|
1825
|
+
!meshPolygons[0] ||
|
|
1826
|
+
!meshPolygons[0].polygon ||
|
|
1827
|
+
meshPolygons[0].polygon.length < 3
|
|
1828
|
+
) {
|
|
1822
1829
|
return [];
|
|
1823
1830
|
}
|
|
1831
|
+
let result = meshPolygons[0].polygon;
|
|
1824
1832
|
|
|
1825
1833
|
for (let i = 1; i < meshPolygons.length; i++) {
|
|
1826
1834
|
if (!meshPolygons[i] || !meshPolygons[i].polygon) continue; // Skip invalid polygons
|
package/src/mesh-gradient.js
CHANGED
|
@@ -795,7 +795,7 @@ export function pathBoundingBox(pathCommands) {
|
|
|
795
795
|
}
|
|
796
796
|
break;
|
|
797
797
|
|
|
798
|
-
case "A": // Arc (
|
|
798
|
+
case "A": // Arc (conservative bounding box using extrema bounds)
|
|
799
799
|
{
|
|
800
800
|
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
801
801
|
if (
|
|
@@ -825,30 +825,22 @@ export function pathBoundingBox(pathCommands) {
|
|
|
825
825
|
maxY = Decimal.max(maxY, currentY, y);
|
|
826
826
|
samplePoints.push({ x: currentX, y: currentY }, { x, y });
|
|
827
827
|
} else {
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
// For now, conservatively expand bbox by radii to ensure containment
|
|
840
|
-
// This may overestimate slightly but guarantees correctness
|
|
841
|
-
const centerX = currentX.plus(x).div(2);
|
|
842
|
-
const centerY = currentY.plus(y).div(2);
|
|
843
|
-
minX = Decimal.min(minX, centerX.minus(rx));
|
|
844
|
-
minY = Decimal.min(minY, centerY.minus(ry));
|
|
845
|
-
maxX = Decimal.max(maxX, centerX.plus(rx));
|
|
846
|
-
maxY = Decimal.max(maxY, centerY.plus(ry));
|
|
847
|
-
|
|
848
|
-
// Sample arc for verification points
|
|
828
|
+
// FIX: Use conservative bounds that guarantee containment (WHY: exact arc center requires complex calculations)
|
|
829
|
+
// The tightest conservative bound without full arc parameterization is to
|
|
830
|
+
// consider all possible extrema within a box of size 2*rx by 2*ry centered at each endpoint
|
|
831
|
+
// This ensures we never underestimate the bbox, though we may slightly overestimate
|
|
832
|
+
minX = Decimal.min(minX, currentX, x, currentX.minus(rx), x.minus(rx));
|
|
833
|
+
minY = Decimal.min(minY, currentY, y, currentY.minus(ry), y.minus(ry));
|
|
834
|
+
maxX = Decimal.max(maxX, currentX, x, currentX.plus(rx), x.plus(rx));
|
|
835
|
+
maxY = Decimal.max(maxY, currentY, y, currentY.plus(ry), y.plus(ry));
|
|
836
|
+
|
|
837
|
+
// Sample arc for verification: use parametric sampling along the arc
|
|
838
|
+
// Note: This is still an approximation, but better than linear interpolation
|
|
849
839
|
const samples = 20;
|
|
850
840
|
for (let i = 0; i <= samples; i++) {
|
|
851
841
|
const t = D(i).div(samples);
|
|
842
|
+
// Simple parametric interpolation (approximation)
|
|
843
|
+
// For exact results, we'd need to convert to center parameterization
|
|
852
844
|
const px = currentX.plus(x.minus(currentX).mul(t));
|
|
853
845
|
const py = currentY.plus(y.minus(currentY).mul(t));
|
|
854
846
|
samplePoints.push({ x: px, y: py });
|
package/src/svg-boolean-ops.js
CHANGED
|
@@ -23,15 +23,10 @@ const EPSILON = new Decimal("1e-40");
|
|
|
23
23
|
|
|
24
24
|
const {
|
|
25
25
|
point,
|
|
26
|
-
pointsEqual: _pointsEqual,
|
|
27
26
|
cross,
|
|
28
|
-
polygonArea: _polygonArea,
|
|
29
27
|
polygonIntersection,
|
|
30
28
|
polygonUnion,
|
|
31
29
|
polygonDifference,
|
|
32
|
-
isCounterClockwise: _isCounterClockwise,
|
|
33
|
-
ensureCCW: _ensureCCW,
|
|
34
|
-
segmentIntersection: _segmentIntersection,
|
|
35
30
|
} = PolygonClip;
|
|
36
31
|
|
|
37
32
|
// ============================================================================
|
package/src/svg-collections.js
CHANGED
|
@@ -459,6 +459,7 @@ export const colorsProps = new Set([
|
|
|
459
459
|
|
|
460
460
|
/**
|
|
461
461
|
* Complete list of known SVG elements
|
|
462
|
+
* Includes both canonical names and lowercase variants for case-insensitive matching
|
|
462
463
|
*/
|
|
463
464
|
export const knownElements = new Set([
|
|
464
465
|
// Structural elements
|
|
@@ -487,8 +488,13 @@ export const knownElements = new Set([
|
|
|
487
488
|
"glyphRef",
|
|
488
489
|
// Gradient elements
|
|
489
490
|
"linearGradient",
|
|
491
|
+
"lineargradient", // lowercase variant for case-insensitive matching
|
|
490
492
|
"radialGradient",
|
|
493
|
+
"radialgradient", // lowercase variant for case-insensitive matching
|
|
491
494
|
"meshGradient",
|
|
495
|
+
"meshgradient", // lowercase variant for case-insensitive matching
|
|
496
|
+
"meshrow", // SVG 2.0 mesh gradient row
|
|
497
|
+
"meshpatch", // SVG 2.0 mesh gradient patch
|
|
492
498
|
"stop",
|
|
493
499
|
// Container elements
|
|
494
500
|
"a",
|
|
@@ -496,6 +502,7 @@ export const knownElements = new Set([
|
|
|
496
502
|
"mask",
|
|
497
503
|
"pattern",
|
|
498
504
|
"clipPath",
|
|
505
|
+
"clippath", // lowercase variant for case-insensitive matching
|
|
499
506
|
"switch",
|
|
500
507
|
// Filter elements
|
|
501
508
|
"filter",
|
|
@@ -531,8 +538,11 @@ export const knownElements = new Set([
|
|
|
531
538
|
// Animation elements
|
|
532
539
|
"animate",
|
|
533
540
|
"animateColor",
|
|
541
|
+
"animatecolor", // lowercase variant for case-insensitive matching
|
|
534
542
|
"animateMotion",
|
|
543
|
+
"animatemotion", // lowercase variant for case-insensitive matching
|
|
535
544
|
"animateTransform",
|
|
545
|
+
"animatetransform", // lowercase variant for case-insensitive matching
|
|
536
546
|
"set",
|
|
537
547
|
"mpath",
|
|
538
548
|
// Other elements
|
|
@@ -542,6 +552,7 @@ export const knownElements = new Set([
|
|
|
542
552
|
"style",
|
|
543
553
|
"script",
|
|
544
554
|
"solidColor",
|
|
555
|
+
"solidcolor", // lowercase variant for case-insensitive matching
|
|
545
556
|
"hatch",
|
|
546
557
|
"hatchpath",
|
|
547
558
|
]);
|
package/src/svg-matrix-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Works in both Node.js and browser environments.
|
|
6
6
|
*
|
|
7
7
|
* @module svg-matrix-lib
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.3.0
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -32,7 +32,7 @@ Decimal.set({ precision: 80 });
|
|
|
32
32
|
/**
|
|
33
33
|
* Library version
|
|
34
34
|
*/
|
|
35
|
-
export const VERSION = "1.
|
|
35
|
+
export const VERSION = "1.3.0";
|
|
36
36
|
|
|
37
37
|
// Export core classes
|
|
38
38
|
export { Decimal, Matrix, Vector };
|
package/src/svg-parser.js
CHANGED
|
@@ -666,30 +666,6 @@ export class SVGElement {
|
|
|
666
666
|
// INTERNAL PARSING FUNCTIONS
|
|
667
667
|
// ============================================================================
|
|
668
668
|
|
|
669
|
-
/**
|
|
670
|
-
* Check if whitespace should be preserved based on xml:space attribute.
|
|
671
|
-
* BUG FIX 2: Helper function to check xml:space="preserve" on element or ancestors
|
|
672
|
-
* @private
|
|
673
|
-
* @param {SVGElement} element - Element to check for xml:space attribute
|
|
674
|
-
* @returns {boolean} True if whitespace should be preserved, false otherwise
|
|
675
|
-
*/
|
|
676
|
-
function _shouldPreserveWhitespace(element) {
|
|
677
|
-
// Validation: Ensure element is not null/undefined
|
|
678
|
-
if (!element) {
|
|
679
|
-
throw new Error(
|
|
680
|
-
"_shouldPreserveWhitespace: element cannot be null or undefined",
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
let current = element;
|
|
684
|
-
while (current) {
|
|
685
|
-
const xmlSpace = current.getAttribute("xml:space");
|
|
686
|
-
if (xmlSpace === "preserve") return true;
|
|
687
|
-
if (xmlSpace === "default") return false;
|
|
688
|
-
current = current.parentNode;
|
|
689
|
-
}
|
|
690
|
-
return false;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
669
|
/**
|
|
694
670
|
* Parse a single element from SVG string.
|
|
695
671
|
* @private
|
|
@@ -31,10 +31,8 @@ import Decimal from "decimal.js";
|
|
|
31
31
|
import {
|
|
32
32
|
FillRule,
|
|
33
33
|
pointInPolygonWithRule,
|
|
34
|
-
offsetPolygon as _offsetPolygon,
|
|
35
34
|
strokeToFilledPolygon,
|
|
36
35
|
} from "./svg-boolean-ops.js";
|
|
37
|
-
import * as _PolygonClip from "./polygon-clip.js";
|
|
38
36
|
|
|
39
37
|
Decimal.set({ precision: 80 });
|
|
40
38
|
|
|
@@ -353,9 +351,9 @@ export class SVGRenderingContext {
|
|
|
353
351
|
extent = halfWidth.times(this.strokeMiterlimit);
|
|
354
352
|
}
|
|
355
353
|
|
|
356
|
-
// Square linecaps extend
|
|
354
|
+
// Square linecaps extend strokeWidth/2 beyond endpoints - Why: total extent is base strokeWidth/2 + cap extension strokeWidth/2 = strokeWidth
|
|
357
355
|
if (this.strokeLinecap === "square") {
|
|
358
|
-
const capExtent = halfWidth.times(2); // strokeWidth
|
|
356
|
+
const capExtent = halfWidth.times(2); // Total extent: strokeWidth (base halfWidth + cap halfWidth)
|
|
359
357
|
if (capExtent.gt(extent)) {
|
|
360
358
|
extent = capExtent;
|
|
361
359
|
}
|
package/src/svg-toolbox-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Provides 69+ operations for cleaning, optimizing, and transforming SVG files.
|
|
6
6
|
*
|
|
7
7
|
* @module svg-toolbox-lib
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.3.0
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -34,7 +34,7 @@ import * as SVGToolboxModule from "./svg-toolbox.js";
|
|
|
34
34
|
/**
|
|
35
35
|
* Library version
|
|
36
36
|
*/
|
|
37
|
-
export const VERSION = "1.
|
|
37
|
+
export const VERSION = "1.3.0";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Default export for browser global (window.SVGToolbox)
|
package/src/svgm-lib.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* comprehensive SVG manipulation (SVGToolbox). Works in Node.js and browser.
|
|
6
6
|
*
|
|
7
7
|
* @module svgm-lib
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.3.0
|
|
9
9
|
* @license MIT
|
|
10
10
|
*
|
|
11
11
|
* @example Browser usage:
|
|
@@ -49,7 +49,7 @@ Decimal.set({ precision: 80 });
|
|
|
49
49
|
/**
|
|
50
50
|
* Library version
|
|
51
51
|
*/
|
|
52
|
-
export const VERSION = "1.
|
|
52
|
+
export const VERSION = "1.3.0";
|
|
53
53
|
|
|
54
54
|
// Export math classes
|
|
55
55
|
export { Decimal, Matrix, Vector };
|
|
@@ -196,6 +196,97 @@ export function matricesEqual(m1, m2, tolerance = VERIFICATION_TOLERANCE) {
|
|
|
196
196
|
return matrixMaxDifference(m1, m2).lessThan(D(tolerance));
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Compute the combined matrix from a list of transform objects.
|
|
201
|
+
* Used internally for verification that optimizations preserve the transform result.
|
|
202
|
+
*
|
|
203
|
+
* @param {Array<Object>} transforms - Array of transform objects with type and params
|
|
204
|
+
* @returns {Matrix} Combined 3x3 transformation matrix
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* const transforms = [
|
|
208
|
+
* { type: 'translate', params: { tx: 10, ty: 20 } },
|
|
209
|
+
* { type: 'rotate', params: { angle: 0.5 } }
|
|
210
|
+
* ];
|
|
211
|
+
* const combined = computeCombinedMatrix(transforms);
|
|
212
|
+
*/
|
|
213
|
+
export function computeCombinedMatrix(transforms) {
|
|
214
|
+
let combined = identityMatrix();
|
|
215
|
+
|
|
216
|
+
for (const t of transforms) {
|
|
217
|
+
// Validate transform object structure
|
|
218
|
+
if (
|
|
219
|
+
!t ||
|
|
220
|
+
typeof t !== "object" ||
|
|
221
|
+
!t.type ||
|
|
222
|
+
!t.params ||
|
|
223
|
+
typeof t.params !== "object"
|
|
224
|
+
) {
|
|
225
|
+
continue; // Skip malformed transforms
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let m = null;
|
|
229
|
+
switch (t.type) {
|
|
230
|
+
case "translate":
|
|
231
|
+
if (
|
|
232
|
+
t.params.tx === null ||
|
|
233
|
+
t.params.tx === undefined ||
|
|
234
|
+
t.params.ty === null ||
|
|
235
|
+
t.params.ty === undefined
|
|
236
|
+
) {
|
|
237
|
+
continue; // Skip transforms with missing params
|
|
238
|
+
}
|
|
239
|
+
m = translationMatrix(t.params.tx, t.params.ty);
|
|
240
|
+
break;
|
|
241
|
+
case "rotate":
|
|
242
|
+
if (t.params.angle === null || t.params.angle === undefined) {
|
|
243
|
+
continue; // Skip transforms with missing angle
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
t.params.cx !== undefined &&
|
|
247
|
+
t.params.cx !== null &&
|
|
248
|
+
t.params.cy !== undefined &&
|
|
249
|
+
t.params.cy !== null
|
|
250
|
+
) {
|
|
251
|
+
m = rotationMatrixAroundPoint(
|
|
252
|
+
t.params.angle,
|
|
253
|
+
t.params.cx,
|
|
254
|
+
t.params.cy,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
m = rotationMatrix(t.params.angle);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
case "scale":
|
|
261
|
+
if (
|
|
262
|
+
t.params.sx === null ||
|
|
263
|
+
t.params.sx === undefined ||
|
|
264
|
+
t.params.sy === null ||
|
|
265
|
+
t.params.sy === undefined
|
|
266
|
+
) {
|
|
267
|
+
continue; // Skip transforms with missing params
|
|
268
|
+
}
|
|
269
|
+
m = scaleMatrix(t.params.sx, t.params.sy);
|
|
270
|
+
break;
|
|
271
|
+
case "matrix":
|
|
272
|
+
if (!t.params.matrix) {
|
|
273
|
+
continue; // Skip transforms with missing matrix
|
|
274
|
+
}
|
|
275
|
+
m = t.params.matrix;
|
|
276
|
+
break;
|
|
277
|
+
default:
|
|
278
|
+
// Skip unknown transform types
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (m !== null) {
|
|
283
|
+
combined = combined.mul(m);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return combined;
|
|
288
|
+
}
|
|
289
|
+
|
|
199
290
|
// ============================================================================
|
|
200
291
|
// Transform Merging Functions
|
|
201
292
|
// ============================================================================
|
|
@@ -920,77 +1011,7 @@ export function optimizeTransformList(transforms) {
|
|
|
920
1011
|
}
|
|
921
1012
|
|
|
922
1013
|
// Calculate original combined matrix for verification
|
|
923
|
-
|
|
924
|
-
for (const t of transforms) {
|
|
925
|
-
// Validate transform object structure
|
|
926
|
-
if (
|
|
927
|
-
!t ||
|
|
928
|
-
typeof t !== "object" ||
|
|
929
|
-
!t.type ||
|
|
930
|
-
!t.params ||
|
|
931
|
-
typeof t.params !== "object"
|
|
932
|
-
) {
|
|
933
|
-
continue; // Skip malformed transforms
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
let m = null; // Initialize m to null to catch missing assignments
|
|
937
|
-
switch (t.type) {
|
|
938
|
-
case "translate":
|
|
939
|
-
if (
|
|
940
|
-
t.params.tx === null ||
|
|
941
|
-
t.params.tx === undefined ||
|
|
942
|
-
t.params.ty === null ||
|
|
943
|
-
t.params.ty === undefined
|
|
944
|
-
) {
|
|
945
|
-
continue; // Skip transforms with missing params
|
|
946
|
-
}
|
|
947
|
-
m = translationMatrix(t.params.tx, t.params.ty);
|
|
948
|
-
break;
|
|
949
|
-
case "rotate":
|
|
950
|
-
if (t.params.angle === null || t.params.angle === undefined) {
|
|
951
|
-
continue; // Skip transforms with missing angle
|
|
952
|
-
}
|
|
953
|
-
if (
|
|
954
|
-
t.params.cx !== undefined &&
|
|
955
|
-
t.params.cx !== null &&
|
|
956
|
-
t.params.cy !== undefined &&
|
|
957
|
-
t.params.cy !== null
|
|
958
|
-
) {
|
|
959
|
-
m = rotationMatrixAroundPoint(
|
|
960
|
-
t.params.angle,
|
|
961
|
-
t.params.cx,
|
|
962
|
-
t.params.cy,
|
|
963
|
-
);
|
|
964
|
-
} else {
|
|
965
|
-
m = rotationMatrix(t.params.angle);
|
|
966
|
-
}
|
|
967
|
-
break;
|
|
968
|
-
case "scale":
|
|
969
|
-
if (
|
|
970
|
-
t.params.sx === null ||
|
|
971
|
-
t.params.sx === undefined ||
|
|
972
|
-
t.params.sy === null ||
|
|
973
|
-
t.params.sy === undefined
|
|
974
|
-
) {
|
|
975
|
-
continue; // Skip transforms with missing params
|
|
976
|
-
}
|
|
977
|
-
m = scaleMatrix(t.params.sx, t.params.sy);
|
|
978
|
-
break;
|
|
979
|
-
case "matrix":
|
|
980
|
-
if (!t.params.matrix) {
|
|
981
|
-
continue; // Skip transforms with missing matrix
|
|
982
|
-
}
|
|
983
|
-
m = t.params.matrix;
|
|
984
|
-
break;
|
|
985
|
-
default:
|
|
986
|
-
// Skip unknown transform types, but don't try to multiply null matrix
|
|
987
|
-
continue;
|
|
988
|
-
}
|
|
989
|
-
// Only multiply if m was successfully assigned (prevents undefined matrix multiplication)
|
|
990
|
-
if (m !== null) {
|
|
991
|
-
originalMatrix = originalMatrix.mul(m);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
1014
|
+
const originalMatrix = computeCombinedMatrix(transforms);
|
|
994
1015
|
|
|
995
1016
|
// Step 1: Remove identity transforms
|
|
996
1017
|
const { transforms: step1, removedCount: _removedCount } =
|
|
@@ -1210,77 +1231,7 @@ export function optimizeTransformList(transforms) {
|
|
|
1210
1231
|
const { transforms: final } = removeIdentityTransforms(optimized);
|
|
1211
1232
|
|
|
1212
1233
|
// Calculate optimized combined matrix for verification
|
|
1213
|
-
|
|
1214
|
-
for (const t of final) {
|
|
1215
|
-
// Validate transform object structure
|
|
1216
|
-
if (
|
|
1217
|
-
!t ||
|
|
1218
|
-
typeof t !== "object" ||
|
|
1219
|
-
!t.type ||
|
|
1220
|
-
!t.params ||
|
|
1221
|
-
typeof t.params !== "object"
|
|
1222
|
-
) {
|
|
1223
|
-
continue; // Skip malformed transforms
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
let m = null; // Initialize m to null to catch missing assignments
|
|
1227
|
-
switch (t.type) {
|
|
1228
|
-
case "translate":
|
|
1229
|
-
if (
|
|
1230
|
-
t.params.tx === null ||
|
|
1231
|
-
t.params.tx === undefined ||
|
|
1232
|
-
t.params.ty === null ||
|
|
1233
|
-
t.params.ty === undefined
|
|
1234
|
-
) {
|
|
1235
|
-
continue; // Skip transforms with missing params
|
|
1236
|
-
}
|
|
1237
|
-
m = translationMatrix(t.params.tx, t.params.ty);
|
|
1238
|
-
break;
|
|
1239
|
-
case "rotate":
|
|
1240
|
-
if (t.params.angle === null || t.params.angle === undefined) {
|
|
1241
|
-
continue; // Skip transforms with missing angle
|
|
1242
|
-
}
|
|
1243
|
-
if (
|
|
1244
|
-
t.params.cx !== undefined &&
|
|
1245
|
-
t.params.cx !== null &&
|
|
1246
|
-
t.params.cy !== undefined &&
|
|
1247
|
-
t.params.cy !== null
|
|
1248
|
-
) {
|
|
1249
|
-
m = rotationMatrixAroundPoint(
|
|
1250
|
-
t.params.angle,
|
|
1251
|
-
t.params.cx,
|
|
1252
|
-
t.params.cy,
|
|
1253
|
-
);
|
|
1254
|
-
} else {
|
|
1255
|
-
m = rotationMatrix(t.params.angle);
|
|
1256
|
-
}
|
|
1257
|
-
break;
|
|
1258
|
-
case "scale":
|
|
1259
|
-
if (
|
|
1260
|
-
t.params.sx === null ||
|
|
1261
|
-
t.params.sx === undefined ||
|
|
1262
|
-
t.params.sy === null ||
|
|
1263
|
-
t.params.sy === undefined
|
|
1264
|
-
) {
|
|
1265
|
-
continue; // Skip transforms with missing params
|
|
1266
|
-
}
|
|
1267
|
-
m = scaleMatrix(t.params.sx, t.params.sy);
|
|
1268
|
-
break;
|
|
1269
|
-
case "matrix":
|
|
1270
|
-
if (!t.params.matrix) {
|
|
1271
|
-
continue; // Skip transforms with missing matrix
|
|
1272
|
-
}
|
|
1273
|
-
m = t.params.matrix;
|
|
1274
|
-
break;
|
|
1275
|
-
default:
|
|
1276
|
-
// Skip unknown transform types, but don't try to multiply null matrix
|
|
1277
|
-
continue;
|
|
1278
|
-
}
|
|
1279
|
-
// Only multiply if m was successfully assigned (prevents undefined matrix multiplication)
|
|
1280
|
-
if (m !== null) {
|
|
1281
|
-
optimizedMatrix = optimizedMatrix.mul(m);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1234
|
+
const optimizedMatrix = computeCombinedMatrix(final);
|
|
1284
1235
|
|
|
1285
1236
|
// VERIFICATION: Combined matrices must be equal
|
|
1286
1237
|
const maxError = matrixMaxDifference(originalMatrix, optimizedMatrix);
|
package/src/verification.js
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SVG Font Replacement Map
|
|
2
|
+
# ========================
|
|
3
|
+
# This file defines font replacements for SVG processing.
|
|
4
|
+
#
|
|
5
|
+
# Format:
|
|
6
|
+
# original_font: replacement_font
|
|
7
|
+
#
|
|
8
|
+
# Examples:
|
|
9
|
+
# "Arial": "Inter" # Replace Arial with Inter
|
|
10
|
+
# "Times New Roman": "Noto Serif"
|
|
11
|
+
#
|
|
12
|
+
# Font sources (in priority order):
|
|
13
|
+
# 1. Local system fonts
|
|
14
|
+
# 2. Google Fonts (default, free)
|
|
15
|
+
# 3. FontGet (npm: fontget)
|
|
16
|
+
# 4. fnt (brew: alexmyczko/fnt/fnt)
|
|
17
|
+
#
|
|
18
|
+
# Options per font:
|
|
19
|
+
# embed: true # Embed as base64 (default: true)
|
|
20
|
+
# subset: true # Only include used glyphs (default: true)
|
|
21
|
+
# source: "google" # Force specific source
|
|
22
|
+
# weight: "400,700" # Specific weights to include
|
|
23
|
+
# style: "normal,italic" # Specific styles
|
|
24
|
+
#
|
|
25
|
+
# Advanced format:
|
|
26
|
+
# "Arial":
|
|
27
|
+
# replacement: "Inter"
|
|
28
|
+
# embed: true
|
|
29
|
+
# subset: true
|
|
30
|
+
# source: "google"
|
|
31
|
+
# weights: ["400", "500", "700"]
|
|
32
|
+
|
|
33
|
+
replacements:
|
|
34
|
+
# Common font replacements
|
|
35
|
+
# "Arial": "Inter"
|
|
36
|
+
# "Helvetica": "Inter"
|
|
37
|
+
# "Times New Roman": "Noto Serif"
|
|
38
|
+
# "Times": "Noto Serif"
|
|
39
|
+
# "Courier New": "Fira Code"
|
|
40
|
+
# "Courier": "Fira Code"
|
|
41
|
+
# "Georgia": "Merriweather"
|
|
42
|
+
# "Verdana": "Open Sans"
|
|
43
|
+
# "Comic Sans MS": "Comic Neue"
|
|
44
|
+
|
|
45
|
+
# Icon fonts
|
|
46
|
+
# "Font Awesome": "Font Awesome 6 Free"
|
|
47
|
+
# "Material Icons": "Material Symbols Outlined"
|
|
48
|
+
|
|
49
|
+
options:
|
|
50
|
+
default_embed: true
|
|
51
|
+
default_subset: true
|
|
52
|
+
fallback_source: "google"
|
|
53
|
+
auto_download: true
|