From f9ac896e00f6595c15b3009ed99c8c754352b3a3 Mon Sep 17 00:00:00 2001 From: Aviral Shukla Date: Mon, 22 Sep 2025 00:16:23 +0530 Subject: [PATCH] Handle transparent colors in removeUselessStrokeAndFill - Added isTransparent utility to detect transparent and zero-alpha color values for fill and stroke attributes. - Updated removeUselessStrokeAndFill plugin to treat these values as 'none', improving SVG cleanup. - New tests to verify correct removal of elements with transparent or zero fill/stroke. --- lib/svgo/tools.js | 37 +++++++++++++ plugins/removeUselessStrokeAndFill.js | 34 ++++++++---- .../removeUselessStrokeAndFill.06.svg.txt | 54 +++++++++++++++++++ .../removeUselessStrokeAndFill.07.svg.txt | 44 +++++++++++++++ 4 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 test/plugins/removeUselessStrokeAndFill.06.svg.txt create mode 100644 test/plugins/removeUselessStrokeAndFill.07.svg.txt diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index 439157a4f..b66a1bb07 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -1,4 +1,5 @@ import { attrsGroups, referencesProps } from '../../plugins/_collections.js'; +import * as csstree from 'css-tree'; /** * @typedef CleanupOutDataParams @@ -239,3 +240,39 @@ export const toFixed = (num, precision) => { const pow = 10 ** precision; return Math.round(num * pow) / pow; }; + +/** + * takes in a attribute value and returns true if the color is transparent OR alpha is 0 + * these values are to fill and stroke attributes + * examples: + * - 'transparent' + * - 'rgba(10, 10, 0, 0)' + * - 'hsla(120, 100%, 50%, 0)' + */ +export const isTransparent = (/** @type {string} */ attribute) => { + if (typeof attribute !== 'string') { + return false; + } + try { + const ast = csstree.parse(attribute, { context: 'value' }); + if (ast.type === 'Value') { + const firstNode = ast.children.first; + if (firstNode && firstNode.type === 'Function' && (firstNode.name === 'rgba' || firstNode.name === 'hsla')) { + const args = firstNode.children.toArray().filter(node => + node.type === 'Number' || node.type === 'Percentage' + ); + const alphaNode = args[3]; + if ((alphaNode && alphaNode.type === 'Number' || alphaNode && alphaNode.type === 'Percentage') && parseFloat(alphaNode.value) === 0) { + return true; + } + } + if (firstNode && firstNode.type === 'Identifier' && (firstNode.name === 'transparent')) { + return true; + } + } + } catch { + return false; + } + return false; +} + diff --git a/plugins/removeUselessStrokeAndFill.js b/plugins/removeUselessStrokeAndFill.js index 5b055890a..b162974e4 100644 --- a/plugins/removeUselessStrokeAndFill.js +++ b/plugins/removeUselessStrokeAndFill.js @@ -3,6 +3,7 @@ import { visit, visitSkip } from '../lib/util/visit.js'; import { collectStylesheet, computeStyle } from '../lib/style.js'; import { hasScripts } from '../lib/svgo/tools.js'; import { elemsGroups } from './_collections.js'; +import { isTransparent } from '../lib/svgo/tools.js'; /** * @typedef RemoveUselessStrokeAndFillParams @@ -59,6 +60,8 @@ export const fn = (root, params) => { const stroke = computedStyle.stroke; const strokeOpacity = computedStyle['stroke-opacity']; const strokeWidth = computedStyle['stroke-width']; + const markerStart = computedStyle['marker-start']; + const markerMid = computedStyle['marker-mid']; const markerEnd = computedStyle['marker-end']; const fill = computedStyle.fill; const fillOpacity = computedStyle['fill-opacity']; @@ -79,7 +82,9 @@ export const fn = (root, params) => { strokeOpacity.value === '0') || (strokeWidth != null && strokeWidth.type === 'static' && - strokeWidth.value === '0') + strokeWidth.value === '0') || + (stroke != null && stroke.type === 'static' && + isTransparent(stroke.value)) ) { // stroke-width may affect the size of marker-end // marker is not visible when stroke-width is 0 @@ -87,7 +92,7 @@ export const fn = (root, params) => { (strokeWidth != null && strokeWidth.type === 'static' && strokeWidth.value === '0') || - markerEnd == null + (markerEnd == null && markerStart == null && markerMid == null) ) { for (const name of Object.keys(node.attributes)) { if (name.startsWith('stroke')) { @@ -112,7 +117,9 @@ export const fn = (root, params) => { (fill != null && fill.type === 'static' && fill.value === 'none') || (fillOpacity != null && fillOpacity.type === 'static' && - fillOpacity.value === '0') + fillOpacity.value === '0') || + (fill != null && fill.type === 'static' && + isTransparent(fill.value)) ) { for (const name of Object.keys(node.attributes)) { if (name.startsWith('fill-')) { @@ -129,13 +136,20 @@ export const fn = (root, params) => { } if (removeNone) { - if ( - (stroke == null || node.attributes.stroke === 'none') && - ((fill != null && - fill.type === 'static' && - fill.value === 'none') || - node.attributes.fill === 'none') - ) { + const isStrokeNone = stroke == null || + (stroke.type === 'static' && stroke.value === 'none') || + (strokeOpacity != null && strokeOpacity.type === 'static' && strokeOpacity.value === '0') || + (strokeWidth != null && strokeWidth.type === 'static' && strokeWidth.value === '0') || + (stroke.type === 'static' && isTransparent(stroke.value)); + + const isFillNone = fill == null || + (fill.type === 'static' && fill.value === 'none') || + (fillOpacity != null && fillOpacity.type === 'static' && fillOpacity.value === '0') || + (fill.type === 'static' && isTransparent(fill.value)); + + const hasMarkers = markerStart || markerMid || markerEnd; + + if (isStrokeNone && isFillNone && !hasMarkers) { detachNodeFromParent(node, parentNode); } } diff --git a/test/plugins/removeUselessStrokeAndFill.06.svg.txt b/test/plugins/removeUselessStrokeAndFill.06.svg.txt new file mode 100644 index 000000000..0ab1c78c5 --- /dev/null +++ b/test/plugins/removeUselessStrokeAndFill.06.svg.txt @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plugins/removeUselessStrokeAndFill.07.svg.txt b/test/plugins/removeUselessStrokeAndFill.07.svg.txt new file mode 100644 index 000000000..cf84653f6 --- /dev/null +++ b/test/plugins/removeUselessStrokeAndFill.07.svg.txt @@ -0,0 +1,44 @@ +should remove elements if they are transparent or have zero fill and stroke values + +=== + + + + + + + + + + + + + + + + + + + + + + + +@@@ + + + + + + + + + + + + + + +@@@ + +{ "removeNone": true } \ No newline at end of file