From 26737da28e6f4858abacb3d3acd9aa4452f0e3b4 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 26 Nov 2023 08:28:55 -0800 Subject: [PATCH 01/15] feat(applyTransformsShapes): new plugin --- lib/builtin.js | 2 + plugins/applyTransformsShapes.js | 213 +++++++++++++++++++++++++++++++ plugins/plugins-types.d.ts | 4 + plugins/preset-default.js | 2 + 4 files changed, 221 insertions(+) create mode 100644 plugins/applyTransformsShapes.js diff --git a/lib/builtin.js b/lib/builtin.js index 57e8bae64..4e8796a7b 100644 --- a/lib/builtin.js +++ b/lib/builtin.js @@ -1,6 +1,7 @@ import presetDefault from '../plugins/preset-default.js'; import * as addAttributesToSVGElement from '../plugins/addAttributesToSVGElement.js'; import * as addClassesToSVGElement from '../plugins/addClassesToSVGElement.js'; +import * as applyTransformsShapes from '../plugins/applyTransformsShapes.js'; import * as cleanupAttrs from '../plugins/cleanupAttrs.js'; import * as cleanupEnableBackground from '../plugins/cleanupEnableBackground.js'; import * as cleanupIds from '../plugins/cleanupIds.js'; @@ -57,6 +58,7 @@ export const builtin = [ presetDefault, addAttributesToSVGElement, addClassesToSVGElement, + applyTransformsShapes, cleanupAttrs, cleanupEnableBackground, cleanupIds, diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js new file mode 100644 index 000000000..9a9c9302e --- /dev/null +++ b/plugins/applyTransformsShapes.js @@ -0,0 +1,213 @@ +const { collectStylesheet, computeStyle } = require('../lib/style.js'); +const { removeLeadingZero } = require('../lib/svgo/tools.js'); +const { attrsGroupsDefaults } = require('./_collections.js'); +const { transformsMultiply, transform2js } = require('./_transforms.js'); + +exports.name = 'applyTransformsShapes'; +exports.description = 'Applies transforms to some shapes.'; + +const APPLICABLE_SHAPES = ['circle', 'ellipse', 'rect']; + +/** + * @typedef {number[]} Matrix + */ + +/** + * Applies transforms to some shapes. + * + * @author Kendell R + * + * @type {import('./plugins-types').Plugin<'applyTransformsShapes'>} + */ +exports.fn = (root, params) => { + const { floatPrecision = 3, leadingZero = true } = params; + const factor = Math.pow(10, floatPrecision); + const stylesheet = collectStylesheet(root); + return { + element: { + enter: (node) => { + if (!APPLICABLE_SHAPES.includes(node.name)) { + return; + } + + const computedStyle = computeStyle(stylesheet, node); + const transformStyle = computedStyle.transform; + if (!transformStyle) return; + if ( + transformStyle.type === 'static' && + transformStyle.value !== node.attributes.transform + ) { + return; + } + if ( + computedStyle.stroke?.type === 'dynamic' || + computedStyle['stroke-width']?.type === 'dynamic' + ) { + return; + } + + const matrix = transformsMultiply( + transform2js(node.attributes.transform) + ); + const hasStroke = + computedStyle.stroke && computedStyle.stroke.value !== 'none'; + const strokeWidth = Number( + computedStyle['stroke-width']?.value || + attrsGroupsDefaults.presentation['stroke-width'] + ); + + const isSimilar = + (matrix.data[0] === matrix.data[3] && + matrix.data[1] === -matrix.data[2]) || + (matrix.data[0] === -matrix.data[3] && + matrix.data[1] === matrix.data[2]); + const isLinear = + (matrix.data[0] != 0 && + matrix.data[1] == 0 && + matrix.data[2] == 0 && + matrix.data[3] != 0) || + (matrix.data[0] == 0 && + matrix.data[1] != 0 && + matrix.data[2] != 0 && + matrix.data[3] == 0); + const scale = Math.sqrt( + matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1] + ); + if (node.name == 'circle') { + if (!isSimilar) return; + + const cx = Number(node.attributes.cx || '0'); + const cy = Number(node.attributes.cy || '0'); + const r = Number(node.attributes.r || '0'); + + const newCenter = transformAbsolutePoint(matrix.data, cx, cy); + if (hasStroke) { + node.attributes['stroke-width'] = stringifyNumber( + strokeWidth * scale, + factor, + leadingZero + ); + } + node.attributes.cx = stringifyNumber( + newCenter[0], + factor, + leadingZero + ); + node.attributes.cy = stringifyNumber( + newCenter[1], + factor, + leadingZero + ); + node.attributes.r = stringifyNumber(r * scale, factor, leadingZero); + delete node.attributes.transform; + } else if (node.name == 'ellipse') { + if (!isLinear) return; + if (hasStroke && !isSimilar) return; + + const cx = Number(node.attributes.cx || '0'); + const cy = Number(node.attributes.cy || '0'); + const rx = Number(node.attributes.rx || '0'); + const ry = Number(node.attributes.ry || '0'); + + const newCenter = transformAbsolutePoint(matrix.data, cx, cy); + const [newRx, newRy] = transformSize(matrix.data, rx, ry); + + if (hasStroke) { + node.attributes['stroke-width'] = stringifyNumber( + strokeWidth * scale, + factor, + leadingZero + ); + } + node.attributes.cx = stringifyNumber( + newCenter[0], + factor, + leadingZero + ); + node.attributes.cy = stringifyNumber( + newCenter[1], + factor, + leadingZero + ); + node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); + node.attributes.ry = stringifyNumber(newRy, factor, leadingZero); + delete node.attributes.transform; + } else if (node.name == 'rect') { + if (!isLinear) return; + if (hasStroke && !isSimilar) return; + + const x = Number(node.attributes.x || '0'); + const y = Number(node.attributes.y || '0'); + const width = Number(node.attributes.width || '0'); + const height = Number(node.attributes.height || '0'); + let rx = node.attributes.rx ? Number(node.attributes.rx) : NaN; + let ry = node.attributes.ry ? Number(node.attributes.ry) : NaN; + rx = Number.isNaN(rx) ? ry || 0 : rx; + ry = Number.isNaN(ry) ? rx || 0 : ry; + + const cornerA = transformAbsolutePoint(matrix.data, x, y); + const cornerB = transformAbsolutePoint( + matrix.data, + x + width, + y + height + ); + const cornerX = Math.min(cornerA[0], cornerB[0]); + const cornerY = Math.min(cornerA[1], cornerB[1]); + const [newW, newH] = transformSize(matrix.data, width, height); + const [newRx, newRy] = transformSize(matrix.data, rx, ry); + + if (hasStroke) { + node.attributes['stroke-width'] = stringifyNumber( + strokeWidth * scale, + factor, + leadingZero + ); + } + node.attributes.x = stringifyNumber(cornerX, factor, leadingZero); + node.attributes.y = stringifyNumber(cornerY, factor, leadingZero); + node.attributes.width = stringifyNumber(newW, factor, leadingZero); + node.attributes.height = stringifyNumber(newH, factor, leadingZero); + if (newRx != undefined) + node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); + else delete node.attributes.rx; + if (newRy != undefined && Math.abs(newRx - newRy) > 1 / factor) + node.attributes.ry = stringifyNumber(newRy, factor, leadingZero); + else delete node.attributes.ry; + delete node.attributes.transform; + } + }, + }, + }; +}; + +/** + * @param {Matrix} matrix + * @param {number} x + * @param {number} y + */ +const transformAbsolutePoint = (matrix, x, y) => { + const newX = matrix[0] * x + matrix[2] * y + matrix[4]; + const newY = matrix[1] * x + matrix[3] * y + matrix[5]; + return [newX, newY]; +}; + +/** + * @param {Matrix} matrix + * @param {number} w + * @param {number} h + */ +const transformSize = (matrix, w, h) => { + const newW = matrix[0] * w + matrix[1] * h; + const newH = matrix[2] * w + matrix[3] * h; + return [Math.abs(newW), Math.abs(newH)]; +}; + +/** + * @param {number} number + * @param {number} factor + * @param {boolean} leadingZero + */ +const stringifyNumber = (number, factor, leadingZero) => { + const rounded = Math.round(number * factor) / factor; + return leadingZero ? removeLeadingZero(rounded) : rounded.toString(); +}; diff --git a/plugins/plugins-types.d.ts b/plugins/plugins-types.d.ts index 246ea6249..2e7687c22 100644 --- a/plugins/plugins-types.d.ts +++ b/plugins/plugins-types.d.ts @@ -5,6 +5,10 @@ import type { } from '../lib/types.js'; type DefaultPlugins = { + applyTransformsShapes: { + floatPrecision?: number; + leadingZero?: boolean; + }; cleanupAttrs: { newlines?: boolean; trim?: boolean; diff --git a/plugins/preset-default.js b/plugins/preset-default.js index 5291ab6c2..1603e913f 100644 --- a/plugins/preset-default.js +++ b/plugins/preset-default.js @@ -27,6 +27,7 @@ import * as moveGroupAttrsToElems from './moveGroupAttrsToElems.js'; import * as collapseGroups from './collapseGroups.js'; import * as convertPathData from './convertPathData.js'; import * as convertTransform from './convertTransform.js'; +import * as applyTransformsShapes from './applyTransformsShapes.js'; import * as removeEmptyAttrs from './removeEmptyAttrs.js'; import * as removeEmptyContainers from './removeEmptyContainers.js'; import * as mergePaths from './mergePaths.js'; @@ -66,6 +67,7 @@ const presetDefault = createPreset({ moveGroupAttrsToElems, collapseGroups, convertPathData, + applyTransformsShapes, convertTransform, removeEmptyAttrs, removeEmptyContainers, From 6f4cf288dcdeaa52c8354e9785ab773d3ee4d553 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 26 Nov 2023 09:12:51 -0800 Subject: [PATCH 02/15] always transform stroke-width to fix tests --- plugins/applyTransformsShapes.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index 9a9c9302e..5abd729f6 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -53,7 +53,7 @@ exports.fn = (root, params) => { computedStyle.stroke && computedStyle.stroke.value !== 'none'; const strokeWidth = Number( computedStyle['stroke-width']?.value || - attrsGroupsDefaults.presentation['stroke-width'] + (hasStroke && attrsGroupsDefaults.presentation['stroke-width']) ); const isSimilar = @@ -81,7 +81,7 @@ exports.fn = (root, params) => { const r = Number(node.attributes.r || '0'); const newCenter = transformAbsolutePoint(matrix.data, cx, cy); - if (hasStroke) { + if (strokeWidth) { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, factor, @@ -102,7 +102,7 @@ exports.fn = (root, params) => { delete node.attributes.transform; } else if (node.name == 'ellipse') { if (!isLinear) return; - if (hasStroke && !isSimilar) return; + if (strokeWidth && !isSimilar) return; const cx = Number(node.attributes.cx || '0'); const cy = Number(node.attributes.cy || '0'); @@ -112,7 +112,7 @@ exports.fn = (root, params) => { const newCenter = transformAbsolutePoint(matrix.data, cx, cy); const [newRx, newRy] = transformSize(matrix.data, rx, ry); - if (hasStroke) { + if (strokeWidth) { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, factor, @@ -134,7 +134,7 @@ exports.fn = (root, params) => { delete node.attributes.transform; } else if (node.name == 'rect') { if (!isLinear) return; - if (hasStroke && !isSimilar) return; + if (strokeWidth && !isSimilar) return; const x = Number(node.attributes.x || '0'); const y = Number(node.attributes.y || '0'); @@ -156,7 +156,7 @@ exports.fn = (root, params) => { const [newW, newH] = transformSize(matrix.data, width, height); const [newRx, newRy] = transformSize(matrix.data, rx, ry); - if (hasStroke) { + if (strokeWidth) { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, factor, From 553cdb29bf40229a6906896cbf68dc0b58120d6d Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 26 Nov 2023 18:12:25 -0800 Subject: [PATCH 03/15] add doc page --- docs/03-plugins/apply-transforms-shapes.mdx | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/03-plugins/apply-transforms-shapes.mdx diff --git a/docs/03-plugins/apply-transforms-shapes.mdx b/docs/03-plugins/apply-transforms-shapes.mdx new file mode 100644 index 000000000..f221295bf --- /dev/null +++ b/docs/03-plugins/apply-transforms-shapes.mdx @@ -0,0 +1,31 @@ +--- +title: Apply Transforms to Shapes +svgo: + pluginId: applyTransformsShapes + parameters: + floatPrecision: + description: Number of decimal places to round to, using conventional rounding rules. + default: 3 + leadingZero: + description: If to trim leading zeros. + default: true + defaultPlugin: true +--- + +Applies the `transform` attribute directly to ``, ``, and `` when possible. + +## Usage + + + +### Parameters + + + +## Demo + + + +## Implementation + +- https://github.com/svg/svgo/blob/main/plugins/applyTransformsShapes.js From 3a8df8675a49f5e278d00c3548e2c9b00043ba1f Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 3 Dec 2023 10:15:16 -0800 Subject: [PATCH 04/15] transform images too --- docs/03-plugins/apply-transforms-shapes.mdx | 2 +- plugins/applyTransformsShapes.js | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/03-plugins/apply-transforms-shapes.mdx b/docs/03-plugins/apply-transforms-shapes.mdx index f221295bf..34bbafb7b 100644 --- a/docs/03-plugins/apply-transforms-shapes.mdx +++ b/docs/03-plugins/apply-transforms-shapes.mdx @@ -12,7 +12,7 @@ svgo: defaultPlugin: true --- -Applies the `transform` attribute directly to ``, ``, and `` when possible. +Applies the `transform` attribute directly to ``, ``, ``, and `` when possible. ## Usage diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index 5abd729f6..e16e426c9 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -6,7 +6,7 @@ const { transformsMultiply, transform2js } = require('./_transforms.js'); exports.name = 'applyTransformsShapes'; exports.description = 'Applies transforms to some shapes.'; -const APPLICABLE_SHAPES = ['circle', 'ellipse', 'rect']; +const APPLICABLE_SHAPES = ['circle', 'ellipse', 'rect', 'image']; /** * @typedef {number[]} Matrix @@ -32,9 +32,9 @@ exports.fn = (root, params) => { const computedStyle = computeStyle(stylesheet, node); const transformStyle = computedStyle.transform; - if (!transformStyle) return; if ( - transformStyle.type === 'static' && + !transformStyle || + transformStyle.type !== 'static' || transformStyle.value !== node.attributes.transform ) { return; @@ -70,6 +70,11 @@ exports.fn = (root, params) => { matrix.data[1] != 0 && matrix.data[2] != 0 && matrix.data[3] == 0); + const isTranslation = + matrix.data[0] == 1 && + matrix.data[1] == 0 && + matrix.data[2] == 0 && + matrix.data[3] == 1; const scale = Math.sqrt( matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1] ); @@ -174,6 +179,16 @@ exports.fn = (root, params) => { node.attributes.ry = stringifyNumber(newRy, factor, leadingZero); else delete node.attributes.ry; delete node.attributes.transform; + } else if (node.name == 'image') { + if (!isTranslation) return; + + const x = Number(node.attributes.x || '0'); + const y = Number(node.attributes.y || '0'); + const corner = transformAbsolutePoint(matrix.data, x, y); + + node.attributes.x = stringifyNumber(corner[0], factor, leadingZero); + node.attributes.y = stringifyNumber(corner[1], factor, leadingZero); + delete node.attributes.transform; } }, }, From 019833b6795ddc9b647b9d42b37dfdbed7b691eb Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 3 Dec 2023 10:28:23 -0800 Subject: [PATCH 05/15] exclude filtered shapes --- plugins/applyTransformsShapes.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index e16e426c9..06cc5bd6a 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -31,17 +31,18 @@ exports.fn = (root, params) => { } const computedStyle = computeStyle(stylesheet, node); - const transformStyle = computedStyle.transform; + if (computedStyle.filter) return; if ( - !transformStyle || - transformStyle.type !== 'static' || - transformStyle.value !== node.attributes.transform + computedStyle.stroke?.type === 'dynamic' || + computedStyle['stroke-width']?.type === 'dynamic' ) { return; } + + const transformStyle = computedStyle.transform; if ( - computedStyle.stroke?.type === 'dynamic' || - computedStyle['stroke-width']?.type === 'dynamic' + transformStyle?.type !== 'static' || + transformStyle.value !== node.attributes.transform ) { return; } From 36788360aae421afc480a9f447778a0d73ba46c5 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 3 Dec 2023 11:02:40 -0800 Subject: [PATCH 06/15] add tests --- plugins/applyTransformsShapes.js | 12 ++++++++---- test/plugins/applyTransformsShapes.01.svg | 19 +++++++++++++++++++ test/plugins/applyTransformsShapes.02.svg | 19 +++++++++++++++++++ test/plugins/applyTransformsShapes.03.svg | 19 +++++++++++++++++++ test/plugins/applyTransformsShapes.04.svg | 11 +++++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 test/plugins/applyTransformsShapes.01.svg create mode 100644 test/plugins/applyTransformsShapes.02.svg create mode 100644 test/plugins/applyTransformsShapes.03.svg create mode 100644 test/plugins/applyTransformsShapes.04.svg diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index 06cc5bd6a..983883c79 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -173,12 +173,16 @@ exports.fn = (root, params) => { node.attributes.y = stringifyNumber(cornerY, factor, leadingZero); node.attributes.width = stringifyNumber(newW, factor, leadingZero); node.attributes.height = stringifyNumber(newH, factor, leadingZero); - if (newRx != undefined) + if (newRx < 1 / factor && newRy < 1 / factor) { + delete node.attributes.rx; + delete node.attributes.ry; + } else if (Math.abs(newRx - newRy) < 1 / factor) { + node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); + delete node.attributes.ry; + } else { node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); - else delete node.attributes.rx; - if (newRy != undefined && Math.abs(newRx - newRy) > 1 / factor) node.attributes.ry = stringifyNumber(newRy, factor, leadingZero); - else delete node.attributes.ry; + } delete node.attributes.transform; } else if (node.name == 'image') { if (!isTranslation) return; diff --git a/test/plugins/applyTransformsShapes.01.svg b/test/plugins/applyTransformsShapes.01.svg new file mode 100644 index 000000000..d2470ae6d --- /dev/null +++ b/test/plugins/applyTransformsShapes.01.svg @@ -0,0 +1,19 @@ + + + + + + + + + +@@@ + + + + + + + + + diff --git a/test/plugins/applyTransformsShapes.02.svg b/test/plugins/applyTransformsShapes.02.svg new file mode 100644 index 000000000..bd264e303 --- /dev/null +++ b/test/plugins/applyTransformsShapes.02.svg @@ -0,0 +1,19 @@ + + + + + + + + + +@@@ + + + + + + + + + diff --git a/test/plugins/applyTransformsShapes.03.svg b/test/plugins/applyTransformsShapes.03.svg new file mode 100644 index 000000000..769e75daf --- /dev/null +++ b/test/plugins/applyTransformsShapes.03.svg @@ -0,0 +1,19 @@ + + + + + + + + + +@@@ + + + + + + + + + diff --git a/test/plugins/applyTransformsShapes.04.svg b/test/plugins/applyTransformsShapes.04.svg new file mode 100644 index 000000000..075a2a1a9 --- /dev/null +++ b/test/plugins/applyTransformsShapes.04.svg @@ -0,0 +1,11 @@ + + + + + +@@@ + + + + + From b1e03dfdb41f70f8be61466cfcb8217a2fd8eea3 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 3 Dec 2023 14:53:22 -0800 Subject: [PATCH 07/15] consistent --- test/plugins/applyTransformsShapes.04.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugins/applyTransformsShapes.04.svg b/test/plugins/applyTransformsShapes.04.svg index 075a2a1a9..f02ed1098 100644 --- a/test/plugins/applyTransformsShapes.04.svg +++ b/test/plugins/applyTransformsShapes.04.svg @@ -1,11 +1,11 @@ - + @@@ - + From ecf814406a5723cd2fb4c11c644ea783c027346c Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sun, 3 Dec 2023 15:06:08 -0800 Subject: [PATCH 08/15] fix namespace --- test/plugins/applyTransformsShapes.04.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/plugins/applyTransformsShapes.04.svg b/test/plugins/applyTransformsShapes.04.svg index f02ed1098..114a918c5 100644 --- a/test/plugins/applyTransformsShapes.04.svg +++ b/test/plugins/applyTransformsShapes.04.svg @@ -1,11 +1,11 @@ - - + + @@@ - - + + From 87f00daa6ae479ff63db8a0021a5a67530a576fb Mon Sep 17 00:00:00 2001 From: Kendell R Date: Wed, 20 Dec 2023 10:29:13 -0500 Subject: [PATCH 09/15] format --- plugins/applyTransformsShapes.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index 983883c79..78d14ade1 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -48,13 +48,13 @@ exports.fn = (root, params) => { } const matrix = transformsMultiply( - transform2js(node.attributes.transform) + transform2js(node.attributes.transform), ); const hasStroke = computedStyle.stroke && computedStyle.stroke.value !== 'none'; const strokeWidth = Number( computedStyle['stroke-width']?.value || - (hasStroke && attrsGroupsDefaults.presentation['stroke-width']) + (hasStroke && attrsGroupsDefaults.presentation['stroke-width']), ); const isSimilar = @@ -77,7 +77,7 @@ exports.fn = (root, params) => { matrix.data[2] == 0 && matrix.data[3] == 1; const scale = Math.sqrt( - matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1] + matrix.data[0] * matrix.data[0] + matrix.data[1] * matrix.data[1], ); if (node.name == 'circle') { if (!isSimilar) return; @@ -91,18 +91,18 @@ exports.fn = (root, params) => { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, factor, - leadingZero + leadingZero, ); } node.attributes.cx = stringifyNumber( newCenter[0], factor, - leadingZero + leadingZero, ); node.attributes.cy = stringifyNumber( newCenter[1], factor, - leadingZero + leadingZero, ); node.attributes.r = stringifyNumber(r * scale, factor, leadingZero); delete node.attributes.transform; @@ -122,18 +122,18 @@ exports.fn = (root, params) => { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, factor, - leadingZero + leadingZero, ); } node.attributes.cx = stringifyNumber( newCenter[0], factor, - leadingZero + leadingZero, ); node.attributes.cy = stringifyNumber( newCenter[1], factor, - leadingZero + leadingZero, ); node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); node.attributes.ry = stringifyNumber(newRy, factor, leadingZero); @@ -155,7 +155,7 @@ exports.fn = (root, params) => { const cornerB = transformAbsolutePoint( matrix.data, x + width, - y + height + y + height, ); const cornerX = Math.min(cornerA[0], cornerB[0]); const cornerY = Math.min(cornerA[1], cornerB[1]); @@ -166,7 +166,7 @@ exports.fn = (root, params) => { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, factor, - leadingZero + leadingZero, ); } node.attributes.x = stringifyNumber(cornerX, factor, leadingZero); From 2a1854a716ce59096703bc2f0e5f0fe121f6761e Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 29 Feb 2024 16:23:08 -0800 Subject: [PATCH 10/15] apply reverted changes and stuff from review --- plugins/applyTransformsShapes.js | 121 +++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index 78d14ade1..cf6f2f7a6 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -1,27 +1,22 @@ -const { collectStylesheet, computeStyle } = require('../lib/style.js'); -const { removeLeadingZero } = require('../lib/svgo/tools.js'); -const { attrsGroupsDefaults } = require('./_collections.js'); -const { transformsMultiply, transform2js } = require('./_transforms.js'); +import { collectStylesheet, computeStyle } from '../lib/style.js'; +import { toFixed, removeLeadingZero } from '../lib/svgo/tools.js'; +import { attrsGroupsDefaults } from './_collections.js'; +import { transform2js, transformsMultiply } from './_transforms.js'; -exports.name = 'applyTransformsShapes'; -exports.description = 'Applies transforms to some shapes.'; +export const name = 'applyTransformsShapes'; +export const description = 'Applies transforms to some shapes.'; const APPLICABLE_SHAPES = ['circle', 'ellipse', 'rect', 'image']; -/** - * @typedef {number[]} Matrix - */ - /** * Applies transforms to some shapes. * * @author Kendell R * - * @type {import('./plugins-types').Plugin<'applyTransformsShapes'>} + * @type {import('./plugins-types.js').Plugin<'applyTransformsShapes'>} */ -exports.fn = (root, params) => { +export const fn = (root, params) => { const { floatPrecision = 3, leadingZero = true } = params; - const factor = Math.pow(10, floatPrecision); const stylesheet = collectStylesheet(root); return { element: { @@ -90,21 +85,25 @@ exports.fn = (root, params) => { if (strokeWidth) { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, - factor, + floatPrecision, leadingZero, ); } node.attributes.cx = stringifyNumber( newCenter[0], - factor, + floatPrecision, leadingZero, ); node.attributes.cy = stringifyNumber( newCenter[1], - factor, + floatPrecision, + leadingZero, + ); + node.attributes.r = stringifyNumber( + r * scale, + floatPrecision, leadingZero, ); - node.attributes.r = stringifyNumber(r * scale, factor, leadingZero); delete node.attributes.transform; } else if (node.name == 'ellipse') { if (!isLinear) return; @@ -121,22 +120,30 @@ exports.fn = (root, params) => { if (strokeWidth) { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, - factor, + floatPrecision, leadingZero, ); } node.attributes.cx = stringifyNumber( newCenter[0], - factor, + floatPrecision, leadingZero, ); node.attributes.cy = stringifyNumber( newCenter[1], - factor, + floatPrecision, + leadingZero, + ); + node.attributes.rx = stringifyNumber( + newRx, + floatPrecision, + leadingZero, + ); + node.attributes.ry = stringifyNumber( + newRy, + floatPrecision, leadingZero, ); - node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); - node.attributes.ry = stringifyNumber(newRy, factor, leadingZero); delete node.attributes.transform; } else if (node.name == 'rect') { if (!isLinear) return; @@ -165,23 +172,51 @@ exports.fn = (root, params) => { if (strokeWidth) { node.attributes['stroke-width'] = stringifyNumber( strokeWidth * scale, - factor, + floatPrecision, leadingZero, ); } - node.attributes.x = stringifyNumber(cornerX, factor, leadingZero); - node.attributes.y = stringifyNumber(cornerY, factor, leadingZero); - node.attributes.width = stringifyNumber(newW, factor, leadingZero); - node.attributes.height = stringifyNumber(newH, factor, leadingZero); - if (newRx < 1 / factor && newRy < 1 / factor) { + node.attributes.x = stringifyNumber( + cornerX, + floatPrecision, + leadingZero, + ); + node.attributes.y = stringifyNumber( + cornerY, + floatPrecision, + leadingZero, + ); + node.attributes.width = stringifyNumber( + newW, + floatPrecision, + leadingZero, + ); + node.attributes.height = stringifyNumber( + newH, + floatPrecision, + leadingZero, + ); + if (newRx < 1 / floatPrecision && newRy < 1 / floatPrecision) { delete node.attributes.rx; delete node.attributes.ry; - } else if (Math.abs(newRx - newRy) < 1 / factor) { - node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); + } else if (Math.abs(newRx - newRy) < 1 / floatPrecision) { + node.attributes.rx = stringifyNumber( + newRx, + floatPrecision, + leadingZero, + ); delete node.attributes.ry; } else { - node.attributes.rx = stringifyNumber(newRx, factor, leadingZero); - node.attributes.ry = stringifyNumber(newRy, factor, leadingZero); + node.attributes.rx = stringifyNumber( + newRx, + floatPrecision, + leadingZero, + ); + node.attributes.ry = stringifyNumber( + newRy, + floatPrecision, + leadingZero, + ); } delete node.attributes.transform; } else if (node.name == 'image') { @@ -191,8 +226,16 @@ exports.fn = (root, params) => { const y = Number(node.attributes.y || '0'); const corner = transformAbsolutePoint(matrix.data, x, y); - node.attributes.x = stringifyNumber(corner[0], factor, leadingZero); - node.attributes.y = stringifyNumber(corner[1], factor, leadingZero); + node.attributes.x = stringifyNumber( + corner[0], + floatPrecision, + leadingZero, + ); + node.attributes.y = stringifyNumber( + corner[1], + floatPrecision, + leadingZero, + ); delete node.attributes.transform; } }, @@ -201,7 +244,7 @@ exports.fn = (root, params) => { }; /** - * @param {Matrix} matrix + * @param {number[]} matrix * @param {number} x * @param {number} y */ @@ -212,7 +255,7 @@ const transformAbsolutePoint = (matrix, x, y) => { }; /** - * @param {Matrix} matrix + * @param {number[]} matrix * @param {number} w * @param {number} h */ @@ -224,10 +267,10 @@ const transformSize = (matrix, w, h) => { /** * @param {number} number - * @param {number} factor + * @param {number} precision * @param {boolean} leadingZero */ -const stringifyNumber = (number, factor, leadingZero) => { - const rounded = Math.round(number * factor) / factor; +const stringifyNumber = (number, precision, leadingZero) => { + const rounded = toFixed(number, precision); return leadingZero ? removeLeadingZero(rounded) : rounded.toString(); }; From 5c7427006abe7f3d810509c3c47583668dbb581a Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 2 Mar 2024 06:48:53 -0800 Subject: [PATCH 11/15] fix tests --- ...lyTransformsShapes.01.svg => applyTransformsShapes.01.svg.txt} | 0 ...lyTransformsShapes.02.svg => applyTransformsShapes.02.svg.txt} | 0 ...lyTransformsShapes.03.svg => applyTransformsShapes.03.svg.txt} | 0 ...lyTransformsShapes.04.svg => applyTransformsShapes.04.svg.txt} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename test/plugins/{applyTransformsShapes.01.svg => applyTransformsShapes.01.svg.txt} (100%) rename test/plugins/{applyTransformsShapes.02.svg => applyTransformsShapes.02.svg.txt} (100%) rename test/plugins/{applyTransformsShapes.03.svg => applyTransformsShapes.03.svg.txt} (100%) rename test/plugins/{applyTransformsShapes.04.svg => applyTransformsShapes.04.svg.txt} (100%) diff --git a/test/plugins/applyTransformsShapes.01.svg b/test/plugins/applyTransformsShapes.01.svg.txt similarity index 100% rename from test/plugins/applyTransformsShapes.01.svg rename to test/plugins/applyTransformsShapes.01.svg.txt diff --git a/test/plugins/applyTransformsShapes.02.svg b/test/plugins/applyTransformsShapes.02.svg.txt similarity index 100% rename from test/plugins/applyTransformsShapes.02.svg rename to test/plugins/applyTransformsShapes.02.svg.txt diff --git a/test/plugins/applyTransformsShapes.03.svg b/test/plugins/applyTransformsShapes.03.svg.txt similarity index 100% rename from test/plugins/applyTransformsShapes.03.svg rename to test/plugins/applyTransformsShapes.03.svg.txt diff --git a/test/plugins/applyTransformsShapes.04.svg b/test/plugins/applyTransformsShapes.04.svg.txt similarity index 100% rename from test/plugins/applyTransformsShapes.04.svg rename to test/plugins/applyTransformsShapes.04.svg.txt From db5004ea377ea4916fdccf15db4c48632f5eb049 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 2 Mar 2024 06:49:39 -0800 Subject: [PATCH 12/15] canChangePath --- lib/svgo/tools.js | 21 +++++++++++++++++++++ plugins/applyTransformsShapes.js | 9 ++++++++- plugins/mergePaths.js | 29 ++--------------------------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index f56e8d939..6bd9b69e6 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -192,6 +192,27 @@ export const includesUrlReference = (body) => { return new RegExp(regReferencesUrl).test(body); }; +/** + * Checks if changing the path would cause the element to look different. + * @param {import('../types.js').ComputedStyles} computedStyle + * @returns {boolean} If it's safe to change the path. + */ +export const canChangePath = (computedStyle) => { + if (computedStyle['marker-start']) return false; + if (computedStyle['marker-mid']) return false; + if (computedStyle['marker-end']) return false; + if (computedStyle['clip-path']) return false; + if (computedStyle['mask']) return false; + if (computedStyle['mask-image']) return false; + for (const name of ['fill', 'filter', 'stroke']) { + const value = computedStyle[name]; + if (value?.type == 'static' && includesUrlReference(value.value)) + return false; + } + + return true; +}; + /** * @param {string} attribute * @param {string} value diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index cf6f2f7a6..eb1b838d2 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -1,5 +1,9 @@ import { collectStylesheet, computeStyle } from '../lib/style.js'; -import { toFixed, removeLeadingZero } from '../lib/svgo/tools.js'; +import { + toFixed, + removeLeadingZero, + canChangePath, +} from '../lib/svgo/tools.js'; import { attrsGroupsDefaults } from './_collections.js'; import { transform2js, transformsMultiply } from './_transforms.js'; @@ -33,6 +37,9 @@ export const fn = (root, params) => { ) { return; } + if (!canChangePath(computedStyle)) { + return; + } const transformStyle = computedStyle.transform; if ( diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index bcfc294f9..1453723a2 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -9,26 +9,11 @@ import { collectStylesheet, computeStyle } from '../lib/style.js'; import { path2js, js2path, intersects } from './_path.js'; -import { includesUrlReference } from '../lib/svgo/tools.js'; +import { canChangePath } from '../lib/svgo/tools.js'; export const name = 'mergePaths'; export const description = 'merges multiple paths in one if possible'; -/** - * @param {ComputedStyles} computedStyle - * @param {string} attName - * @returns {boolean} - */ -function elementHasUrl(computedStyle, attName) { - const style = computedStyle[attName]; - - if (style?.type === 'static') { - return includesUrlReference(style.value); - } - - return false; -} - /** * Merge multiple Paths into one. * @@ -98,17 +83,7 @@ export const fn = (root, params) => { } const computedStyle = computeStyle(stylesheet, child); - if ( - computedStyle['marker-start'] || - computedStyle['marker-mid'] || - computedStyle['marker-end'] || - computedStyle['clip-path'] || - computedStyle['mask'] || - computedStyle['mask-image'] || - ['fill', 'filter', 'stroke'].some((attName) => - elementHasUrl(computedStyle, attName), - ) - ) { + if (!canChangePath(computedStyle)) { if (prevPathData) { updatePreviousPath(prevChild, prevPathData); } From 56457680c134f4ed0e44dd922ce38be54cb690bc Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 2 Mar 2024 06:52:26 -0800 Subject: [PATCH 13/15] semantics --- lib/svgo/tools.js | 7 ++++--- plugins/applyTransformsShapes.js | 4 ++-- plugins/mergePaths.js | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index 6bd9b69e6..514c667ac 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -193,11 +193,12 @@ export const includesUrlReference = (body) => { }; /** - * Checks if changing the path would cause the element to look different. + * Checks if changing the position or size of an element would + * cause the element to look different. * @param {import('../types.js').ComputedStyles} computedStyle - * @returns {boolean} If it's safe to change the path. + * @returns {boolean} If it's safe to change the position. */ -export const canChangePath = (computedStyle) => { +export const canChangePosition = (computedStyle) => { if (computedStyle['marker-start']) return false; if (computedStyle['marker-mid']) return false; if (computedStyle['marker-end']) return false; diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index eb1b838d2..473fa4ef4 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -2,7 +2,7 @@ import { collectStylesheet, computeStyle } from '../lib/style.js'; import { toFixed, removeLeadingZero, - canChangePath, + canChangePosition, } from '../lib/svgo/tools.js'; import { attrsGroupsDefaults } from './_collections.js'; import { transform2js, transformsMultiply } from './_transforms.js'; @@ -37,7 +37,7 @@ export const fn = (root, params) => { ) { return; } - if (!canChangePath(computedStyle)) { + if (!canChangePosition(computedStyle)) { return; } diff --git a/plugins/mergePaths.js b/plugins/mergePaths.js index 1453723a2..bb4172ae6 100644 --- a/plugins/mergePaths.js +++ b/plugins/mergePaths.js @@ -9,7 +9,7 @@ import { collectStylesheet, computeStyle } from '../lib/style.js'; import { path2js, js2path, intersects } from './_path.js'; -import { canChangePath } from '../lib/svgo/tools.js'; +import { canChangePosition } from '../lib/svgo/tools.js'; export const name = 'mergePaths'; export const description = 'merges multiple paths in one if possible'; @@ -83,7 +83,7 @@ export const fn = (root, params) => { } const computedStyle = computeStyle(stylesheet, child); - if (!canChangePath(computedStyle)) { + if (!canChangePosition(computedStyle)) { if (prevPathData) { updatePreviousPath(prevChild, prevPathData); } From 09730849619c35b963c78194c5c87c6da6d2d4a6 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 2 Mar 2024 06:55:23 -0800 Subject: [PATCH 14/15] slightly improve perf --- plugins/applyTransformsShapes.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index 473fa4ef4..1190ae3cd 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -25,7 +25,10 @@ export const fn = (root, params) => { return { element: { enter: (node) => { - if (!APPLICABLE_SHAPES.includes(node.name)) { + if ( + !APPLICABLE_SHAPES.includes(node.name) || + !node.attributes.transform + ) { return; } @@ -43,7 +46,7 @@ export const fn = (root, params) => { const transformStyle = computedStyle.transform; if ( - transformStyle?.type !== 'static' || + transformStyle.type !== 'static' || transformStyle.value !== node.attributes.transform ) { return; From d3aabf2872a7bd5cc2726a21054b5bc6934d76f6 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Sat, 2 Mar 2024 07:40:12 -0800 Subject: [PATCH 15/15] reorganize stroke handling & watch out for `style` --- plugins/applyTransformsShapes.js | 33 ++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/plugins/applyTransformsShapes.js b/plugins/applyTransformsShapes.js index 1190ae3cd..d73f89ffc 100644 --- a/plugins/applyTransformsShapes.js +++ b/plugins/applyTransformsShapes.js @@ -4,7 +4,6 @@ import { removeLeadingZero, canChangePosition, } from '../lib/svgo/tools.js'; -import { attrsGroupsDefaults } from './_collections.js'; import { transform2js, transformsMultiply } from './_transforms.js'; export const name = 'applyTransformsShapes'; @@ -34,12 +33,6 @@ export const fn = (root, params) => { const computedStyle = computeStyle(stylesheet, node); if (computedStyle.filter) return; - if ( - computedStyle.stroke?.type === 'dynamic' || - computedStyle['stroke-width']?.type === 'dynamic' - ) { - return; - } if (!canChangePosition(computedStyle)) { return; } @@ -51,16 +44,28 @@ export const fn = (root, params) => { ) { return; } - const matrix = transformsMultiply( transform2js(node.attributes.transform), ); - const hasStroke = - computedStyle.stroke && computedStyle.stroke.value !== 'none'; - const strokeWidth = Number( - computedStyle['stroke-width']?.value || - (hasStroke && attrsGroupsDefaults.presentation['stroke-width']), - ); + + if ( + computedStyle.stroke?.type === 'dynamic' || + computedStyle['stroke-width']?.type === 'dynamic' + ) { + return; + } + let strokeWidth = 0; + if (computedStyle['stroke-width']) { + if (!node.attributes['stroke-width']) { + return; + } + strokeWidth = +computedStyle['stroke-width'].value; + } else if ( + computedStyle.stroke && + computedStyle.stroke.value !== 'none' + ) { + strokeWidth = 1; + } const isSimilar = (matrix.data[0] === matrix.data[3] &&