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