1
0
mirror of https://github.com/svg/svgo.git synced 2025-07-31 07:44:22 +03:00

Implement style computing (#1399)

Ref https://github.com/svg/svgo/issues/777

Currently a lot of optimisations are attributes specific and may be
broken because of inline or shared styles.

In this diff I'm trying to solve the problem with getComputedStyle
analog.

`computeStyle` collects attributes, shared css rules, inline styles
and inherited styles and checks whether they can be statically optimised
or left as deoptimisation.
This commit is contained in:
Bogdan Chadkin
2021-03-04 13:13:44 +03:00
committed by GitHub
parent f3a6cf259a
commit be28d65d78
6 changed files with 665 additions and 198 deletions

184
lib/style.js Normal file
View File

@ -0,0 +1,184 @@
'use strict';
const stable = require('stable');
const csstree = require('css-tree');
const specificity = require('csso/lib/restructure/prepare/specificity');
const { selectAll, is } = require('css-select');
const svgoCssSelectAdapter = require('./svgo/css-select-adapter.js');
const { compareSpecificity } = require('./css-tools.js');
const {
attrsGroups,
inheritableAttrs,
presentationNonInheritableGroupAttrs,
} = require('../plugins/_collections.js');
const cssSelectOptions = {
xmlMode: true,
adapter: svgoCssSelectAdapter,
};
const parseRule = (ruleNode, dynamic) => {
let selectors;
let selectorsSpecificity;
const declarations = [];
csstree.walk(ruleNode, (cssNode) => {
if (cssNode.type === 'SelectorList') {
// compute specificity from original node to consider pseudo classes
selectorsSpecificity = specificity(cssNode);
const newSelectorsNode = csstree.clone(cssNode);
csstree.walk(newSelectorsNode, (pseudoClassNode, item, list) => {
if (pseudoClassNode.type === 'PseudoClassSelector') {
dynamic = true;
list.remove(item);
}
});
selectors = csstree.generate(newSelectorsNode);
return csstree.walk.skip;
}
if (cssNode.type === 'Declaration') {
declarations.push({
name: cssNode.property,
value: csstree.generate(cssNode.value),
important: cssNode.important,
});
return csstree.walk.skip;
}
});
return {
dynamic,
selectors,
specificity: selectorsSpecificity,
declarations,
};
};
const parseStylesheet = (css, dynamic) => {
const rules = [];
const ast = csstree.parse(css);
csstree.walk(ast, (cssNode) => {
if (cssNode.type === 'Rule') {
rules.push(parseRule(cssNode, dynamic || false));
return csstree.walk.skip;
}
if (cssNode.type === 'Atrule') {
csstree.walk(cssNode, (ruleNode) => {
if (ruleNode.type === 'Rule') {
rules.push(parseRule(ruleNode, dynamic || true));
return csstree.walk.skip;
}
});
return csstree.walk.skip;
}
});
return rules;
};
const computeOwnStyle = (node, stylesheet) => {
const computedStyle = {};
const importantStyles = new Map();
// collect attributes
if (node.attrs) {
for (const { name, value } of Object.values(node.attrs)) {
if (attrsGroups.presentation.includes(name)) {
computedStyle[name] = { type: 'static', inherited: false, value };
importantStyles.set(name, false);
}
}
}
// collect matching rules
for (const { selectors, declarations, dynamic } of stylesheet) {
if (is(node, selectors, cssSelectOptions)) {
for (const { name, value, important } of declarations) {
const computed = computedStyle[name];
if (computed && computed.type === 'dynamic') {
continue;
}
if (dynamic) {
computedStyle[name] = { type: 'dynamic', inherited: false };
continue;
}
if (
computed == null ||
important === true ||
importantStyles.get(name) === false
) {
computedStyle[name] = { type: 'static', inherited: false, value };
importantStyles.set(name, important);
}
}
}
}
// collect inline styles
for (const [name, { value, priority }] of node.style.properties) {
const computed = computedStyle[name];
const important = priority === 'important';
if (computed && computed.type === 'dynamic') {
continue;
}
if (
computed == null ||
important === true ||
importantStyles.get(name) === false
) {
computedStyle[name] = { type: 'static', inherited: false, value };
importantStyles.set(name, important);
}
}
return computedStyle;
};
const computeStyle = (node) => {
// find root
let root = node;
while (root.parentNode) {
root = root.parentNode;
}
// find all styles
const styleNodes = selectAll('style', root, cssSelectOptions);
// parse all styles
const stylesheet = [];
for (const styleNode of styleNodes) {
const dynamic =
styleNode.hasAttr('media') && styleNode.attr('media').value !== 'all';
if (
styleNode.hasAttr('type') === false ||
styleNode.attr('type').value === '' ||
styleNode.attr('type').value === 'text/css'
) {
const children = styleNode.content || [];
for (const child of children) {
const css = child.text || child.cdata;
stylesheet.push(...parseStylesheet(css, dynamic));
}
}
}
// sort by selectors specificity
stable.inplace(stylesheet, (a, b) =>
compareSpecificity(a.specificity, b.specificity)
);
// collect inherited styles
const computedStyles = computeOwnStyle(node, stylesheet);
let parent = node;
while (parent.parentNode && parent.parentNode.elem !== '#document') {
const inheritedStyles = computeOwnStyle(parent.parentNode, stylesheet);
for (const [name, computed] of Object.entries(inheritedStyles)) {
if (
computedStyles[name] == null &&
// ignore not inheritable styles
inheritableAttrs.includes(name) === true &&
presentationNonInheritableGroupAttrs.includes(name) === false
) {
computedStyles[name] = { ...computed, inherited: true };
}
}
parent = parent.parentNode;
}
return computedStyles;
};
exports.computeStyle = computeStyle;

215
lib/style.test.js Normal file
View File

@ -0,0 +1,215 @@
'use strict';
const { expect } = require('chai');
const { computeStyle } = require('./style.js');
const svg2js = require('./svgo/svg2js.js');
describe('computeStyle', () => {
it('collects styles', () => {
const root = svg2js(`
<svg>
<rect id="class" class="a" />
<rect id="two-classes" class="b a" />
<rect id="attribute" fill="purple" />
<rect id="inline-style" style="fill: grey;" />
<g fill="yellow">
<rect id="inheritance" />
<g style="fill: blue;">
<g>
<rect id="nested-inheritance" />
</g>
</g>
</g>
<style>
.a { fill: red; }
</style>
<style>
<![CDATA[
.b { fill: green; stroke: black; }
]]>
</style>
</svg>
`);
expect(computeStyle(root.querySelector('#class'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'red' },
});
expect(computeStyle(root.querySelector('#two-classes'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'green' },
stroke: { type: 'static', inherited: false, value: 'black' },
});
expect(computeStyle(root.querySelector('#attribute'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'purple' },
});
expect(computeStyle(root.querySelector('#inline-style'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'grey' },
});
expect(computeStyle(root.querySelector('#inheritance'))).to.deep.equal({
fill: { type: 'static', inherited: true, value: 'yellow' },
});
expect(
computeStyle(root.querySelector('#nested-inheritance'))
).to.deep.equal({
fill: { type: 'static', inherited: true, value: 'blue' },
});
});
it('prioritizes different kinds of styles', () => {
const root = svg2js(`
<svg>
<style>
g > .a { fill: red; }
.a { fill: green; }
.b { fill: blue; }
</style>
<g fill="yellow">
<rect id="complex-selector" class="a" />
<rect id="attribute-over-inheritance" fill="orange" />
<rect id="style-rule-over-attribute" class="b" fill="grey" />
<rect id="inline-style-over-style-rule" style="fill: purple;" class="b" />
</g>
</svg>
`);
expect(computeStyle(root.querySelector('#complex-selector'))).to.deep.equal(
{
fill: { type: 'static', inherited: false, value: 'red' },
}
);
expect(
computeStyle(root.querySelector('#attribute-over-inheritance'))
).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'orange' },
});
expect(
computeStyle(root.querySelector('#style-rule-over-attribute'))
).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'blue' },
});
expect(
computeStyle(root.querySelector('#inline-style-over-style-rule'))
).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'purple' },
});
});
it('prioritizes important styles', () => {
const root = svg2js(`
<svg>
<style>
g > .a { fill: red; }
.b { fill: green !important; }
</style>
<rect id="complex-selector" class="a b" />
<rect id="style-rule-over-inline-style" style="fill: orange;" class="b" />
<rect id="inline-style-over-style-rule" style="fill: purple !important;" class="b" />
</svg>
`);
expect(computeStyle(root.querySelector('#complex-selector'))).to.deep.equal(
{
fill: { type: 'static', inherited: false, value: 'green' },
}
);
expect(
computeStyle(root.querySelector('#style-rule-over-inline-style'))
).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'green' },
});
expect(
computeStyle(root.querySelector('#inline-style-over-style-rule'))
).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'purple' },
});
});
it('treats at-rules and pseudo-classes as dynamic styles', () => {
const root = svg2js(`
<svg>
<style>
@media screen {
.a { fill: red; }
}
.b:hover { fill: green; }
.c { fill: blue; }
.d { fill: purple; }
</style>
<rect id="media-query" class="a d" style="fill: orange;" />
<rect id="hover" class="b" style="fill: yellow;" />
<g class="a">
<rect id="inherited" />
<rect id="inherited-overriden" class="c" />
</g>
<rect id="static" class="c" style="fill: black" />
</svg>
`);
expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({
fill: { type: 'dynamic', inherited: false },
});
expect(computeStyle(root.querySelector('#hover'))).to.deep.equal({
fill: { type: 'dynamic', inherited: false },
});
expect(computeStyle(root.querySelector('#inherited'))).to.deep.equal({
fill: { type: 'dynamic', inherited: true },
});
expect(
computeStyle(root.querySelector('#inherited-overriden'))
).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'blue' },
});
expect(computeStyle(root.querySelector('#static'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'black' },
});
});
it('considers <style> media attribute', () => {
const root = svg2js(`
<svg>
<style media="print">
@media screen {
.a { fill: red; }
}
.b { fill: green; }
</style>
<style media="all">
.c { fill: blue; }
</style>
<rect id="media-query" class="a" />
<rect id="kinda-static" class="b" />
<rect id="static" class="c" />
</svg>
`);
expect(computeStyle(root.querySelector('#media-query'))).to.deep.equal({
fill: { type: 'dynamic', inherited: false },
});
expect(computeStyle(root.querySelector('#kinda-static'))).to.deep.equal({
fill: { type: 'dynamic', inherited: false },
});
expect(computeStyle(root.querySelector('#static'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'blue' },
});
});
it('ignores <style> with invalid type', () => {
const root = svg2js(`
<svg>
<style type="text/css">
.a { fill: red; }
</style>
<style type="">
.b { fill: green; }
</style>
<style type="text/invalid">
.c { fill: blue; }
</style>
<rect id="valid-type" class="a" />
<rect id="empty-type" class="b" />
<rect id="invalid-type" class="c" />
</svg>
`);
expect(computeStyle(root.querySelector('#valid-type'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'red' },
});
expect(computeStyle(root.querySelector('#empty-type'))).to.deep.equal({
fill: { type: 'static', inherited: false, value: 'green' },
});
expect(computeStyle(root.querySelector('#invalid-type'))).to.deep.equal({});
});
});

View File

@ -1,27 +1,30 @@
'use strict';
const { computeStyle } = require('../lib/style.js');
exports.type = 'perItem';
exports.active = true;
exports.description = 'removes hidden elements (zero sized, with absent attributes)';
exports.description =
'removes hidden elements (zero sized, with absent attributes)';
exports.params = {
isHidden: true,
displayNone: true,
opacity0: true,
circleR0: true,
ellipseRX0: true,
ellipseRY0: true,
rectWidth0: true,
rectHeight0: true,
patternWidth0: true,
patternHeight0: true,
imageWidth0: true,
imageHeight0: true,
pathEmptyD: true,
polylineEmptyPoints: true,
polygonEmptyPoints: true
isHidden: true,
displayNone: true,
opacity0: true,
circleR0: true,
ellipseRX0: true,
ellipseRY0: true,
rectWidth0: true,
rectHeight0: true,
patternWidth0: true,
patternHeight0: true,
imageWidth0: true,
imageHeight0: true,
pathEmptyD: true,
polylineEmptyPoints: true,
polygonEmptyPoints: true,
};
var regValidPath = /M\s*(?:[-+]?(?:\d*\.\d+|\d+(?:\.|(?!\.)))([eE][-+]?\d+)?(?!\d)\s*,?\s*){2}\D*\d/i;
@ -46,184 +49,218 @@ var regValidPath = /M\s*(?:[-+]?(?:\d*\.\d+|\d+(?:\.|(?!\.)))([eE][-+]?\d+)?(?!\
* @author Kir Belevich
*/
exports.fn = function (item, params) {
if (item.elem) {
// Removes hidden elements
// https://www.w3schools.com/cssref/pr_class_visibility.asp
if (
params.isHidden &&
item.hasAttr('visibility', 'hidden') &&
// keep if any descendant enables visibility
item.querySelector('[visibility=visible]') == null
) return false;
// display="none"
//
// https://www.w3.org/TR/SVG11/painting.html#DisplayProperty
// "A value of display: none indicates that the given element
// and its children shall not be rendered directly"
if (
params.displayNone &&
item.hasAttr('display', 'none')
) return false;
// opacity="0"
//
// https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
if (
params.opacity0 &&
item.hasAttr('opacity', '0') &&
// transparent element inside clipPath still affect clipped elements
item.closestElem('clipPath') == null
) return false;
// Circles with zero radius
//
// https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute
// "A value of zero disables rendering of the element"
//
// <circle r="0">
if (
params.circleR0 &&
item.isElem('circle') &&
item.isEmpty() &&
item.hasAttr('r', '0')
) return false;
// Ellipse with zero x-axis radius
//
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute
// "A value of zero disables rendering of the element"
//
// <ellipse rx="0">
if (
params.ellipseRX0 &&
item.isElem('ellipse') &&
item.isEmpty() &&
item.hasAttr('rx', '0')
) return false;
// Ellipse with zero y-axis radius
//
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute
// "A value of zero disables rendering of the element"
//
// <ellipse ry="0">
if (
params.ellipseRY0 &&
item.isElem('ellipse') &&
item.isEmpty() &&
item.hasAttr('ry', '0')
) return false;
// Rectangle with zero width
//
// https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute
// "A value of zero disables rendering of the element"
//
// <rect width="0">
if (
params.rectWidth0 &&
item.isElem('rect') &&
item.isEmpty() &&
item.hasAttr('width', '0')
) return false;
// Rectangle with zero height
//
// https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute
// "A value of zero disables rendering of the element"
//
// <rect height="0">
if (
params.rectHeight0 &&
params.rectWidth0 &&
item.isElem('rect') &&
item.isEmpty() &&
item.hasAttr('height', '0')
) return false;
// Pattern with zero width
//
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
//
// <pattern width="0">
if (
params.patternWidth0 &&
item.isElem('pattern') &&
item.hasAttr('width', '0')
) return false;
// Pattern with zero height
//
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
//
// <pattern height="0">
if (
params.patternHeight0 &&
item.isElem('pattern') &&
item.hasAttr('height', '0')
) return false;
// Image with zero width
//
// https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute
// "A value of zero disables rendering of the element"
//
// <image width="0">
if (
params.imageWidth0 &&
item.isElem('image') &&
item.hasAttr('width', '0')
) return false;
// Image with zero height
//
// https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute
// "A value of zero disables rendering of the element"
//
// <image height="0">
if (
params.imageHeight0 &&
item.isElem('image') &&
item.hasAttr('height', '0')
) return false;
// Path with empty data
//
// https://www.w3.org/TR/SVG11/paths.html#DAttribute
//
// <path d=""/>
if (
params.pathEmptyD &&
item.isElem('path') &&
(!item.hasAttr('d') || !regValidPath.test(item.attr('d').value))
) return false;
// Polyline with empty points
//
// https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute
//
// <polyline points="">
if (
params.polylineEmptyPoints &&
item.isElem('polyline') &&
!item.hasAttr('points')
) return false;
// Polygon with empty points
//
// https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
//
// <polygon points="">
if (
params.polygonEmptyPoints &&
item.isElem('polygon') &&
!item.hasAttr('points')
) return false;
if (item.elem) {
// Removes hidden elements
// https://www.w3schools.com/cssref/pr_class_visibility.asp
const computedStyle = computeStyle(item);
if (
params.isHidden &&
computedStyle.visibility &&
computedStyle.visibility.type === 'static' &&
computedStyle.visibility.value === 'hidden' &&
// keep if any descendant enables visibility
item.querySelector('[visibility=visible]') == null
) {
return false;
}
// display="none"
//
// https://www.w3.org/TR/SVG11/painting.html#DisplayProperty
// "A value of display: none indicates that the given element
// and its children shall not be rendered directly"
if (
params.displayNone &&
computedStyle.display &&
computedStyle.display.type === 'static' &&
computedStyle.display.value === 'none'
) {
return false;
}
// opacity="0"
//
// https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
if (
params.opacity0 &&
computedStyle.opacity &&
computedStyle.opacity.type === 'static' &&
computedStyle.opacity.value === '0' &&
// transparent element inside clipPath still affect clipped elements
item.closestElem('clipPath') == null
) {
return false;
}
// Circles with zero radius
//
// https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute
// "A value of zero disables rendering of the element"
//
// <circle r="0">
if (
params.circleR0 &&
item.isElem('circle') &&
item.isEmpty() &&
item.hasAttr('r', '0')
) {
return false;
}
// Ellipse with zero x-axis radius
//
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute
// "A value of zero disables rendering of the element"
//
// <ellipse rx="0">
if (
params.ellipseRX0 &&
item.isElem('ellipse') &&
item.isEmpty() &&
item.hasAttr('rx', '0')
) {
return false;
}
// Ellipse with zero y-axis radius
//
// https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute
// "A value of zero disables rendering of the element"
//
// <ellipse ry="0">
if (
params.ellipseRY0 &&
item.isElem('ellipse') &&
item.isEmpty() &&
item.hasAttr('ry', '0')
) {
return false;
}
// Rectangle with zero width
//
// https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute
// "A value of zero disables rendering of the element"
//
// <rect width="0">
if (
params.rectWidth0 &&
item.isElem('rect') &&
item.isEmpty() &&
item.hasAttr('width', '0')
) {
return false;
}
// Rectangle with zero height
//
// https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute
// "A value of zero disables rendering of the element"
//
// <rect height="0">
if (
params.rectHeight0 &&
params.rectWidth0 &&
item.isElem('rect') &&
item.isEmpty() &&
item.hasAttr('height', '0')
) {
return false;
}
// Pattern with zero width
//
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
//
// <pattern width="0">
if (
params.patternWidth0 &&
item.isElem('pattern') &&
item.hasAttr('width', '0')
) {
return false;
}
// Pattern with zero height
//
// https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute
// "A value of zero disables rendering of the element (i.e., no paint is applied)"
//
// <pattern height="0">
if (
params.patternHeight0 &&
item.isElem('pattern') &&
item.hasAttr('height', '0')
) {
return false;
}
// Image with zero width
//
// https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute
// "A value of zero disables rendering of the element"
//
// <image width="0">
if (
params.imageWidth0 &&
item.isElem('image') &&
item.hasAttr('width', '0')
) {
return false;
}
// Image with zero height
//
// https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute
// "A value of zero disables rendering of the element"
//
// <image height="0">
if (
params.imageHeight0 &&
item.isElem('image') &&
item.hasAttr('height', '0')
) {
return false;
}
// Path with empty data
//
// https://www.w3.org/TR/SVG11/paths.html#DAttribute
//
// <path d=""/>
if (
params.pathEmptyD &&
item.isElem('path') &&
(!item.hasAttr('d') || !regValidPath.test(item.attr('d').value))
) {
return false;
}
// Polyline with empty points
//
// https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute
//
// <polyline points="">
if (
params.polylineEmptyPoints &&
item.isElem('polyline') &&
!item.hasAttr('points')
) {
return false;
}
// Polygon with empty points
//
// https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
//
// <polygon points="">
if (
params.polygonEmptyPoints &&
item.isElem('polygon') &&
!item.hasAttr('points')
) {
return false;
}
}
};

View File

@ -1,11 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg">
<style>
.a { display: block; }
</style>
<g>
<path display="none" d="..."/>
<rect display="none" x="0" y="0" width="20" height="20" />
<rect display="none" class="a" x="0" y="0" width="20" height="20" />
</g>
</svg>
@@@
<svg xmlns="http://www.w3.org/2000/svg">
<g/>
<style>
.a { display: block; }
</style>
<g>
<rect display="none" class="a" x="0" y="0" width="20" height="20"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 167 B

After

Width:  |  Height:  |  Size: 466 B

View File

@ -1,11 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg">
<style>
.a { opacity: 0.5; }
</style>
<g>
<path opacity="0" d="..."/>
<rect opacity="0" x="0" y="0" width="20" height="20" />
<rect opacity="0" class="a" x="0" y="0" width="20" height="20" />
</g>
</svg>
@@@
<svg xmlns="http://www.w3.org/2000/svg">
<g/>
<style>
.a { opacity: 0.5; }
</style>
<g>
<rect opacity="0" class="a" x="0" y="0" width="20" height="20"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 164 B

After

Width:  |  Height:  |  Size: 453 B

View File

@ -1,4 +1,12 @@
Keep invisible elements which have visibile ones inside
and resolve styles
===
<svg width="480" height="360" xmlns="http://www.w3.org/2000/svg">
<style>
.a { visibility: visible; }
</style>
<rect x="96" y="96" width="96" height="96" fill="lime" />
<g visibility="hidden">
<rect x="96" y="96" width="96" height="96" fill="red" />
@ -7,14 +15,19 @@
<g visibility="hidden">
<rect x="196" y="196" width="96" height="96" fill="lime" visibility="visible" />
</g>
<rect x="96" y="96" width="96" height="96" visibility="hidden" class="a" />
</svg>
@@@
<svg width="480" height="360" xmlns="http://www.w3.org/2000/svg">
<style>
.a { visibility: visible; }
</style>
<rect x="96" y="96" width="96" height="96" fill="lime"/>
<rect x="196.5" y="196.5" width="95" height="95" fill="red"/>
<g visibility="hidden">
<rect x="196" y="196" width="96" height="96" fill="lime" visibility="visible"/>
</g>
<rect x="96" y="96" width="96" height="96" visibility="hidden" class="a"/>
</svg>

Before

Width:  |  Height:  |  Size: 740 B

After

Width:  |  Height:  |  Size: 1.1 KiB