svg-path-simplify 0.3.6 → 0.4.1

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.
@@ -123,9 +123,16 @@
123
123
  } = Math;
124
124
 
125
125
  const rad2Deg = 180/Math.PI;
126
+ const deg2rad = Math.PI/180;
126
127
  const root2 = 1.4142135623730951;
127
128
  const svgNs = 'http://www.w3.org/2000/svg';
128
129
 
130
+ // 1/2.54
131
+ const inch2cm = 0.39370078;
132
+
133
+ // 1/72
134
+ const inch2pt = 0.01388889;
135
+
129
136
  /*
130
137
  import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
131
138
  log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
@@ -1472,6 +1479,118 @@
1472
1479
  return segmentPoints;
1473
1480
  }
1474
1481
 
1482
+ function parseColor(str) {
1483
+ let type = str.startsWith('#') ? 'rgbHex' : (str.includes('(') ? 'fn' : typeof str);
1484
+ let col = {};
1485
+ let mode = null;
1486
+ let colObj = { mode: null, values: [] };
1487
+ if (type === 'rgbHex') {
1488
+ col = hex2Rgb(str);
1489
+ mode = 'rgba';
1490
+ }
1491
+ else if (type === 'fn') {
1492
+ let colVals = str.split(/\(|\)/).filter(Boolean);
1493
+ if (colVals.length < 2) return str;
1494
+
1495
+ mode = colVals[0];
1496
+ let colorComponents = colVals[1].split(/,| /).filter(Boolean).map(Number);
1497
+
1498
+ let keys = mode.split('');
1499
+ keys.forEach((k, i) => {
1500
+ let val = colorComponents[i];
1501
+ if (mode === 'rgba' && k === 'a') {
1502
+ val = Math.floor(val * 255);
1503
+ }
1504
+ col[k] = val;
1505
+ });
1506
+ }
1507
+ else if (type === 'string') {
1508
+ colObj.mode = 'keyword';
1509
+ colObj.values = [str];
1510
+ return colObj
1511
+ }
1512
+
1513
+ if (mode === 'rgba' || mode === 'rgb') {
1514
+ col.a = !col.a ? 255 : col.a;
1515
+ }
1516
+
1517
+ colObj.mode = mode;
1518
+ colObj.values = Object.values(col);
1519
+
1520
+ return colObj;
1521
+ }
1522
+
1523
+ function hex2Rgb(hex = '') {
1524
+ // Remove # if present
1525
+ if (hex.startsWith('#')) hex = hex.substring(1);
1526
+
1527
+ // normalize short notation (e.g., 'fff' or 'ffff')
1528
+ if (hex.length === 3) {
1529
+ hex = hex.split('').map(char => char + char).join('');
1530
+ } else if (hex.length === 4) {
1531
+ // Handle short notation with alpha (e.g., 'ffff')
1532
+ hex = hex.split('').map(char => char + char).join('');
1533
+ }
1534
+
1535
+ let r = 0, g = 0, b = 0, a = 0;
1536
+
1537
+ // invalid
1538
+ if (hex.length < 6 || hex.length > 8) {
1539
+ console.warn('Invalid hex format');
1540
+ return { r, g, b, a };
1541
+ }
1542
+
1543
+ let isRgba = hex.length === 8;
1544
+
1545
+ let numericValue = parseInt(hex, 16);
1546
+ r = isRgba ? parseInt(hex.substring(0, 2), 16) : numericValue >> 16 & 0xFF;
1547
+ g = isRgba ? parseInt(hex.substring(2, 4), 16) : numericValue >> 8 & 0xFF;
1548
+ b = isRgba ? parseInt(hex.substring(4, 6), 16) : numericValue & 0xFF;
1549
+ a = isRgba ? parseInt(hex.substring(6, 8), 16) : 255;
1550
+
1551
+ return { r, g, b, a };
1552
+
1553
+ }
1554
+
1555
+ function rgba2Hex({ r = 0, g = 0, b = 0, a = 255, values = [] }) {
1556
+ // Helper function to convert number to 2-digit hex
1557
+ const toHex = (num) => {
1558
+ const hex = Math.min(255, Math.max(0, Math.round(num))).toString(16);
1559
+ return hex.length === 1 ? '0' + hex : hex;
1560
+ };
1561
+
1562
+ // convert from number array input
1563
+ if (!r && !g && !b && values.length) {
1564
+ [r, g, b, a = 255] = values;
1565
+ }
1566
+
1567
+ // Get hex values
1568
+ let rHex = toHex(r);
1569
+ let gHex = toHex(g);
1570
+ let bHex = toHex(b);
1571
+ let aHex = a < 255 ? toHex(a) : 0;
1572
+
1573
+ let allowsShort = rHex[0] === rHex[1] && gHex[0] === gHex[1] && bHex[0] === bHex[1];
1574
+
1575
+ // Check for 3-character RGB short notation (e.g., #fff)
1576
+ if (!aHex && allowsShort) {
1577
+ return `#${rHex[0]}${gHex[0]}${bHex[0]}`;
1578
+ }
1579
+
1580
+ // Check for 4-character RGBA short notation (e.g., #ffff)
1581
+ if (aHex && allowsShort) {
1582
+ return `#${rHex[0]}${gHex[0]}${bHex[0]}${aHex[0]}`;
1583
+ }
1584
+
1585
+ // Return 6-character RGB if no alpha
1586
+ if (!aHex) {
1587
+ return `#${rHex}${gHex}${bHex}`;
1588
+ }
1589
+
1590
+ // Return 8-character RGBA
1591
+ return `#${rHex}${gHex}${bHex}${aHex}`;
1592
+ }
1593
+
1475
1594
  function detectAccuracyPoly(pts) {
1476
1595
  let dims = [];
1477
1596
 
@@ -1606,12 +1725,23 @@
1606
1725
 
1607
1726
  const horizontalProps = ['x', 'cx', 'rx', 'dx', 'width', 'translateX'];
1608
1727
  const verticalProps = ['y', 'cy', 'ry', 'dy', 'height', 'translateY'];
1728
+ const transHorizontal = ['scaleX', 'translateX', 'skewX'];
1729
+ const transVertical = ['scaleY', 'translateY', 'skewY'];
1730
+
1731
+ const colorProps = ['fill', 'stroke', 'stop-color'];
1609
1732
 
1610
1733
  const geometryEls = [
1611
1734
  "path",
1612
1735
  ...shapeEls
1613
1736
  ];
1614
1737
 
1738
+ const renderedEls = [
1739
+ "text",
1740
+ "textPath",
1741
+ "tspan",
1742
+ ...geometryEls
1743
+ ];
1744
+
1615
1745
  const textEls = [
1616
1746
  "textPath",
1617
1747
  "text",
@@ -1877,7 +2007,7 @@
1877
2007
 
1878
2008
  defaults: {
1879
2009
 
1880
- transform: ["none", "matrix(1, 0, 0, 1, 0, 0)"],
2010
+ transform: ["none", "matrix(1, 0, 0, 1, 0, 0)", "matrix(1 0 0 1 0 0)"],
1881
2011
  "transform-origin": ["0px, 0px", "0 0"],
1882
2012
  rx: ["0", "0px"],
1883
2013
  ry: ["0", "0px"],
@@ -1888,9 +2018,9 @@
1888
2018
  "color": ["black", "rgb(0, 0, 0)", "rgba(0, 0, 0, 0)", "#000", "#000000"],
1889
2019
 
1890
2020
  stroke: ["none"],
1891
- "stroke-width": ["1", "1px"],
1892
2021
  opacity: ["1"],
1893
2022
  "fill-opacity": ["1"],
2023
+ "stroke-width": ["1", "1px"],
1894
2024
  "stroke-opacity": ["1"],
1895
2025
  "stroke-linecap": ["butt"],
1896
2026
  "stroke-miterlimit": ["4"],
@@ -1978,20 +2108,21 @@
1978
2108
  // only required for circle r normalization when height!=width
1979
2109
  normalizedDiagonal = width === height ? false : normalizedDiagonal;
1980
2110
 
2111
+ let type = typeof value;
1981
2112
  if (!value) return value;
1982
2113
 
1983
2114
  // check if value is string
1984
- let isArray = value.split(/,| /).length>1;
1985
- let isFunction = value.includes('(');
2115
+ let isNum = type === 'number' ? true : isNumericValue(value);
2116
+ let isArray = type === 'string' ? value.split(/,| /).length > 1 : false;
2117
+ let isFunction = type === 'string' ? value.includes('(') : false;
1986
2118
 
1987
- let isNum = isNumericValue(value);
1988
2119
  if (!isNum || isArray || isFunction) return value
1989
2120
 
1990
2121
  // check unit if not specified
1991
2122
  unit = !unit ? getUnit(value) : unit;
1992
2123
 
1993
2124
  let val = parseFloat(value);
1994
- let scale=1;
2125
+ let scale = 1;
1995
2126
  let scaleRoot = Math.sqrt(width * width + height * height) / root2;
1996
2127
 
1997
2128
  // no unit - already pixes/user unit
@@ -2022,15 +2153,31 @@
2022
2153
  case "in":
2023
2154
  scale = dpi;
2024
2155
  break;
2156
+
2025
2157
  case "pt":
2026
- scale = (1 / 72) * dpi;
2158
+ // 1/72
2159
+ scale = dpi * inch2pt;
2160
+ break;
2161
+
2162
+ case "pc":
2163
+ // 1/6
2164
+ scale = dpi * 0.16666667;
2027
2165
  break;
2166
+
2028
2167
  case "cm":
2029
- scale = (1 / 2.54) * dpi;
2168
+ // 1/2.54
2169
+ scale = inch2cm * dpi;
2030
2170
  break;
2031
2171
  case "mm":
2032
- scale = ((1 / 2.54) * dpi) / 10;
2172
+
2173
+ scale = inch2cm * dpi * 0.1;
2174
+ break;
2175
+
2176
+ // has anyone ever used it?
2177
+ case "Q":
2178
+ scale = inch2cm * dpi * 0.025;
2033
2179
  break;
2180
+
2034
2181
  // just a default approximation
2035
2182
  case "em":
2036
2183
  case "rem":
@@ -3080,6 +3227,10 @@
3080
3227
  let pathDataProps = [pathData[0]];
3081
3228
  let len = pathData.length;
3082
3229
 
3230
+ // threshold for corner angles: 10 deg
3231
+
3232
+ // define angle threshold for semi extremes
3233
+
3083
3234
  for (let c = 2; len && c <= len; c++) {
3084
3235
 
3085
3236
  let com = pathData[c - 1];
@@ -3099,7 +3250,7 @@
3099
3250
  (type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p]) :
3100
3251
  ([p0, p]);
3101
3252
  let thresholdLength = dimA * 0.1;
3102
- let threshold = thresholdLength*0.01;
3253
+ let threshold = thresholdLength * 0.01;
3103
3254
 
3104
3255
  // bezier types
3105
3256
  let isBezier = type === 'Q' || type === 'C';
@@ -3116,14 +3267,22 @@
3116
3267
  let dx = type === 'C' ? Math.abs(com.cp2.x - com.p.x) : Math.abs(com.cp1.x - com.p.x);
3117
3268
  let dy = type === 'C' ? Math.abs(com.cp2.y - com.p.y) : Math.abs(com.cp1.y - com.p.y);
3118
3269
 
3119
- let horizontal = (dy === 0 || dy<threshold ) && dx > 0;
3120
- let vertical = (dx === 0 || dx<threshold ) && dy > 0;
3270
+ let horizontal = (dy === 0 || dy <= threshold) && dx > 0;
3271
+ let vertical = (dx === 0 || dx <= threshold) && dy > 0;
3121
3272
 
3122
3273
  if (horizontal || vertical) {
3123
3274
  hasExtremes = true;
3124
3275
  }
3125
3276
 
3126
3277
  // is extreme relative to bounding box
3278
+
3279
+ // (cp1.x===p0.x && cp1.y!==p0.y ) ||
3280
+ if ((cp1.x === p0.x && cp1.y !== p0.y) || (cp1.y === p0.y && cp1.x !== p0.x)) {
3281
+
3282
+ pathDataProps[pathDataProps.length - 1].extreme = true;
3283
+
3284
+ }
3285
+
3127
3286
  if ((p.x === left || p.y === top || p.x === right || p.y === bottom)) {
3128
3287
  hasExtremes = true;
3129
3288
  }
@@ -3133,6 +3292,7 @@
3133
3292
  let couldHaveExtremes = bezierhasExtreme(null, commandPts);
3134
3293
  if (couldHaveExtremes) {
3135
3294
  let tArr = getTatAngles(commandPts);
3295
+
3136
3296
  if (tArr.length && (tArr[0] > 0.2)) {
3137
3297
  hasExtremes = true;
3138
3298
  }
@@ -3187,21 +3347,23 @@
3187
3347
 
3188
3348
  let signChange2 = (areaCpt < 0 && com.cptArea > 0) || (areaCpt > 0 && com.cptArea < 0) ? true : false;
3189
3349
 
3190
- let isCorner=!isFlat && signChange2;
3350
+ let isCorner = !isFlat && signChange2;
3191
3351
  if (isCorner) com.corner = true;
3192
3352
  }
3193
3353
  }
3194
3354
 
3195
- if (debug) {
3355
+ pathDataProps.push(com);
3356
+
3357
+ }
3358
+
3359
+
3360
+ if (debug) {
3361
+ pathDataProps.forEach(com=>{
3196
3362
 
3197
3363
  if (com.directionChange) renderPoint(markers, com.p, 'orange', '1.5%', '0.5');
3198
3364
  if (com.corner) renderPoint(markers, com.p, 'magenta', '1.5%', '0.5');
3199
3365
  if (com.extreme) renderPoint(markers, com.p, 'cyan', '1%', '0.5');
3200
-
3201
- }
3202
-
3203
- pathDataProps.push(com);
3204
-
3366
+ });
3205
3367
  }
3206
3368
 
3207
3369
  let dimA = (width + height) / 2;
@@ -3752,6 +3914,7 @@
3752
3914
  hasShorthands = true,
3753
3915
  hasQuadratics = true,
3754
3916
  hasArcs = true,
3917
+ optimizeArcs = true,
3755
3918
  testTypes = false
3756
3919
 
3757
3920
  } = {}) {
@@ -3771,23 +3934,68 @@
3771
3934
  toRelative = toAbsolute ? false : toRelative;
3772
3935
  toShorthands = toLonghands ? false : toShorthands;
3773
3936
 
3774
- if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3775
-
3776
- if (toShorthands) pathData = pathDataToShorthands(pathData);
3937
+ if (toAbsolute) pathData = pathDataToAbsolute(pathData);
3777
3938
  if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData);
3778
3939
 
3779
- if (toAbsolute) pathData = pathDataToAbsolute(pathData);
3940
+ // minify semicircle radii
3941
+ if (optimizeArcs) pathData = optimizeArcPathData(pathData);
3942
+
3943
+ if (toShorthands) pathData = pathDataToShorthands(pathData);
3780
3944
 
3781
3945
  if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData);
3782
3946
 
3947
+ if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3948
+
3783
3949
  // pre round - before relative conversion to minimize distortions
3784
3950
  if (decimals > -1 && toRelative) pathData = roundPathData(pathData, decimals);
3951
+
3785
3952
  if (toRelative) pathData = pathDataToRelative(pathData);
3786
3953
  if (decimals > -1) pathData = roundPathData(pathData, decimals);
3787
3954
 
3788
3955
  return pathData
3789
3956
  }
3790
3957
 
3958
+ /**
3959
+ *
3960
+ * @param {*} pathData
3961
+ * @returns
3962
+ */
3963
+
3964
+ function optimizeArcPathData(pathData = []) {
3965
+ pathData.forEach((com, i) => {
3966
+ let { type, values } = com;
3967
+ if (type === 'A') {
3968
+ let [rx, ry, largeArc, x, y] = [values[0], values[1], values[3], values[5], values[6]];
3969
+ let comPrev = pathData[i - 1];
3970
+ let [x0, y0] = [comPrev.values[comPrev.values.length - 2], comPrev.values[comPrev.values.length - 1]];
3971
+ let M = { x: x0, y: y0 };
3972
+ let p = { x, y };
3973
+
3974
+ // rx and ry are large enough
3975
+ if (rx >= 1 && (x === x0 || y === y0)) {
3976
+ let diff = Math.abs(rx - ry) / rx;
3977
+
3978
+ // rx~==ry
3979
+ if (diff < 0.01) {
3980
+
3981
+ // test radius against mid point
3982
+ let pMid = interpolate(M, p, 0.5);
3983
+ let distM = getDistance(pMid, M);
3984
+ let rDiff = Math.abs(distM - rx) / rx;
3985
+
3986
+ // half distance between mid and start point should be ~ equal
3987
+ if(rDiff<0.01){
3988
+ pathData[i].values[0] = 1;
3989
+ pathData[i].values[1] = 1;
3990
+ pathData[i].values[2] = 0;
3991
+ }
3992
+ }
3993
+ }
3994
+ }
3995
+ });
3996
+ return pathData;
3997
+ }
3998
+
3791
3999
  /**
3792
4000
  * parse normalized
3793
4001
  */
@@ -4224,7 +4432,7 @@
4224
4432
  let dx1 = (p0.x - cpPrev.x);
4225
4433
  let dy1 = (p0.y - cpPrev.y);
4226
4434
 
4227
- maxDist = getDistManhattan(cpPrev, cpFirst) * 0.025;
4435
+ maxDist = getDistManhattan(cpPrev, cpFirst) * 0.01;
4228
4436
 
4229
4437
  // reflected cp
4230
4438
  let cpR = { x: cpPrev.x + dx1 * 2, y: cpPrev.y + dy1 * 2 };
@@ -5092,6 +5300,324 @@
5092
5300
  return svg;
5093
5301
  }
5094
5302
 
5303
+ /**
5304
+ * scale pathData
5305
+ */
5306
+ function transformPathData(pathData, matrix) {
5307
+
5308
+ // new pathdata
5309
+ let pathDataTrans = [];
5310
+
5311
+ // transform point by 2d matrix
5312
+ const transformPoint = (pt, matrix) => {
5313
+ let { a, b, c, d, e, f } = matrix;
5314
+ let { x, y } = pt;
5315
+ return { x: a * x + c * y + e, y: b * x + d * y + f };
5316
+ };
5317
+
5318
+ const normalizeMatrix = (matrix) => {
5319
+ matrix =
5320
+ typeof matrix === "string"
5321
+ ? (matrix = matrix
5322
+ .replace(/^matrix\(|\)$/g, "")
5323
+ .split(",")
5324
+ .map(Number))
5325
+ : matrix;
5326
+ matrix = !Array.isArray(matrix)
5327
+ ? {
5328
+ a: matrix.a,
5329
+ b: matrix.b,
5330
+ c: matrix.c,
5331
+ d: matrix.d,
5332
+ e: matrix.e,
5333
+ f: matrix.f
5334
+ }
5335
+ : {
5336
+ a: matrix[0],
5337
+ b: matrix[1],
5338
+ c: matrix[2],
5339
+ d: matrix[3],
5340
+ e: matrix[4],
5341
+ f: matrix[5]
5342
+ };
5343
+ return matrix;
5344
+ };
5345
+
5346
+ const transformArc = (p0, values, matrix) => {
5347
+ let [rx, ry, angle, largeArc, sweep, x, y] = values;
5348
+
5349
+ /**
5350
+ * parametrize arc command
5351
+ * to get the actual arc params
5352
+ */
5353
+ let arcData = svgArcToCenterParam(
5354
+ p0.x,
5355
+ p0.y,
5356
+ values[0],
5357
+ values[1],
5358
+ angle,
5359
+ largeArc,
5360
+ sweep,
5361
+ x,
5362
+ y
5363
+ );
5364
+ ({ rx, ry } = arcData);
5365
+ let { a, b, c, d, e, f } = matrix;
5366
+
5367
+ let ellipsetr = transformEllipse(rx, ry, angle, matrix);
5368
+ let p = transformPoint({ x: x, y: y }, matrix);
5369
+
5370
+ // adjust sweep if flipped
5371
+ let denom = a * a + b * b;
5372
+ let scaleX = Math.sqrt(denom);
5373
+ let scaleY = (a * d - c * b) / scaleX;
5374
+
5375
+ let flipX = scaleX < 0 ? true : false;
5376
+ let flipY = scaleY < 0 ? true : false;
5377
+
5378
+ // adjust sweep
5379
+ if (flipX || flipY) {
5380
+ sweep = sweep === 0 ? 1 : 0;
5381
+ }
5382
+
5383
+ return {
5384
+ type: 'A',
5385
+ values: [
5386
+ ellipsetr.rx,
5387
+ ellipsetr.ry,
5388
+ ellipsetr.ax,
5389
+ largeArc,
5390
+ sweep,
5391
+ p.x,
5392
+ p.y]
5393
+ };
5394
+ };
5395
+
5396
+ // normalize matrix input
5397
+ matrix = normalizeMatrix(matrix);
5398
+
5399
+ let matrixStr = [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f]
5400
+ .map((val) => {
5401
+ return +val.toFixed(1);
5402
+ })
5403
+ .join("");
5404
+
5405
+ // no transform: quit
5406
+ if (matrixStr === "100100") {
5407
+
5408
+ return pathData;
5409
+ }
5410
+
5411
+ pathData.forEach((com, i) => {
5412
+ let { type, values } = com;
5413
+ let typeRel = type.toLowerCase();
5414
+ let comPrev = i > 0 ? pathData[i - 1] : pathData[i];
5415
+ let comPrevValues = comPrev.values;
5416
+ let comPrevValuesL = comPrevValues.length;
5417
+ let p0 = {
5418
+ x: comPrevValues[comPrevValuesL - 2],
5419
+ y: comPrevValues[comPrevValuesL - 1]
5420
+ };
5421
+ ({ x: values[values.length - 2], y: values[values.length - 1] });
5422
+ let comT = { type: type, values: [] };
5423
+
5424
+ switch (typeRel) {
5425
+ case "a":
5426
+ comT = transformArc(p0, values, matrix);
5427
+ break;
5428
+
5429
+ default:
5430
+ // all other point based commands
5431
+ if (values.length) {
5432
+ for (let i = 0; i < values.length; i += 2) {
5433
+ let ptTrans = transformPoint(
5434
+ { x: com.values[i], y: com.values[i + 1] },
5435
+ matrix
5436
+ );
5437
+
5438
+ comT.values[i] = ptTrans.x;
5439
+ comT.values[i + 1] = ptTrans.y;
5440
+ }
5441
+ }
5442
+ }
5443
+
5444
+ pathDataTrans.push(comT);
5445
+ });
5446
+
5447
+ return pathDataTrans;
5448
+ }
5449
+
5450
+ /**
5451
+ * Based on: https://github.com/fontello/svgpath/blob/master/lib/ellipse.js
5452
+ * and fork: https://github.com/kpym/SVGPathy/blob/master/lib/ellipse.js
5453
+ */
5454
+
5455
+ function transformEllipse(rx, ry, ax, matrix) {
5456
+ const torad = Math.PI / 180;
5457
+ const epsilon = 1e-7;
5458
+
5459
+ matrix = !Array.isArray(matrix)
5460
+ ? matrix
5461
+ : {
5462
+ a: matrix[0],
5463
+ b: matrix[1],
5464
+ c: matrix[2],
5465
+ d: matrix[3],
5466
+ e: matrix[4],
5467
+ f: matrix[5]
5468
+ };
5469
+
5470
+ // We consider the current ellipse as image of the unit circle
5471
+ // by first scale(rx,ry) and then rotate(ax) ...
5472
+ // So we apply ma = m x rotate(ax) x scale(rx,ry) to the unit circle.
5473
+ let c = Math.cos(ax * torad),
5474
+ s = Math.sin(ax * torad);
5475
+ let ma = [
5476
+ rx * (matrix.a * c + matrix.c * s),
5477
+ rx * (matrix.b * c + matrix.d * s),
5478
+ ry * (-matrix.a * s + matrix.c * c),
5479
+ ry * (-matrix.b * s + matrix.d * c)
5480
+ ];
5481
+
5482
+ // ma * transpose(ma) = [ J L ]
5483
+ // [ L K ]
5484
+ // L is calculated later (if the image is not a circle)
5485
+ let J = ma[0] * ma[0] + ma[2] * ma[2],
5486
+ K = ma[1] * ma[1] + ma[3] * ma[3];
5487
+
5488
+ // the sqrt of the discriminant of the characteristic polynomial of ma * transpose(ma)
5489
+ // this is also the geometric mean of the eigenvalues
5490
+ let D = Math.sqrt(
5491
+ ((ma[0] - ma[3]) * (ma[0] - ma[3]) + (ma[2] + ma[1]) * (ma[2] + ma[1])) *
5492
+ ((ma[0] + ma[3]) * (ma[0] + ma[3]) + (ma[2] - ma[1]) * (ma[2] - ma[1]))
5493
+ );
5494
+
5495
+ // the arithmetic mean of the eigenvalues
5496
+ let JK = (J + K) / 2;
5497
+
5498
+ // check if the image is (almost) a circle
5499
+ if (D <= epsilon) {
5500
+ rx = ry = Math.sqrt(JK);
5501
+ ax = 0;
5502
+ return { rx: rx, ry: ry, ax: ax };
5503
+ }
5504
+
5505
+ // check if ma * transpose(ma) is (almost) diagonal
5506
+ if (Math.abs(D - Math.abs(J - K)) <= epsilon) {
5507
+ rx = Math.sqrt(J);
5508
+ ry = Math.sqrt(K);
5509
+ ax = 0;
5510
+ return { rx: rx, ry: ry, ax: ax };
5511
+ }
5512
+
5513
+ // if it is not a circle, nor diagonal
5514
+ let L = ma[0] * ma[1] + ma[2] * ma[3];
5515
+
5516
+ // {l1,l2} = the two eigen values of ma * transpose(ma)
5517
+ let l1 = JK + D / 2,
5518
+ l2 = JK - D / 2;
5519
+
5520
+ // the x - axis - rotation angle is the argument of the l1 - eigenvector
5521
+ if (Math.abs(L) <= epsilon && Math.abs(l1 - K) <= epsilon) {
5522
+ // if (ax == 90) => ax = 0 and exchange axes
5523
+ ax = 0;
5524
+ rx = Math.sqrt(l2);
5525
+ ry = Math.sqrt(l1);
5526
+ return { rx: rx, ry: ry, ax: ax };
5527
+ }
5528
+
5529
+ ax =
5530
+ Math.atan(Math.abs(L) > Math.abs(l1 - K) ? (l1 - J) / L : L / (l1 - K)) /
5531
+ torad; // the angle in degree
5532
+
5533
+ // if ax > 0 => rx = sqrt(l1), ry = sqrt(l2), else exchange axes and ax += 90
5534
+ if (ax >= 0) {
5535
+ // if ax in [0,90]
5536
+ rx = Math.sqrt(l1);
5537
+ ry = Math.sqrt(l2);
5538
+ } else {
5539
+ // if ax in ]-90,0[ => exchange axes
5540
+ ax += 90;
5541
+ rx = Math.sqrt(l2);
5542
+ ry = Math.sqrt(l1);
5543
+ }
5544
+
5545
+ return { rx: rx, ry: ry, ax: ax };
5546
+ }
5547
+
5548
+ /**
5549
+ * Decompose matrix to readable transform properties
5550
+ * translate() rotate() scale() etc.
5551
+ * based on @AndreaBogazzi's answer
5552
+ * https://stackoverflow.com/questions/5107134/find-the-rotation-and-skew-of-a-matrix-transformation#32125700
5553
+ * return object with seperate transform properties
5554
+ * and ready to use css or svg attribute strings
5555
+ */
5556
+ function qrDecomposeMatrix(matrix, precision = 4) {
5557
+ let { a, b, c, d, e, f } = matrix;
5558
+ // matrix is array
5559
+ if (Array.isArray(matrix)) {
5560
+ [a, b, c, d, e, f] = matrix;
5561
+ }
5562
+ let angle = Math.atan2(b, a),
5563
+ denom = Math.pow(a, 2) + Math.pow(b, 2),
5564
+ scaleX = Math.sqrt(denom),
5565
+ scaleY = (a * d - c * b) / scaleX,
5566
+ skewX = Math.atan2(a * c + b * d, denom) / (Math.PI / 180),
5567
+ translateX = e ? e : 0,
5568
+ translateY = f ? f : 0,
5569
+ rotate = angle ? angle / (Math.PI / 180) : 0;
5570
+ let transObj = {
5571
+ translateX: translateX,
5572
+ translateY: translateY,
5573
+ rotate: rotate,
5574
+ scaleX: scaleX,
5575
+ scaleY: scaleY,
5576
+ skewX: skewX,
5577
+ skewY: 0
5578
+ };
5579
+ let cssTransforms = [];
5580
+ let svgTransforms = [];
5581
+ for (let prop in transObj) {
5582
+ transObj[prop] = +parseFloat(transObj[prop]).toFixed(precision);
5583
+ let val = transObj[prop];
5584
+ let unit = "";
5585
+ if (prop == "rotate" || prop == "skewX") {
5586
+ unit = "deg";
5587
+ }
5588
+ if (prop.indexOf("translate") != -1) {
5589
+ unit = "px";
5590
+ }
5591
+ // combine these properties
5592
+ let convert = ["scaleX", "scaleY", "translateX", "translateY"];
5593
+ if (val !== 0) {
5594
+ cssTransforms.push(`${prop}(${val}${unit})`);
5595
+ }
5596
+ if (convert.indexOf(prop) == -1 && val !== 0) {
5597
+ svgTransforms.push(`${prop}(${val})`);
5598
+ } else if (prop == "scaleX") {
5599
+ svgTransforms.push(
5600
+ `scale(${+scaleX.toFixed(precision)} ${+scaleY.toFixed(precision)})`
5601
+ );
5602
+ } else if (prop == "translateX") {
5603
+ svgTransforms.push(
5604
+ `translate(${transObj.translateX} ${transObj.translateY})`
5605
+ );
5606
+ }
5607
+
5608
+ }
5609
+ // append css style string to object
5610
+ transObj.cssTransform = cssTransforms.join(" ");
5611
+ transObj.svgTransform = svgTransforms.join(" ");
5612
+
5613
+ transObj.matrix = [a, b, c, d, e, f ].map(val=>roundTo(val, precision));
5614
+ transObj.matrixAtt = `matrix(${transObj.matrix.join(' ')})`;
5615
+
5616
+
5617
+
5618
+ return transObj;
5619
+ }
5620
+
5095
5621
  function pathElToShape(el, {
5096
5622
  convert_rects = false,
5097
5623
  convert_ellipses = false,
@@ -5224,14 +5750,16 @@
5224
5750
  convert_rects = false,
5225
5751
  convert_ellipses = false,
5226
5752
  convert_poly = false,
5227
- convert_lines = false
5753
+ convert_lines = false,
5754
+
5755
+ matrix=null
5228
5756
 
5229
5757
  } = {}) {
5230
5758
 
5231
5759
  let nodeName = el.nodeName.toLowerCase();
5232
5760
 
5233
5761
  if (
5234
- nodeName === 'path' ||
5762
+ nodeName === 'path' && !matrix ||
5235
5763
  nodeName === 'rect' && !convert_rects ||
5236
5764
  (nodeName === 'circle' || nodeName === 'ellipse') && !convert_ellipses ||
5237
5765
  (nodeName === 'polygon' || nodeName === 'polyline') && !convert_poly ||
@@ -5239,17 +5767,25 @@
5239
5767
  ) return el;
5240
5768
 
5241
5769
  let pathData = getPathDataFromEl(el, { width, height });
5770
+
5771
+ // shape attributes – obsolete for path els
5772
+ let exclude = ['d', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
5773
+
5774
+ // transform pathData
5775
+ if(matrix && Object.values(matrix).join('')!=='100100'){
5776
+ pathData = transformPathData(pathData, matrix);
5777
+ exclude.push('transform', 'transform-origin');
5778
+ }
5779
+
5242
5780
  let d = pathData.map(com => { return `${com.type} ${com.values} ` }).join(' ');
5243
5781
  let attributes = [...el.attributes].map(att => att.name);
5244
5782
 
5245
5783
  let pathN = document.createElementNS(svgNs, 'path');
5246
5784
  pathN.setAttribute('d', d);
5247
5785
 
5248
- let exclude = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
5249
-
5786
+ // copy attributes
5250
5787
  attributes.forEach(att => {
5251
5788
  if (!exclude.includes(att)) {
5252
-
5253
5789
  let val = el.getAttribute(att);
5254
5790
  pathN.setAttribute(att, val);
5255
5791
  }
@@ -6034,117 +6570,28 @@
6034
6570
  * transform property object
6035
6571
  */
6036
6572
 
6037
- function parseCSSTransform(transformString, transformOrigin = { x: 0, y: 0 }) {
6038
-
6039
- if (!transformString) return false;
6040
-
6041
- let transformOptions = {
6042
- transforms: [],
6043
- transformOrigin,
6044
- };
6045
-
6046
- let regex = /(\w+)\(([^)]+)\)/g;
6047
- let match;
6048
-
6049
- function convertToDegrees(value) {
6050
- if (typeof value === 'string') {
6051
- if (value.includes('rad')) {
6052
- return parseFloat(value) * (180 / Math.PI);
6053
- } else if (value.includes('turn')) {
6054
- return parseFloat(value) * 360;
6055
- }
6056
- }
6057
- return parseFloat(value);
6058
- }
6059
-
6060
- while ((match = regex.exec(transformString)) !== null) {
6061
- let name = match[1];
6062
- let values = match[2].split(/,\s*/).map(v => convertToDegrees(v));
6063
-
6064
- switch (name) {
6065
-
6066
- case 'translate':
6067
- transformOptions.transforms.push({ translate: [values[0] || 0, values[1] || 0] });
6068
- break;
6069
- case 'translateX':
6070
- transformOptions.transforms.push({ translate: [values[0] || 0, 0, 0] });
6071
- break;
6072
-
6073
- case 'translateY':
6074
- transformOptions.transforms.push({ translate: [0, values[0] || 0, 0] });
6075
- break;
6076
- case 'scale':
6077
- transformOptions.transforms.push({ scale: [values[0] || 0, values[1] || 0] });
6078
- break;
6079
- case 'skew':
6080
- transformOptions.transforms.push({ skew: [values[0] || 0, values[1] || 0] });
6081
- break;
6082
-
6083
- case 'skewX':
6084
- transformOptions.transforms.push({ skew: [values[0] || 0, 0] });
6085
- break;
6086
-
6087
- case 'skewY':
6088
- transformOptions.transforms.push({ skew: [0, values[0] || 0] });
6089
- break;
6090
-
6091
- case 'rotate':
6092
-
6093
- console.log('rotate', values);
6094
-
6095
- transformOptions.transforms.push({ rotate: [0, 0, values[0] || 0] });
6096
- break;
6097
- case 'matrix':
6098
- transformOptions.transforms.push({ matrix: values });
6099
- break;
6100
- }
6101
- }
6102
-
6103
- // Extract transform-origin, perspective-origin, and perspective if included as separate properties
6104
- let styleProperties = transformString.split(/;\s*/);
6105
- styleProperties.forEach(prop => {
6106
- let [key, value] = prop.split(':').map(s => s.trim());
6107
- if (key === 'transform-origin' || key === 'perspective-origin') {
6108
- let [x, y] = value.split(/\s+/).map(parseFloat);
6109
- if (key === 'transform-origin') {
6110
- transformOptions.transformOrigin = { x: x || 0, y: y || 0 };
6111
- }
6112
- }
6113
- });
6114
-
6115
- return transformOptions;
6116
- }
6117
-
6118
- /**
6119
- * wrapper function to switch between
6120
- * 2D or 3D matrix
6121
- */
6122
- function getMatrix({
6123
- transforms = [],
6124
- transformOrigin = { x: 0, y: 0 },
6125
- } = {}) {
6126
-
6127
- let matrix = getMatrix2D(transforms, transformOrigin);
6573
+ function getMatrixFromTransform(transformations = []) {
6128
6574
 
6129
- return matrix
6130
- }
6575
+ // Helper function to multiply two 2D matrices
6131
6576
 
6132
- function getMatrix2D(transformations = [], origin = { x: 0, y: 0 }) {
6577
+ const multiply = (m1, m2) => {
6578
+ let mtxN = {
6579
+ a: m1.a * m2.a + m1.c * m2.b,
6580
+ b: m1.b * m2.a + m1.d * m2.b,
6581
+ c: m1.a * m2.c + m1.c * m2.d,
6582
+ d: m1.b * m2.c + m1.d * m2.d,
6583
+ e: m1.a * m2.e + m1.c * m2.f + m1.e,
6584
+ f: m1.b * m2.e + m1.d * m2.f + m1.f
6585
+ };
6133
6586
 
6134
- // Helper function to multiply two 2D matrices
6135
- const multiply = (m1, m2) => ({
6136
- a: m1.a * m2.a + m1.c * m2.b,
6137
- b: m1.b * m2.a + m1.d * m2.b,
6138
- c: m1.a * m2.c + m1.c * m2.d,
6139
- d: m1.b * m2.c + m1.d * m2.d,
6140
- e: m1.a * m2.e + m1.c * m2.f + m1.e,
6141
- f: m1.b * m2.e + m1.d * m2.f + m1.f
6142
- });
6587
+ return mtxN;
6588
+ };
6143
6589
 
6144
6590
  // Helper function to create a translation matrix
6145
- const translationMatrix = (x, y) => ({
6146
- a: 1, b: 0, c: 0, d: 1, e: x, f: y
6147
- });
6591
+ const translationMatrix = (x, y) => {
6592
+ let mtx ={a: 1, b: 0, c: 0, d: 1, e: x, f: y};
6593
+ return mtx
6594
+ };
6148
6595
 
6149
6596
  // Helper function to create a scaling matrix
6150
6597
  const scalingMatrix = (x, y) => ({
@@ -6153,13 +6600,13 @@
6153
6600
 
6154
6601
  // get skew or rotation axis matrix
6155
6602
  const angleMatrix = (angles, type) => {
6156
- const toRad = (angle) => angle * Math.PI / 180;
6157
- let [angleX, angleY] = angles.map(ang => { return toRad(ang) });
6603
+
6604
+ let [angleX, angleY=0] = angles.map(ang => ang*deg2rad);
6158
6605
  let m = {};
6159
6606
 
6160
6607
  if (type === 'rot') {
6161
- let cos = Math.cos(angleX), sin = Math.sin(angleX);
6162
- m = { a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 };
6608
+ let cosX = Math.cos(angleX), sinX = Math.sin(angleX);
6609
+ m = { a: cosX, b: sinX, c: -sinX, d: cosX, e: 0, f: 0 };
6163
6610
  } else if (type === 'skew') {
6164
6611
  let tanX = Math.tan(angleX), tanY = Math.tan(angleY);
6165
6612
  m = {
@@ -6172,201 +6619,48 @@
6172
6619
  // Start with an identity matrix
6173
6620
  let matrix = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
6174
6621
 
6175
- // Apply transform origin: translate to origin, apply transformations, translate back
6176
- if (origin.x !== 0 || origin.y !== 0) {
6177
- matrix = multiply(matrix, translationMatrix(origin.x, origin.y));
6178
- }
6622
+ // Process transformations in the provided order (right-to-left)
6623
+ for (let i = 0; i < transformations.length; i++) {
6179
6624
 
6180
- // Default values for transformations
6181
- const defaults = {
6182
- translate: [0, 0],
6183
- scale: [1, 1],
6184
- skew: [0, 0],
6185
- rotate: [0],
6186
- matrix: [1, 0, 0, 1, 0, 0]
6187
- };
6625
+ let transform = transformations[i];
6188
6626
 
6189
- // Process transformations in the provided order (right-to-left)
6190
- for (let transform of transformations) {
6191
- let type = Object.keys(transform)[0]; // Get the transformation type (e.g., "translate")
6192
- let values = transform[type] || defaults[type]; // Use default values if none provided
6627
+ // Get the transformation type (e.g., "translate")
6628
+ let type = Object.keys(transform)[0];
6193
6629
 
6194
- // Destructure values with fallbacks
6195
- let [x, y = defaults[type][1]] = values;
6630
+ let values = transform[type];
6196
6631
 
6197
- // Z-rotate as 2d rotation
6198
- if (type === 'rotate' && values.length === 3) {
6199
- x = values[2];
6200
- }
6632
+ let [x, y] = values;
6201
6633
 
6202
6634
  switch (type) {
6203
6635
  case "matrix":
6204
6636
  let keys = ['a', 'b', 'c', 'd', 'e', 'f'];
6205
6637
  let obj = Object.fromEntries(keys.map((key, i) => [key, values[i]]));
6638
+
6206
6639
  matrix = multiply(matrix, obj);
6207
6640
  break;
6208
6641
  case "translate":
6209
- if (x || y) matrix = multiply(matrix, translationMatrix(x, y));
6642
+ matrix = multiply(matrix, translationMatrix(x, y));
6210
6643
  break;
6211
6644
  case "skew":
6212
- if (x || y) matrix = multiply(matrix, angleMatrix([x, y], 'skew'));
6645
+ matrix = multiply(matrix, angleMatrix([x, y], 'skew'));
6213
6646
  break;
6214
6647
  case "rotate":
6215
- if (x) matrix = multiply(matrix, angleMatrix([x], 'rot'));
6648
+ matrix = multiply(matrix, angleMatrix([x], 'rot'));
6649
+
6216
6650
  break;
6217
6651
  case "scale":
6218
- if (x !== 1 || y !== 1) matrix = multiply(matrix, scalingMatrix(x, y));
6652
+ matrix = multiply(matrix, scalingMatrix(x, y));
6219
6653
  break;
6220
6654
 
6221
6655
  default:
6222
6656
  throw new Error(`Unknown transformation type: ${type}`);
6223
6657
  }
6224
- }
6225
6658
 
6226
- // Revert transform origin
6227
- if (origin.x !== 0 || origin.y !== 0) {
6228
- matrix = multiply(matrix, translationMatrix(-origin.x, -origin.y));
6229
6659
  }
6230
6660
 
6231
6661
  return matrix;
6232
6662
  }
6233
6663
 
6234
- function svgStylesToAttributes(el, {
6235
- removeNameSpaced = true,
6236
- decimals = -1
6237
- } = {}) {
6238
-
6239
- let nodeName = el.nodeName.toLowerCase();
6240
- let attProps = getElAttributes(el);
6241
- let cssProps = getElStyleProps(el);
6242
-
6243
- // normalize transform attributes
6244
- /*
6245
- if (attProps['transform']) {
6246
- console.log(`attProps['transform']`, attProps['transform']);
6247
- }
6248
- */
6249
-
6250
- // merge properties
6251
- let props = {
6252
- ...attProps,
6253
- ...cssProps
6254
- };
6255
-
6256
- // filter out obsolete properties
6257
- let propsFiltered = {};
6258
-
6259
- // parse CSS transforms
6260
- let cssTrans = cssProps['transform'];
6261
-
6262
- if (cssTrans) {
6263
- let transStr = `${cssTrans}`;
6264
- let transformObj = parseCSSTransform(transStr);
6265
- let matrix = getMatrix(transformObj);
6266
-
6267
- // apply as SVG matrix transform
6268
- props['transform'] = `matrix(${Object.values(matrix).join(',')})`;
6269
- }
6270
-
6271
- // can't be replaced with attributes
6272
- let cssOnlyProps = ['inline-size'];
6273
- let styleProps = [];
6274
-
6275
- for (let prop in props) {
6276
-
6277
- let value = props[prop];
6278
-
6279
- // CSS variable
6280
- if (value && prop.startsWith('--') || cssOnlyProps.includes(prop) ||
6281
- (!removeNameSpaced && prop.startsWith('-'))) {
6282
- styleProps.push(`${prop}:${value}`);
6283
- continue
6284
- }
6285
-
6286
- // check if property is valid
6287
- if (value && attLookup.atts[prop] &&
6288
- (attLookup.atts[prop] === '*' ||
6289
- attLookup.atts[prop].includes(nodeName) ||
6290
- !removeNameSpaced && (prop.includes(':'))
6291
- )
6292
- ) {
6293
- propsFiltered[prop] = value;
6294
- }
6295
-
6296
- // remove property
6297
- el.removeAttribute(prop);
6298
-
6299
- }
6300
-
6301
- // apply filtered attributes
6302
- for (let prop in propsFiltered) {
6303
- let value = propsFiltered[prop];
6304
- el.setAttribute(prop, value);
6305
- }
6306
-
6307
- if (styleProps.length) {
6308
- el.setAttribute('style', styleProps.join(';'));
6309
- }
6310
-
6311
- return propsFiltered;
6312
-
6313
- }
6314
-
6315
- function parseInlineStyle(styleAtt = '') {
6316
-
6317
- let props = {};
6318
- if (!styleAtt) return props;
6319
-
6320
- let styleArr = styleAtt.split(';').filter(Boolean).map(prop => prop.trim());
6321
- let l = styleArr.length;
6322
- if (!l) return props;
6323
-
6324
- for (let i = 0; l && i < l; i++) {
6325
- let style = styleArr[i];
6326
- let [prop, value] = style.split(':').filter(Boolean);
6327
- props[prop] = value;
6328
-
6329
- }
6330
-
6331
- return props
6332
- }
6333
-
6334
- function getElStyleProps(el) {
6335
- let styleAtt = el.getAttribute('style');
6336
- let props = styleAtt ? parseInlineStyle(styleAtt) : {};
6337
- return props
6338
- }
6339
-
6340
- function getElAttributes(el) {
6341
- let props = {};
6342
- let atts = [...el.attributes].map((att) => att.name);
6343
- let l = atts.length;
6344
- if (!l) return props;
6345
-
6346
- for (let i = 0; i < l; i++) {
6347
- let att = atts[i];
6348
- let value = el.getAttribute(att);
6349
- props[att] = value;
6350
- }
6351
-
6352
- return props;
6353
- }
6354
-
6355
- /*
6356
- function roundValue(value = '', decimals = -1) {
6357
- if (decimals < 0) return value;
6358
- value = value.replace(/["]/g, '').trim()
6359
- let valueNum = parseFloat(value);
6360
- let valueHasNumber = !isNaN(valueNum);
6361
- if (!valueHasNumber) return value;
6362
-
6363
- let unit = valueHasNumber ? getUnit(value) : '';
6364
- if (valueHasNumber) value = `${valueNum.toFixed(decimals)}${unit}`;
6365
-
6366
- return value;
6367
- }
6368
- */
6369
-
6370
6664
  function normalizePoly(pts, {
6371
6665
  toObject = true,
6372
6666
  toArray = false,
@@ -6477,6 +6771,522 @@
6477
6771
  return bb;
6478
6772
  }
6479
6773
 
6774
+ /**
6775
+ * parse svg presentational attributes
6776
+ * or CSS styles
6777
+ */
6778
+
6779
+ function parseStylesProperties(el, {
6780
+ fontSize = 16,
6781
+ removeNameSpaced = true,
6782
+ autoRoundValues = false,
6783
+ minifyRgbColors = false,
6784
+ removeInvalid = true,
6785
+ removeDefaults = true,
6786
+ cleanUpStrokes = true,
6787
+ normalizeTransforms = true,
6788
+ exclude = [],
6789
+ width = 0,
6790
+ height = 0,
6791
+ } = {}) {
6792
+
6793
+ let nodeName = el.nodeName.toLowerCase();
6794
+ let attProps = getSvgPresentationAtts(el);
6795
+ let cssProps = getSvgCssProps(el);
6796
+
6797
+ /**
6798
+ * merge props
6799
+ * CSS has higher specificity
6800
+ */
6801
+ let props = {
6802
+ ...attProps,
6803
+ ...cssProps,
6804
+ };
6805
+
6806
+ delete props['style'];
6807
+ exclude.push('style');
6808
+
6809
+ let remove = ['style'];
6810
+ let transformsStandalone = ['scale', 'translate', 'rotate'];
6811
+
6812
+ /**
6813
+ * remove invalid properties
6814
+ * e.g font-family for <path>
6815
+ */
6816
+
6817
+ if (removeInvalid || removeDefaults || removeNameSpaced) {
6818
+ let propsFilteredObj = filterSvgElProps(nodeName, props, { removeDefaults, removeNameSpaced, exclude, cleanUpStrokes, include: transformsStandalone, cleanUpStrokes: false });
6819
+ props = propsFilteredObj.propsFiltered;
6820
+ remove.push(...propsFilteredObj.remove);
6821
+ }
6822
+
6823
+ // sanitized prop array
6824
+ let propArr = [];
6825
+
6826
+ for (let prop in props) {
6827
+
6828
+ let valueStr = props[prop];
6829
+
6830
+ // we parse the path data separately
6831
+ if (prop === 'd' || prop.startsWith('data-')) {
6832
+ continue;
6833
+ }
6834
+
6835
+ let item = { prop, values: [] };
6836
+
6837
+ // minify rgb values
6838
+ if (minifyRgbColors && colorProps.includes(prop)) {
6839
+ let color = parseColor(valueStr);
6840
+ if (color.mode === 'rgba' || color.mode === 'rgb') {
6841
+ let hex = rgba2Hex(color);
6842
+ valueStr = hex;
6843
+ }
6844
+ }
6845
+
6846
+ if (prop === 'transform') {
6847
+ let transArr = [];
6848
+
6849
+ let transFormFunctions = valueStr.split(/(\w+)\(([^)]+)\)/).map(val => val.trim()).filter(Boolean);
6850
+
6851
+ for (let i = 1; i < transFormFunctions.length; i += 2) {
6852
+ let fn = transFormFunctions[i - 1];
6853
+ let isHorizontal = transHorizontal.includes(fn);
6854
+ let isVertical = transVertical.includes(fn);
6855
+ if (isHorizontal) fn = fn.replace('X', '');
6856
+ if (isVertical) fn = fn.replace('Y', '');
6857
+ let values = transFormFunctions[i].split(/,| /).filter(Boolean);
6858
+ let transItem = { fn, values: [] };
6859
+
6860
+ for (let v = 0; v < values.length; v++) {
6861
+ let transValues = parseValue(values[v]);
6862
+ transItem.values.push(...transValues);
6863
+ }
6864
+
6865
+ let defaultX = fn.startsWith('scale') ? 1 : 0;
6866
+ let defaultY = fn.startsWith('scale') ? 1 : 0;
6867
+
6868
+ if (isHorizontal) transItem.values = [transItem.values[0], { value: defaultX, unit: '', numeric: true }];
6869
+ if (isVertical) transItem.values = [{ value: defaultY, unit: '', numeric: true }, transItem.values[0]];
6870
+
6871
+ transArr.push(transItem);
6872
+ }
6873
+
6874
+ if (transArr.length) {
6875
+ propArr.push({ prop: 'transforms', values: transArr });
6876
+ }
6877
+ }
6878
+
6879
+ // other props
6880
+ else {
6881
+
6882
+ item.values = parseValue(valueStr);
6883
+
6884
+ }
6885
+
6886
+ if (item.values.length) {
6887
+ propArr.push(item);
6888
+ }
6889
+
6890
+ }
6891
+
6892
+ /**
6893
+ * normalize values to
6894
+ * user units
6895
+ */
6896
+
6897
+ let propsNorm = { transformArr: [], matrix: null, transComponents: null };
6898
+ let transFormOrigin = [];
6899
+ let normalizedDiagonal = false;
6900
+
6901
+ for (let i = 0; i < propArr.length; i++) {
6902
+ let item = propArr[i];
6903
+ let { prop, values } = item;
6904
+ let valsNew = [], valX = 0, valY = 0, unitX = '', unitY = '';
6905
+
6906
+ if (prop !== 'transforms') {
6907
+
6908
+ if (cleanUpStrokes && (prop === 'stroke-dasharray' || prop === 'stroke-dashoffset')) {
6909
+ normalizedDiagonal = true;
6910
+ for (let i = 0; i < values.length; i++) {
6911
+ let val = normalizeUnits(values[i].value, { unit: values[i].unit, width, height, normalizedDiagonal, fontSize });
6912
+ valsNew.push(val);
6913
+ }
6914
+ }
6915
+
6916
+ else if (prop === 'transform-origin') {
6917
+
6918
+ values.forEach((item, i) => {
6919
+ let val = item.value;
6920
+ if (val === 'left') values[i].value = 0;
6921
+ else if (val === 'right') values[i].value = width;
6922
+ else if (val === 'top') values[i].value = 0;
6923
+ else if (val === 'bottom') values[i].value = height;
6924
+ else if (val === 'center') values[i].value = '50%';
6925
+ });
6926
+
6927
+ valX = values[0].value;
6928
+ valY = values[1] ? values[1].value : valX;
6929
+ unitX = values[0].unit;
6930
+ unitY = values[1] ? values[1].unit : unitX;
6931
+
6932
+ // normalize units for matrix calculation
6933
+ valX = normalizeUnits(valX, { unit: unitX, width, height, isHorizontal: true, fontSize });
6934
+ valY = normalizeUnits(valY, { unit: unitY, width, height, isVertical: true, fontSize });
6935
+ transFormOrigin.push(valX, valY);
6936
+
6937
+ } else {
6938
+
6939
+ for (let v = 0; v < values.length; v++) {
6940
+ let val = values[v];
6941
+
6942
+ let unit = val.unit;
6943
+ let valAbs = val.value;
6944
+ let isNumeric = val.numeric;
6945
+
6946
+ let isHorizontal = horizontalProps.includes(prop);
6947
+ let isVertical = verticalProps.includes(prop);
6948
+
6949
+ if (unit) {
6950
+ if (prop === 'scale' && unit === '%') {
6951
+ valAbs = valAbs * 0.01;
6952
+ } else {
6953
+ if (prop === 'r') normalizedDiagonal = true;
6954
+ valAbs = normalizeUnits(val.value, { unit, width, height, isHorizontal, isVertical, normalizedDiagonal, fontSize });
6955
+
6956
+ if (autoRoundValues && isNumeric) {
6957
+ valAbs = autoRound(valAbs);
6958
+ }
6959
+
6960
+ }
6961
+ }
6962
+ valsNew.push(valAbs);
6963
+ }
6964
+ }
6965
+
6966
+ if (valsNew.length) propsNorm[prop] = valsNew;
6967
+
6968
+ }
6969
+
6970
+ // is transform properties and functions
6971
+ else {
6972
+
6973
+ let transforms = values || [];
6974
+
6975
+ let len = transforms.length;
6976
+ let transFormAllObj = [];
6977
+
6978
+ for (let t = 0; len && t < len; t++) {
6979
+ let { fn, values } = transforms[t];
6980
+ let valsN = [], unitX = '', unitY = '', transformFunctionArr = [];
6981
+
6982
+ // defaults
6983
+ let valX = 0;
6984
+ let valY = 0;
6985
+ let transObj = {};
6986
+
6987
+ // console.log('!!!values', values);
6988
+ if (fn === 'scale' || fn === 'translate') {
6989
+ valX = values[0].value;
6990
+ valY = values[1] ? values[1].value : valX;
6991
+ unitX = values[0].unit;
6992
+ unitY = values[1] ? values[1].unit : unitX;
6993
+
6994
+ if (fn === 'scale') {
6995
+ valX = unitX === '%' ? valX * 0.01 : valX;
6996
+ valY = unitY === '%' ? valY * 0.01 : valY;
6997
+ } else {
6998
+ valX = normalizeUnits(valX, { unit: unitX, width, height, isHorizontal: true, fontSize });
6999
+ valY = normalizeUnits(valY, { unit: unitY, width, height, isVertical: true, fontSize });
7000
+
7001
+ }
7002
+ valsN.push(valX, valY);
7003
+
7004
+ transObj[fn] = valsN;
7005
+ transformFunctionArr.push(transObj);
7006
+
7007
+ }
7008
+
7009
+ if (fn === 'matrix') {
7010
+ valsN = values.map(val => val.value);
7011
+ transObj[fn] = valsN;
7012
+ transformFunctionArr.push(transObj);
7013
+ }
7014
+
7015
+ if (fn === 'skew') {
7016
+
7017
+ valX = values[0].value;
7018
+ unitX = values[0].unit;
7019
+ valY = values[1].value;
7020
+ unitY = values[1].unit;
7021
+
7022
+ valX = normalizeUnits(valX, { unit: unitX, isHorizontal: true, fontSize });
7023
+ valY = normalizeUnits(valY, { unit: unitY, isVertical: true, fontSize });
7024
+
7025
+ // normalize large angles
7026
+ valX = valX > 360 ? (valX % 360) : valX;
7027
+ valY = valY > 360 ? (valY % 360) : valY;
7028
+
7029
+ valsN = [valX, valY];
7030
+ transObj[fn] = valsN;
7031
+ transformFunctionArr.push(transObj);
7032
+
7033
+ }
7034
+
7035
+ // SVG rotations may contain a transform origin
7036
+ if (fn === 'rotate') {
7037
+
7038
+ let angle = values[0].value;
7039
+ let unit = values[0].unit;
7040
+ angle = normalizeUnits(angle, { unit });
7041
+
7042
+ let hasPivot = values.length === 3;
7043
+ let transOrigin = [];
7044
+
7045
+ if (hasPivot) {
7046
+
7047
+ let cx = values[1].value;
7048
+ let cy = values[2].value;
7049
+ transOrigin.push({ translate: [cx, cy] }, { translate: [-cx, -cy] });
7050
+
7051
+ }
7052
+
7053
+ transObj[fn] = [angle];
7054
+
7055
+ if (transOrigin.length) {
7056
+ transformFunctionArr.push(transOrigin[0], transObj, transOrigin[1]);
7057
+ } else {
7058
+ transformFunctionArr.push(transObj);
7059
+ }
7060
+ }
7061
+
7062
+ transFormAllObj.push(...transformFunctionArr);
7063
+
7064
+ }
7065
+
7066
+ propsNorm['transformArr'] = transFormAllObj;
7067
+
7068
+ }
7069
+
7070
+ }
7071
+
7072
+ // prepend standalone transforms before standards
7073
+ let translate = propsNorm['translate'] !== undefined ? { translate: propsNorm['translate'] } : null;
7074
+ let scale = propsNorm['scale'] !== undefined ? { scale: propsNorm['scale'] } : null;
7075
+ let rotate = propsNorm['rotate'] !== undefined ? { rotate: propsNorm['rotate'] } : null;
7076
+ let standaloneTransforms = [translate, rotate, scale].filter(Boolean);
7077
+
7078
+ if (standaloneTransforms.length) {
7079
+ if (normalizeTransforms) remove.push('translate', 'scale', 'rotate');
7080
+ propsNorm['transformArr'] = [...standaloneTransforms, ...propsNorm['transformArr']];
7081
+ }
7082
+
7083
+ // replace transform-origin with translates
7084
+
7085
+ if (transFormOrigin.length && propsNorm['transformArr'] !== undefined) {
7086
+ propsNorm['transformArr'] = [
7087
+ { translate: [transFormOrigin[0], transFormOrigin[1]] },
7088
+ ...propsNorm['transformArr'],
7089
+ { translate: [-transFormOrigin[0], -transFormOrigin[1]] },
7090
+ ];
7091
+ if (normalizeTransforms) remove.push('transform-origin');
7092
+ }
7093
+
7094
+ /**
7095
+ * test run
7096
+ * apply parsed transforms
7097
+ */
7098
+ let { transformArr = [] } = propsNorm;
7099
+
7100
+ let transAtt = [];
7101
+ let l = transformArr.length;
7102
+ if (l) {
7103
+ for (let i = 0; l && i < l; i++) {
7104
+ let prop = transformArr[i];
7105
+ let values = Object.values(prop).flat();
7106
+ let name = Object.keys(prop)[0];
7107
+ if (name === 'skew') {
7108
+ if (values[0]) transAtt.push(`skewX(${values[0]})`);
7109
+ if (values[1]) transAtt.push(`skewY(${values[1]})`);
7110
+ } else {
7111
+ transAtt.push(`${name}(${values.join(' ')})`);
7112
+ }
7113
+ }
7114
+ // consolidate transforms to matrix
7115
+
7116
+ }
7117
+
7118
+ propsNorm.remove = remove;
7119
+ propsNorm.type = nodeName;
7120
+
7121
+ return propsNorm
7122
+
7123
+ }
7124
+
7125
+ /**
7126
+ * consolidate transforms to matrix
7127
+ */
7128
+ function addTransFormProps(propsObj = {}, transformArr = []) {
7129
+ if (propsObj.transformArr === undefined || !transformArr.length) return;
7130
+
7131
+ // take existing array or custom
7132
+ transformArr = transformArr.length ? transformArr : propsObj.transformArr;
7133
+ let matrix = getMatrixFromTransform(transformArr);
7134
+ propsObj['matrix'] = matrix;
7135
+
7136
+ let transComponents = qrDecomposeMatrix(matrix, 3);
7137
+ propsObj.transComponents = transComponents;
7138
+
7139
+ return propsObj
7140
+ }
7141
+
7142
+ /**
7143
+ * filter out nonsense
7144
+ * presentation attributes or
7145
+ * style properties not valid
7146
+ * for element type
7147
+ */
7148
+ function filterSvgElProps(elNodename = '', props = {}, {
7149
+ removeInvalid = true,
7150
+ removeDefaults = true,
7151
+ allowDataAtts = true,
7152
+ cleanUpStrokes = true,
7153
+ include = ['id', 'class'],
7154
+ exclude = [],
7155
+ } = {}) {
7156
+ let propsFiltered = {};
7157
+ let remove = [];
7158
+
7159
+ // allow defaults for nested
7160
+
7161
+ let noStrokeColor = cleanUpStrokes ? (props['stroke'] === undefined) : false;
7162
+
7163
+ for (let prop in props) {
7164
+ let values = props[prop];
7165
+ let value = Array.isArray(values) ? values[0] : values;
7166
+
7167
+ // filter out useless
7168
+ let isValid = removeInvalid ?
7169
+ (attLookup.atts[prop] ? attLookup.atts[prop].includes(elNodename) : false) :
7170
+ false;
7171
+
7172
+ // remove null transforms
7173
+ if(prop==='transform' && value==='matrix(1 0 0 1 0 0)') isValid = false;
7174
+
7175
+ // allow data attributes
7176
+ let isDataAtt = allowDataAtts ? prop.startsWith('data-') : false;
7177
+
7178
+ // filter out defaults
7179
+ let isDefault = removeDefaults ?
7180
+ (attLookup.defaults[prop] ? attLookup.defaults[prop] !== undefined && attLookup.defaults[prop].includes(value) : false) :
7181
+ false;
7182
+
7183
+ if (isDataAtt || include.includes(prop)) isValid = true;
7184
+ if (isDefault) isValid = false;
7185
+ if (exclude.length && exclude.includes(prop)) isValid = false;
7186
+ if (noStrokeColor && strokeAtts.includes(prop)) isValid = false;
7187
+
7188
+ if (isValid) {
7189
+ propsFiltered[prop] = props[prop];
7190
+ }
7191
+ else {
7192
+ remove.push(prop);
7193
+ }
7194
+ }
7195
+
7196
+ /*
7197
+ // set explicit stroke width when disabled by stroke color
7198
+ if (propsFiltered['stroke'] && propsFiltered['stroke'][0] === 'none') {
7199
+ propsFiltered['stroke-width'] = [1]
7200
+ remove.push('stroke', 'stroke-width')
7201
+ console.log('remove', remove);
7202
+ }
7203
+ */
7204
+
7205
+ return { propsFiltered, remove }
7206
+ }
7207
+
7208
+ function parseValue(valStr = '') {
7209
+ let valArr = valStr.split(/,| /);
7210
+
7211
+ for (let i = 0; i < valArr.length; i++) {
7212
+
7213
+ let valStr = valArr[i];
7214
+ let val = { value: null, unit: '', numeric: false };
7215
+ let isNumeric = isNumericValue(valStr);
7216
+ if (!isNumeric) {
7217
+ val.value = valStr;
7218
+ }
7219
+ else if (isNumeric) {
7220
+ let unit = getUnit(valStr);
7221
+ let valNum = parseFloat(valStr);
7222
+ val.value = valNum;
7223
+ val.unit = unit;
7224
+ val.numeric = true;
7225
+ }
7226
+ valArr[i] = val;
7227
+ }
7228
+
7229
+ return valArr;
7230
+ }
7231
+
7232
+ function getSvgCssProps(el) {
7233
+ let styleAtt = el.getAttribute('style');
7234
+ let props = styleAtt ? parseInlineCss(styleAtt) : {};
7235
+ return props
7236
+ }
7237
+
7238
+ function getSvgPresentationAtts(el) {
7239
+ let props = {};
7240
+ let atts = [...el.attributes].map((att) => att.name);
7241
+ let l = atts.length;
7242
+ if (!l) return props;
7243
+
7244
+ for (let i = 0; i < l; i++) {
7245
+ let att = atts[i];
7246
+ let value = el.getAttribute(att);
7247
+
7248
+ // test invalid transform functions
7249
+ if (att === 'transform') {
7250
+ let transformSan = [];
7251
+ let transFormFunctions = value.split(/(\w+)\(([^)]+)\)/).map(val => val.trim()).filter(Boolean);
7252
+
7253
+ for (let i = 1; i < transFormFunctions.length; i += 2) {
7254
+ let prop = transFormFunctions[i - 1];
7255
+ let val = transFormFunctions[i];
7256
+ let units = val.split(/,| /).map(val => getUnit(val.trim())).filter(Boolean);
7257
+
7258
+ // remove invalid transform function
7259
+ if (!units.length) {
7260
+ transformSan.push(`${prop}(${val})`);
7261
+ }
7262
+ }
7263
+ value = transformSan.join(' ');
7264
+ }
7265
+
7266
+ props[att] = value.trim();
7267
+ }
7268
+
7269
+ return props;
7270
+ }
7271
+
7272
+ function parseInlineCss(styleAtt = '') {
7273
+
7274
+ let props = {};
7275
+ if (!styleAtt) return props;
7276
+
7277
+ let styleArr = styleAtt.split(';').filter(Boolean).map(prop => prop.trim());
7278
+ let l = styleArr.length;
7279
+ if (!l) return props;
7280
+
7281
+ for (let i = 0; l && i < l; i++) {
7282
+ let style = styleArr[i];
7283
+ let [prop, value] = style.split(':').filter(Boolean);
7284
+ props[prop] = value;
7285
+ }
7286
+
7287
+ return props
7288
+ }
7289
+
6480
7290
  function removeEmptySVGEls(svg) {
6481
7291
  let els = svg.querySelectorAll('g, defs');
6482
7292
  els.forEach(el => {
@@ -6498,6 +7308,12 @@
6498
7308
  cleanupClip = true,
6499
7309
  addViewBox = false,
6500
7310
  addDimensions = false,
7311
+ minifyRgbColors = false,
7312
+
7313
+ normalizeTransforms = true,
7314
+ autoRoundValues = true,
7315
+
7316
+ unGroup = false,
6501
7317
 
6502
7318
  mergePaths = false,
6503
7319
  removeOffCanvas = true,
@@ -6509,16 +7325,15 @@
6509
7325
  convert_rects = false,
6510
7326
  convert_ellipses = false,
6511
7327
  convert_poly = false,
6512
- convert_lines=false,
7328
+ convert_lines = false,
6513
7329
 
6514
7330
  convertTransforms = false,
7331
+ removeDefaults = true,
6515
7332
  cleanUpStrokes = true,
6516
7333
  decimals = -1,
6517
7334
  excludedEls = [],
6518
7335
  } = {}) {
6519
7336
 
6520
- attributesToGroup = cleanupSVGAtts ? true : false;
6521
-
6522
7337
  // replace namespaced refs
6523
7338
  if (fixHref) svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
6524
7339
 
@@ -6529,16 +7344,57 @@
6529
7344
  let viewBox = getViewBox(svg);
6530
7345
  let { x, y, width, height } = viewBox;
6531
7346
 
7347
+ // get svg styles
7348
+ let propOptions = {
7349
+ width: width,
7350
+ height: height,
7351
+ normalizeTransforms,
7352
+ removeDefaults: false,
7353
+ cleanUpStrokes: false,
7354
+ autoRoundValues,
7355
+ minifyRgbColors,
7356
+ };
7357
+ let stylePropsSVG = parseStylesProperties(svg, propOptions);
7358
+
7359
+ // add svg font size for scaling relative
7360
+ propOptions.fontSize = stylePropsSVG['font-size'] ? stylePropsSVG['font-size'][0] : 16;
7361
+
7362
+ /**
7363
+ * get group styles
7364
+ * especially transformations to
7365
+ * be inherited by children
7366
+ */
7367
+ let groups = svg.querySelectorAll('g');
7368
+ let groupProps = [];
7369
+
7370
+ groups.forEach(g => {
7371
+ let stylePropsG = parseStylesProperties(g, propOptions);
7372
+ groupProps.push(stylePropsG);
7373
+ let children = g.querySelectorAll(`${renderedEls.join(', ')}`);
7374
+
7375
+ // store parent styles to child property
7376
+ children.forEach(child => {
7377
+ if (child.parentStyleProps === undefined) {
7378
+ child.parentStyleProps = [];
7379
+ }
7380
+ child.parentStyleProps.push(stylePropsG);
7381
+ });
7382
+ });
7383
+
6532
7384
  if (cleanupSVGAtts) {
6533
7385
 
6534
- let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'];
7386
+ let allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
7387
+ if (!stylesToAttributes) {
7388
+ allowed.push('fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'font-size', 'font-family', 'font-style', 'style');
7389
+ }
7390
+
6535
7391
  removeExcludedAttribues(svg, allowed);
6536
7392
  }
6537
7393
 
6538
7394
  // add viewBox
6539
7395
  if (addViewBox) addSvgViewBox(svg, { x, y, width, height });
6540
7396
  if (addDimensions) {
6541
- svg.setAttribute('width', width);
7397
+ svg.setAttribute('width', width);
6542
7398
  svg.setAttribute('height', height);
6543
7399
  }
6544
7400
 
@@ -6551,40 +7407,18 @@
6551
7407
  // always remove scripts
6552
7408
  let removeEls = ['metadata', 'script', ...excludedEls];
6553
7409
 
6554
- let els = svg.querySelectorAll('*');
7410
+ removeSVGEls(svg, { removeEls, removeNameSpaced });
6555
7411
 
6556
7412
  // an array of all elements' properties
6557
7413
  let svgElProps = [];
6558
-
6559
- let geometryElements = ['polygon', 'polyline', 'line', 'rect', 'circle', 'ellipse'];
6560
-
6561
- /** convert paths to shapes */
6562
- if(shapeConvert === 'toShapes'){
6563
- let paths = svg.querySelectorAll('path');
6564
- paths.forEach(path=>{
6565
- let shape = pathElToShape(path, {convert_rects, convert_ellipses, convert_poly, convert_lines});
6566
- path.replaceWith(shape);
6567
- path = shape;
6568
-
6569
- });
6570
-
6571
- }
7414
+ let els = svg.querySelectorAll(`${renderedEls.join(', ')}`);
6572
7415
 
6573
7416
  for (let i = 0; i < els.length; i++) {
6574
7417
  let el = els[i];
6575
7418
 
6576
7419
  let name = el.nodeName.toLowerCase();
6577
7420
 
6578
- // convert shapes
6579
- if (shapeConvert === 'toPaths' && name !== 'path' && geometryElements.includes(name)) {
6580
- let path = shapeElToPath(el, { width, height, convert_rects, convert_ellipses, convert_poly, convert_lines });
6581
- el.replaceWith(path);
6582
- name = 'path';
6583
- el = path;
6584
-
6585
- }
6586
-
6587
- // remove hidden elements
7421
+ // 1. remove hidden elements
6588
7422
  let style = el.getAttribute('style') || '';
6589
7423
  let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
6590
7424
  let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
@@ -6598,85 +7432,214 @@
6598
7432
  * convert relative or physical units
6599
7433
  * to user units
6600
7434
  */
7435
+ let styleProps = parseStylesProperties(el, propOptions);
6601
7436
 
6602
- /*
6603
- let styleProps = parseStylesProperties(el, {
6604
- width:viewBox.width,
6605
- height:viewBox.height
6606
- })
6607
- */
7437
+ // get parent styles
7438
+ let { parentStyleProps = [] } = el;
7439
+ let inheritedProps = {};
7440
+ let transFormInherited = [];
6608
7441
 
6609
- /*
6610
- let propTest = normalizeUnits('50%',{width:200, height:100, isHorizontal:true})
6611
- console.log('propTest', propTest);
6612
- */
7442
+ /** inherit transforms
7443
+ * and styles from group
7444
+ */
7445
+ parentStyleProps.forEach(props => {
7446
+ // transforms from groups are applied cumulatively
7447
+ let { transformArr = [] } = props;
7448
+ transFormInherited.push(...transformArr);
7449
+
7450
+ // merge
7451
+ inheritedProps = {
7452
+ ...inheritedProps,
7453
+ ...props
7454
+ };
7455
+ });
7456
+
7457
+ transFormInherited = [...transFormInherited, ...styleProps.transformArr];
7458
+ styleProps.transformArr = transFormInherited;
7459
+
7460
+ // merge with svg props
7461
+ styleProps = {
7462
+ ...stylePropsSVG,
7463
+ ...inheritedProps,
7464
+ ...styleProps
7465
+ };
6613
7466
 
6614
- // styles to attributes
6615
- if (stylesToAttributes || attributesToGroup || mergePaths || cleanUpStrokes) {
6616
- let propsFiltered = svgStylesToAttributes(el, { removeNameSpaced, decimals });
7467
+ // add combined transforms
7468
+ addTransFormProps(styleProps, transFormInherited);
6617
7469
 
6618
- svgElProps.push({ el, name, idx: i, propsFiltered });
7470
+ let { remove, matrix, transComponents } = styleProps;
7471
+
7472
+ // mark attributes for removal
7473
+ if (removeClassNames) styleProps.remove.push('class');
7474
+ if (removeIds) styleProps.remove.push('id');
7475
+ if (removeDimensions) {
7476
+ styleProps.remove.push('width');
7477
+ styleProps.remove.push('height');
6619
7478
  }
6620
7479
 
6621
- }
7480
+ // styles to atts
7481
+ if (unGroup || convertTransforms || minifyRgbColors ) stylesToAttributes = true;
6622
7482
 
6623
- // remove stroke properties if no stroke color applied - common inkscape issue
6624
- if (cleanUpStrokes) {
7483
+ if (stylesToAttributes) {
6625
7484
 
6626
- for (let item of svgElProps) {
7485
+ /**
7486
+ * normalize transforms
7487
+ */
7488
+ if (normalizeTransforms && matrix) {
7489
+ let { rotate, scaleX, scaleY, skewX, translateX, translateY } = transComponents;
6627
7490
 
6628
- let { el, propsFiltered } = item;
6629
- let strokeProps = Object.keys(propsFiltered);
7491
+ // scale attributes instead of transform
7492
+ let hasRot = rotate !== 0 || skewX !== 0;
7493
+ let unProportional = scaleX !== scaleY;
7494
+ let scalableByAtt = ['circle', 'ellipse', 'rect'];
7495
+ let needsTrans = convertTransforms || (name === 'g') || (hasRot) || unProportional;
6630
7496
 
6631
- if (!strokeProps.includes('stroke')) {
6632
- strokeAtts.forEach(att => {
6633
- el.removeAttribute(att);
7497
+ if (!needsTrans && scalableByAtt.includes(name)) {
6634
7498
 
6635
- // delete in property object
6636
- if (item['propsFiltered'][att] !== undefined) delete item['propsFiltered'][att];
7499
+ if (name === 'circle' || name === 'ellipse') {
7500
+ styleProps.cx[0] = [styleProps.cx[0] * scaleX + translateX];
7501
+ styleProps.cy[0] = [styleProps.cy[0] * scaleX + translateY];
7502
+
7503
+ if (styleProps.r) styleProps.r[0] = [styleProps.r[0] * scaleX];
7504
+
7505
+ if (styleProps.rx) styleProps.rx[0] = [styleProps.rx[0] * scaleX];
7506
+ if (styleProps.ry) styleProps.ry[0] = [styleProps.ry[0] * scaleX];
7507
+
7508
+ }
7509
+ else if (name === 'rect') {
7510
+ let x = styleProps.x ? styleProps.x[0] + translateX : translateX;
7511
+ let y = styleProps.y ? styleProps.y[0] + translateY : translateY;
7512
+
7513
+ let rx = styleProps.rx ? styleProps.rx[0] * scaleX : 0;
7514
+ let ry = styleProps.ry ? styleProps.ry[0] * scaleY : 0;
7515
+
7516
+ styleProps.x = [x];
7517
+ styleProps.y = [y];
7518
+
7519
+ styleProps.rx = [rx];
7520
+ styleProps.ry = [ry];
7521
+
7522
+ styleProps.width = [styleProps.width[0] * scaleX];
7523
+ styleProps.height = [styleProps.height[0] * scaleX];
7524
+ }
7525
+
7526
+ remove.push('transform');
7527
+
7528
+ // scale props like stroke width or dash-array
7529
+ styleProps = scaleProps(styleProps, { props: ['stroke-width', 'stroke-dasharray'], scale: scaleX });
7530
+
7531
+ } else {
7532
+ el.setAttribute('transform', transComponents.matrixAtt);
7533
+
7534
+ }
7535
+ }
7536
+
7537
+ /**
7538
+ * apply consolidated
7539
+ * element attributes
7540
+ */
7541
+
7542
+ let stylePropsFiltered = filterSvgElProps(name, styleProps,
7543
+ { removeDefaults: true, cleanUpStrokes });
7544
+
7545
+ remove = [...remove, ...stylePropsFiltered.remove];
7546
+
7547
+ for (let prop in stylePropsFiltered.propsFiltered) {
7548
+ let values = styleProps[prop];
7549
+
7550
+ let val = values.length ? values.join(' ') : values[0];
7551
+ el.setAttribute(prop, val);
7552
+ }
7553
+
7554
+ // remove obsolete attributes
7555
+ for (let i = 0; i < remove.length; i++) {
7556
+ let att = remove[i];
7557
+ if (!stylesToAttributes && att === 'style') continue
7558
+
7559
+ el.removeAttribute(att);
7560
+ }
7561
+
7562
+ /**
7563
+ * remove group styles
7564
+ * copied to children
7565
+ * or remove nesting
7566
+ */
7567
+
7568
+ if (unGroup) {
7569
+ groups.forEach((g, i) => {
7570
+ let children = [...g.children];
7571
+
7572
+ children.forEach(child => {
7573
+ g.parentNode.insertBefore(child, g);
7574
+ });
7575
+ g.remove();
7576
+ });
7577
+ } else {
7578
+ groups.forEach((g, i) => {
7579
+ let atts = [...Object.keys(groupProps[i]), 'style', 'transform'];
7580
+ atts.forEach(att => {
7581
+ g.removeAttribute(att);
7582
+ });
6637
7583
  });
7584
+
6638
7585
  }
7586
+
7587
+ } // endof style processing
7588
+
7589
+ /**
7590
+ * element conversions:
7591
+ * shapes to paths or
7592
+ * paths to shapes
7593
+ */
7594
+
7595
+ // force shape conversion when transform conversion is enabled
7596
+ if (convertTransforms) {
7597
+ shapeConvert = 'toPaths';
7598
+ convert_rects = true;
7599
+ convert_ellipses = true;
7600
+ convert_poly = true;
7601
+ convert_lines = true;
6639
7602
  }
6640
- }
6641
7603
 
6642
- // group styles
6643
- if (attributesToGroup || mergePaths) {
6644
- moveAttributesToGroup(svgElProps, mergePaths);
6645
- }
7604
+ // convert shapes to paths
7605
+ if (shapeConvert === 'toPaths') {
6646
7606
 
6647
- if (removeDimensions) {
6648
- svg.removeAttribute('width');
6649
- svg.removeAttribute('height');
6650
- }
7607
+ let { matrix = null, transComponents = null } = styleProps;
6651
7608
 
6652
- if (removeClassNames || removeIds) {
6653
- let att = removeClassNames ? 'class' : 'id';
6654
- let selector = `[${att}]`;
6655
- let els = svg.querySelectorAll(selector);
6656
- svg.removeAttribute(att);
6657
- els.forEach(el => {
6658
- el.removeAttribute(att);
6659
- });
6660
- }
7609
+ if (matrix && transComponents) {
7610
+ // scale props like stroke width or dash-array before conversion
7611
+ ['stroke-width', 'stroke-dasharray'].forEach(att => {
7612
+ let attVal = el.getAttribute(att);
7613
+ let vals = attVal ? attVal.split(' ').filter(Boolean).map(Number).map(val => val * transComponents.scaleX) : [];
7614
+ if (vals.length) el.setAttribute(att, vals.join(' '));
7615
+ });
7616
+ }
6661
7617
 
6662
- /**
6663
- * refine properties
6664
- * such as transforms or properties including units
6665
- */
7618
+ // convert paths only if a matrix transform is required
7619
+ if (matrix ? geometryEls.includes(name) : shapeEls.includes(name)) {
7620
+
7621
+ let path = shapeElToPath(el, { width, height, convert_rects, convert_ellipses, convert_poly, convert_lines, matrix });
7622
+ el.replaceWith(path);
6666
7623
 
6667
- /*
6668
- for(let i=0; i<svgElProps.length; i++){
6669
- let item = svgElProps[i];
6670
- let {propsFiltered} = item;
7624
+ name = 'path';
7625
+ el = path;
6671
7626
 
6672
- for(let prop in propsFiltered){
7627
+ }
7628
+
7629
+ }
6673
7630
 
6674
- let propOb = parseStyleProperty(prop, propsFiltered[prop])
7631
+ // convert paths to shapes
7632
+ else if (shapeConvert === 'toShapes') {
7633
+ let paths = svg.querySelectorAll('path');
7634
+ paths.forEach(path => {
7635
+ let shape = pathElToShape(path, { convert_rects, convert_ellipses, convert_poly, convert_lines });
7636
+ path.replaceWith(shape);
7637
+ path = shape;
7638
+ });
6675
7639
 
6676
7640
  }
6677
7641
 
6678
- }
6679
- */
7642
+ }//endof element loop
6680
7643
 
6681
7644
  // remove futile clip-paths
6682
7645
  if (cleanupClip) removeFutileClipPaths(svg, { x, y, width, height });
@@ -6806,139 +7769,32 @@
6806
7769
 
6807
7770
  }
6808
7771
 
6809
- function moveAttributesToGroup(svgElProps = [], mergePaths = true) {
6810
-
6811
- let combine = [[svgElProps[0]]];
6812
- let idx = 0;
6813
- let lastProps = '';
6814
- let l = svgElProps.length;
6815
- let itemsWithProps = svgElProps.filter(item => item.propstr);
6816
- let path0;
6817
-
6818
- // merge paths without properties
6819
- let dCombined = '';
6820
- if (!itemsWithProps.length && mergePaths) {
6821
- let path0 = null;
6822
-
6823
- for (let i = 0; i < l; i++) {
6824
- let item = svgElProps[i];
6825
- if (item.name !== 'path') continue;
6826
- let remove = true;
6827
-
6828
- let path = item.el;
6829
-
6830
- // set 1st path
6831
- if (!path0) {
6832
- path0 = path;
6833
- remove = false;
6834
- }
7772
+ function scaleProps(styleProps = {}, { props = [], scale = 1 } = {}) {
7773
+ if (scale === 1 || !props.length) return props;
6835
7774
 
6836
- let d = item.propsFiltered.d;
6837
- let isAbs = d.startsWith('M');
6838
- let dAbs = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ');
7775
+ for (let i = 0; i < props.length; i++) {
7776
+ let prop = props[i];
6839
7777
 
6840
- dCombined += dAbs;
6841
-
6842
- // delete path el
6843
- if (remove) path.remove();
6844
- }
6845
-
6846
- if (path0) path0.setAttribute('d', dCombined);
6847
- return
6848
- }
6849
-
6850
- // add to combine chunks
6851
- for (let i = 0; i < l; i++) {
6852
- let item = svgElProps[i];
6853
- let props = item.propsFiltered;
6854
- let propstr = [];
6855
- for (let prop in props) {
6856
- if (prop !== 'd' && prop !== 'id') {
6857
- propstr.push(`${prop}:${props[prop]}`);
6858
- }
6859
- }
6860
- propstr = propstr.join('_');
6861
- item.propstr = propstr;
6862
-
6863
- if (l > 1 && propstr === lastProps) {
6864
- combine[idx].push(item);
6865
- } else {
6866
- if (l > 1 && combine[idx].length) {
6867
- combine.push([]);
6868
- idx++;
6869
- }
7778
+ if (styleProps[prop] !== undefined) {
7779
+ styleProps[prop] = styleProps[prop].map(val => val * scale);
6870
7780
  }
6871
- lastProps = propstr;
6872
7781
  }
7782
+ return styleProps
7783
+ }
6873
7784
 
6874
- // add att groups
6875
- for (let i = 0; i < combine.length; i++) {
6876
- let group = combine[i];
6877
-
6878
- if (group.length > 1) {
6879
- // 1st el
6880
- let el0 = group[0].el;
6881
- let props = group[0].propsFiltered;
6882
- let g = el0.parentNode.closest('g') ? el0.parentNode.closest('g') : null;
6883
-
6884
- // wrap in group if not existent
6885
- if (!g) {
6886
- g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
6887
- el0.parentNode.insertBefore(g, el0);
6888
- group.forEach(item => {
6889
- g.append(item.el);
6890
- });
6891
- }
6892
-
6893
- let children = [...g.children];
6894
- for (let prop in props) {
6895
- if (prop !== 'd' && prop !== 'id') {
6896
- let value = props[prop];
6897
- // apply to parent group
6898
- g.setAttribute(prop, value);
6899
-
6900
- // remove from children
6901
- children.forEach(el => {
6902
- if (el.getAttribute(prop) === value) {
6903
- el.removeAttribute(prop);
6904
- }
6905
- });
6906
- }
6907
-
6908
- if (mergePaths) {
6909
- group = group.filter(Boolean);
6910
- let l = group.length;
6911
- // nothing to merge
6912
- if (l === 1) return group[0].el;
6913
-
6914
- path0 = group[0].el;
6915
- let dCombined = group[0].propsFiltered.d;
6916
-
6917
- for (let i = 1; i < l; i++) {
6918
- let item = group[i];
6919
- let path = item.el;
6920
- let d = item.propsFiltered.d;
6921
- let isAbs = d.startsWith('M');
6922
-
6923
- let dAbs = isAbs ? d : parsePathDataString(d).pathData.map(com => `${com.type} ${com.values.join(' ')}`).join(' ');
6924
-
6925
- console.log('dAbs', dAbs);
6926
-
6927
- // concat pathdata string
6928
- dCombined += dAbs;
6929
-
6930
- // delete path el
6931
- path.remove();
6932
- }
6933
-
6934
- path0.setAttribute('d', dCombined);
6935
-
6936
- }
6937
-
6938
- }
7785
+ function removeSVGEls(svg, {
7786
+ remove = ['metadata', 'script'],
7787
+ removeNameSpaced = true,
7788
+ } = {}) {
7789
+ let els = svg.querySelectorAll('*');
7790
+ els.forEach(el => {
7791
+ let nodeName = el.nodeName;
7792
+ if ((removeNameSpaced && nodeName.includes(':')) ||
7793
+ remove.includes(nodeName)
7794
+ ) {
7795
+ el.remove();
6939
7796
  }
6940
- }
6941
-
7797
+ });
6942
7798
  }
6943
7799
 
6944
7800
  function removeExcludedAttribues(el, allowed = ['viewBox', 'xmlns', 'width', 'height', 'id', 'class']) {
@@ -6950,13 +7806,21 @@
6950
7806
  });
6951
7807
  }
6952
7808
 
6953
- function stringifySVG(svg, omitNamespace = false) {
7809
+ function stringifySVG(svg, {
7810
+ omitNamespace = false,
7811
+ removeComments = true,
7812
+ } = {}) {
6954
7813
  let markup = new XMLSerializer().serializeToString(svg);
6955
7814
 
6956
7815
  if (omitNamespace) {
6957
7816
  markup = markup.replaceAll('xmlns="http://www.w3.org/2000/svg"', '');
6958
7817
  }
6959
7818
 
7819
+ if (removeComments) {
7820
+ markup = markup
7821
+ .replace(/(<!--.*?-->)|(<!--[\S\s]+?-->)|(<!--[\S\s]*?$)/g, '');
7822
+ }
7823
+
6960
7824
  markup = markup
6961
7825
  .replace(/\t/g, "")
6962
7826
  .replace(/[\n\r|]/g, "\n")
@@ -6965,7 +7829,7 @@
6965
7829
 
6966
7830
  .replace(/> </g, '><')
6967
7831
  .trim()
6968
- // sanitize linebreaks within pathdata
7832
+ // sanitize linebreaks within pathdata
6969
7833
  .replaceAll('&#10;', '\n');
6970
7834
 
6971
7835
  return markup
@@ -8814,7 +9678,7 @@
8814
9678
  poly = poly.map(pt => { return [pt.x, pt.y] });
8815
9679
  }
8816
9680
  else if(polyFormat==='string'){
8817
- poly = poly.map(pt => { return [pt.x, pt.y].join(',') }).join(' ');
9681
+ poly = poly.map(pt => { return [pt.x, pt.y].join(',') }).flat().join(' ');
8818
9682
  }
8819
9683
 
8820
9684
  return { pathData, poly }
@@ -9031,6 +9895,8 @@
9031
9895
  scaleTo = 0,
9032
9896
  crop = false,
9033
9897
  alignToOrigin = false,
9898
+
9899
+ // flatten transforms
9034
9900
  convertTransforms = false,
9035
9901
 
9036
9902
  decimals = 3,
@@ -9042,20 +9908,21 @@
9042
9908
  tolerance = 1,
9043
9909
  reversePath = false,
9044
9910
 
9911
+ minifyRgbColors = false,
9045
9912
  removePrologue = true,
9046
9913
  removeHidden = true,
9047
9914
  removeUnused = true,
9048
9915
  cleanupDefs = true,
9049
9916
  cleanupClip = true,
9050
9917
  cleanupSVGAtts = true,
9051
-
9918
+
9052
9919
  stylesToAttributes = false,
9053
9920
  fixHref = false,
9054
9921
  legacyHref = false,
9055
9922
  removeNameSpaced = true,
9056
- attributesToGroup = false,
9057
- removeOffCanvas = false,
9058
9923
 
9924
+ removeOffCanvas = false,
9925
+ unGroup = false,
9059
9926
  mergePaths = false,
9060
9927
 
9061
9928
  // shape conversions
@@ -9072,6 +9939,8 @@
9072
9939
  addViewBox = false,
9073
9940
  addDimensions = false,
9074
9941
 
9942
+ removeComments = true,
9943
+
9075
9944
  } = {}) {
9076
9945
 
9077
9946
  // clamp tolerance and scale
@@ -9150,10 +10019,18 @@
9150
10019
  // mode:1 – process complete svg DOM
9151
10020
  else {
9152
10021
 
10022
+ // convert all shapes to paths
10023
+ if (shapesToPaths) {
10024
+ shapeConvert = true;
10025
+ convert_rects = true;
10026
+ convert_ellipses = true;
10027
+ convert_poly = true;
10028
+ convert_lines = true;
10029
+ }
10030
+
9153
10031
  let svgPropObject = cleanUpSVG(input, {
9154
10032
  removeIds, removeClassNames, removeDimensions, cleanupSVGAtts, cleanUpStrokes, removeHidden, removeUnused, removeNameSpaced, stylesToAttributes, removePrologue, fixHref, mergePaths, convertTransforms, legacyHref, cleanupDefs, cleanupClip, addViewBox, removeOffCanvas, addDimensions,
9155
- shapeConvert, convert_rects, convert_ellipses, convert_poly, convert_lines
9156
-
10033
+ shapeConvert, convert_rects, convert_ellipses, convert_poly, convert_lines, minifyRgbColors, unGroup, convertTransforms
9157
10034
  }
9158
10035
  );
9159
10036
  svg = svgPropObject.svg;
@@ -9435,8 +10312,8 @@
9435
10312
 
9436
10313
  if (autoAccuracy) {
9437
10314
  accuracyArr = accuracyArr.sort().reverse();
9438
- let decimalsMid = accuracyArr[Math.floor(accuracyArr.length*0.5)];
9439
- decimals = Math.floor( (accuracyArr[0] + decimalsMid) * 0.5 );
10315
+ let decimalsMid = accuracyArr[Math.floor(accuracyArr.length * 0.5)];
10316
+ decimals = Math.floor((accuracyArr[0] + decimalsMid) * 0.5);
9440
10317
 
9441
10318
  pathOptions.decimals = decimals;
9442
10319
  }
@@ -9559,7 +10436,7 @@
9559
10436
  });
9560
10437
  }
9561
10438
 
9562
- svg = stringifySVG(svg, omitNamespace);
10439
+ svg = stringifySVG(svg, { omitNamespace, removeComments });
9563
10440
 
9564
10441
  svgSizeOpt = svg.length;
9565
10442