scratch-svg-renderer/test/transform-applier.js

796 lines
43 KiB
JavaScript

// Test transform-applier
const test = require('tap').test;
const jsdom = require('jsdom');
const {JSDOM} = jsdom;
const transformStrokeWidths = require('../src/transform-applier');
const log = require('../src/util/log');
// PathData, absolute instructions only
const d = 'M -20 -20 0 10 ' +
'L 5 5 H 10 V 10 ' +
'C 10 10 20 10 15 25 ' +
'S 15 30 15 40 ' +
'Q 20 50 30 60 ' +
'T 30 70 20 80 ' +
'M 0 0 ' +
'A 40 50 0 1 1 0 100 Z ';
// Path constructed specifically for testing elliptical arcs
const ellipticalPath = 'M10,300 l 50,-25 ' +
'a25,25 -60 0,1 50,-25 l 50,-25 ' +
'a25,50 -45 0,1 50,-25 l 50,-25 ' +
'a25,75 -30 0,1 50,-25 l 50,-25 ' +
'a25,100 -15 0,1 50,-25 l 50,-25 v 50 l -50,25 ' +
'a25,25 60 1,1 -50,25 l -50,25 ' +
'a25,50 45 1,1 -50,25 l -50,25 ' +
'a25,75 30 1,1 -50,25 l -50,25 ' +
'a25,100 15 1,1 -50,25 l -50,25 ';
// This path is tricky because all of its bounds lie outside
// the 2 given points
const trickyBoundsPath = 'M 40 40 A 30 50 -45 1,1 80 80';
// Because jsdom doesn't simulate SvgElement.getBBox(), we need to store
// the bounds for testing.
const trickyBoundsPathBounds = {
height: 82.46210479736328,
width: 82.46211242675781,
x: 36.26179885864258,
y: 1.2760814428329468
};
const {window} = new JSDOM();
const parser = new window.DOMParser();
const fs = require('fs');
const OUTPUT_COMPARISON_FILES = false;
let comparisonFileString = '';
const comparisonFileAppend = function (svgString, svgElement, name) {
if (!OUTPUT_COMPARISON_FILES) return;
const newSvgString = new window.XMLSerializer().serializeToString(svgElement);
comparisonFileString +=
`<p>${name}</p>
<div style="width: 500px; border-style: solid; border-width: 1px;">
${svgString}
</div>
<div style="width: 500px; border-style: solid; border-width: 0 1px 1px 1px;">
${newSvgString}
</div>`;
};
const outputComparisonFile = function () {
if (!OUTPUT_COMPARISON_FILES) return;
fs.writeFile(
`${__dirname}/test-output/transform-applier-test.html`,
`<!-- THIS IS A GENERATED FILE -->\n<html><body>${comparisonFileString}\n</body></html>`,
err => log.error(err)
);
};
// No transform attribute on the path
test('noTransformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 100 100">` +
`<path id="path" fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'noTransformPath');
t.equals(d, svgElement.getElementById('path').attributes.d.value);
t.false(svgElement.getElementById('path').attributes.transform);
t.end();
});
// No stroke width attribute on the path. Stroke width is 1 by default in SVG, so transform should increase it to 2.
test('transformedNoStrokeWidthPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 100 100">` +
`<path id="path" transform="scale(2)" fill="#0000" stroke="red" d="${d}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'noStrokeWidthPath');
t.equals('2', svgElement.getElementById('path').attributes['stroke-width'].value);
t.end();
});
// Transform is identity matrix
test('identityTransformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 100 100">` +
`<path transform="matrix(1 0 0 1 0 0)" id="path" fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'identityTransformPath');
t.equals(d, svgElement.getElementById('path').attributes.d.value);
t.false(svgElement.getElementById('path').attributes.transform);
t.end();
});
// Transform on a simple box
test('transformBox', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="250px" height="250px" viewBox="0 0 250 250">` +
`<path transform="matrix(20 0 0 10 45 45)" id="path" fill="#0000" stroke="red" stroke-width="1" ` +
`d="M0,0 h 10 v 10 h -10 z"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'transformBox');
const transformed = `M 45 45 L 245 45 L 245 145 L 45 145 Z `;
t.equals(transformed, svgElement.getElementById('path').attributes.d.value);
// Transform is integrated into path, so the attribute should be gone
t.false(svgElement.getElementById('path').attributes.transform);
const quadraticMean = Math.sqrt(((20 * 20) + (10 * 10)) / 2);
t.equals(`${quadraticMean}`, svgElement.getElementById('path').attributes['stroke-width'].value);
t.end();
});
// Transform is not identity matrix
test('transformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="250px" height="250px" viewBox="0 0 250 250">` +
`<path transform="matrix(2 0 0 2 45 45)" id="path" fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'transformPath');
const doubled = 'M 5 5 L 45 65 L 55 55 L 65 55 L 65 65 C 65 65 85 65 75 95 C 65 125 75 105 75 125 ' +
'Q 85 145 105 165 Q 125 185 105 185 Q 85 185 85 205 M 45 45 A 80 100 0 1 1 45 245 Z ';
t.equals(doubled, svgElement.getElementById('path').attributes.d.value);
// Transform is integrated into path, so the attribute should be gone
t.false(svgElement.getElementById('path').attributes.transform);
t.equals('2', svgElement.getElementById('path').attributes['stroke-width'].value);
t.end();
});
// Transform has multiple matrices that compose to identity matrix
test('composedTransformPathIdentity', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 100 100">` +
`<path transform="matrix(.5,0,0,.5,0,0) matrix(2,0,0,2,0,0)" id="path" ` +
`fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'composedTransformPathIdentity');
t.equals(d, svgElement.getElementById('path').attributes.d.value);
t.false(svgElement.getElementById('path').attributes.transform);
t.end();
});
// Transform has multiple matrices that don't compose to identity
test('composedTransformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="230px" height="230px" viewBox="-30 -30 200 200">` +
`<path transform="matrix(.5,0,0,.5,0,0) matrix(3,0,0,3,1,2)" id="path" ` +
`fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'composedTransformPath');
const transformedPath = 'M -29.5 -29 L 0.5 16 L 8 8.5 L 15.5 8.5 L 15.5 16 C 15.5 16 30.5 16 23 38.5 ' +
'C 15.5 61 23 46 23 61 Q 30.5 76 45.5 91 Q 60.5 106 45.5 106 Q 30.5 106 30.5 121 M 0.5 1 ' +
'A 60 75 0 1 1 0.5 151 Z ';
t.equals(transformedPath, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Transform is on parent group
test('parentTransformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="300px" height="300px" viewBox="-50 -50 250 250">` +
`<g id="group" transform="matrix(2, 0, 0, 2, 0, 0)">` +
`<path id="path" fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</g>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'parentTransformPath');
const doubled = 'M -40 -40 L 0 20 L 10 10 L 20 10 L 20 20 C 20 20 40 20 30 50 C 20 80 30 60 30 80 ' +
'Q 40 100 60 120 Q 80 140 60 140 Q 40 140 40 160 M 0 0 A 80 100 0 1 1 0 200 Z ';
t.equals(doubled, svgElement.getElementById('path').attributes.d.value);
// Transform should be gone from both child and parent
t.false(svgElement.getElementById('group').attributes.transform);
t.false(svgElement.getElementById('path').attributes.transform);
t.equals('2', svgElement.getElementById('path').attributes['stroke-width'].value);
t.end();
});
// Nested path
test('nestedNoTransformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="100px" height="100px" viewBox="0 0 100 100">` +
`<g>` +
`<path id="path" fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</g>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'nestedNoTransformPath');
t.equals(d, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Transforms on parents and children
test('nestedTransformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="300px" height="300px" viewBox="-40 -40 260 260">` +
`<g transform=" matrix(1.5 0 0 1.5 0 0) ">` +
`<g>` +
`<path transform="matrix(1.5,0,0,1.5,0,0)" id="path" fill="#0000" stroke="red" stroke-width="1" ` +
`d="${d}"/>` +
`</g>` +
`</g>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'nestedTransformPath');
const quartered = 'M -45 -45 L 0 22.5 L 11.25 11.25 L 22.5 11.25 L 22.5 22.5 C 22.5 22.5 45 22.5 33.75 56.25 ' +
'C 22.5 90 33.75 67.5 33.75 90 Q 45 112.5 67.5 135 Q 90 157.5 67.5 157.5 Q 45 157.5 45 180 M 0 0 ' +
'A 90 112.5 0 1 1 0 225 Z ';
t.equals(quartered, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Transform combines all types of transforms
test('variousTransformsPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="500px" height="400px" viewBox="0 0 500 400">` +
`<path transform="rotate(25) matrix(2,0,0,2,0,0) skewX(10) translate(20) ` +
`rotate(25, 100, 100) skewY(-10) translate(-10, 4) scale(1.5,0.8) translate(40,80) " ` +
`id="path" fill="#0000" stroke="red" stroke-width="1" d="${d}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'variousTransformsPath');
const transformedPath = 'M 115.3172 96.7866 L 134.6908 171.2195 L 151.9584 175.6212 L 164.2563 185.7055 ' +
'L 159.2866 191.3881 C 159.2866 191.3881 183.8824 211.5567 156.6755 218.5202 ' +
'C 129.4685 225.4837 151.7058 224.2028 141.7664 235.568 Q 144.1249 257.0175 158.7814 288.5514 ' +
'Q 173.4379 320.0852 148.842 299.9166 Q 124.2462 279.7479 114.3068 291.1131 M 144.6301 159.8543 ' +
'A 75.4328 127.2656 -51.6345 1 1 45.2364 273.5062 Z ';
t.equals(transformedPath, svgElement.getElementById('path').attributes.d.value);
t.false(svgElement.getElementById('path').attributes.transform);
t.end();
});
// Transform is pushed down to other children
test('siblingsTransformPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="160px" height="160px" viewBox="-20 -20 140 140">` +
`<g transform="matrix(0.5 0 0 0.5 0 0)">` +
`<g transform="translate(10, 20)">` +
`<path transform="matrix(2 0 0 2 0 0)" id="path" fill="#0000" stroke="red" stroke-width="1" ` +
`d="${d}"/>` +
`<rect id="sibling" x="40" y="40" width="40" height="40" fill="#0000" stroke="blue" />` +
`</g>` +
`<rect id="distantCousin1" transform="translate(-0.5,-.5)" ` +
`x="40" y="40" width="40" height="40" fill="#0000" stroke="blue" />` +
`<rect id="distantCousin2" x="40" y="40" width="40" height="40" fill="#0000" stroke="blue" />` +
`</g>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'siblingsTransformPath');
t.equals('matrix(0.5,0,0,0.5,5,10)', svgElement.getElementById('sibling').attributes.transform.value);
t.equals('matrix(0.5,0,0,0.5,-0.25,-0.25)', svgElement.getElementById('distantCousin1').attributes.transform.value);
t.equals('matrix(0.5,0,0,0.5,0,0)', svgElement.getElementById('distantCousin2').attributes.transform.value);
t.end();
});
// Stroke width is pushed down to leaf level
test('siblingsStroke', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="120px" height="120px" viewBox="-20 -20 100 100">` +
`<g stroke-width="5">` +
`<g stroke-width="10">` +
`<path transform="matrix(.5 0 0 .5 0 0)" fill="#0000" stroke="red" stroke-width="1" ` +
`d="${d}" id="path"/>` +
`<rect id="sibling" x="10" y="10" width="40" height="40" fill="#0000" stroke="blue" />` +
`</g>` +
`<rect id="distantCousin1" stroke-width="15" x="25" y="25" width="40" height="40" fill="#0000" ` +
`stroke="blue" />` +
`<rect id="distantCousin2" x="40" y="40" width="40" height="40" fill="#0000" stroke="blue" />` +
`</g>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'siblingsStroke');
t.equals('10', svgElement.getElementById('sibling').attributes['stroke-width'].value);
t.equals('15', svgElement.getElementById('distantCousin1').attributes['stroke-width'].value);
t.equals('5', svgElement.getElementById('distantCousin2').attributes['stroke-width'].value);
t.end();
});
// Nested stroke width is transformed
test('transformedNestedStroke', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="650px" height="170px" viewBox="-100 -20 550 150">` +
`<g stroke-width="1" transform="scale(-.5,.5)">` +
`<path transform="matrix(5 0 0 2 0 0)" fill="#0000" stroke="red" d="${d}" id="path"/>` +
`</g>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'transformedNestedStroke');
const quadraticMean = Math.sqrt(((5 / 2 * 5 / 2) + (2 / 2 * 2 / 2)) / 2);
t.equals(`${quadraticMean}`, svgElement.getElementById('path').attributes['stroke-width'].value);
t.end();
});
// Various transforms applied to a path with relative instructions
test('variousTransformsRelativePath', t => {
const pathData = 'm 20 20 0 20 10 0 l 5 5 h 10 v 10 c 0 10 0 20 15 5 z ' +
'm -50 5 s 15 0 15 10 q 20 10 10 20 t 20 10 20 10 a 30 10 30 1 1 0 1 ';
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="200px" height="150px" viewBox="0 0 200 150">` +
`<path transform="skewX(10) rotate(-25) translate(5 20)" ` +
`id="path" fill="#0000" stroke="red" stroke-width="5" d="${pathData}" />` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'variousTransformsRelativePath');
const transformed = 'M 44.0917 25.6869 L 55.7402 43.813 L 64.0581 39.5868 L 71.1292 42.0053 L 79.447 37.7791 ' +
'L 85.2713 46.8422 C 91.0955 55.9052 96.9198 64.9683 100.6603 45.0344 Z M 5.4144 51.3493 ' +
'C 5.4144 51.3493 17.8912 45.01 23.7155 54.0731 Q 46.1755 54.6838 43.6819 67.9731 ' +
'Q 41.1882 81.2623 66.1419 68.5838 Q 91.0955 55.9052 88.6019 69.1945 ' +
'A 9.8314 30.5145 -83.9007 1 1 89.1843 70.1008 ';
t.equals(transformed, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Testing scale transform, elliptical paths
test('scaleTransformEllipticalPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="600px" height="250px" viewBox="0 0 600 250"> ` +
`<path transform="scale(.5)" ` +
`id="path" fill="#0000" stroke="red" stroke-width="5" d="${ellipticalPath}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'scaleTransformEllipticalPath');
const scaled = 'M 5 150 L 30 137.5 A 12.5 12.5 -50.7685 0 1 55 125 L 80 112.5 A 12.5 25 -45 0 1 105 100 ' +
'L 130 87.5 A 12.5 37.5 -30 0 1 155 75 L 180 62.5 A 12.5 50 -15 0 1 205 50 L 230 37.5 L 230 62.5 L 205 75 ' +
'A 12.5 12.5 -50.7685 1 1 180 87.5 L 155 100 A 12.5 25 45 1 1 130 112.5 L 105 125 A 12.5 37.5 30 1 1 80 137.5 ' +
'L 55 150 A 12.5 50 15 1 1 30 162.5 L 5 175 ';
t.equals(scaled, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Testing invert transform, elliptical paths
test('invertTransformEllipticalPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="600px" height="500px" viewBox="0 0 600 500"> ` +
`<path transform="matrix(0 1 1 0 0 0)" ` +
`id="path" fill="#0000" stroke="red" stroke-width="5" d="${ellipticalPath}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'invertTransformEllipticalPath');
const inverted = 'M 300 10 L 275 60 A 25 25 -50.7685 0 0 250 110 L 225 160 A 25 50 -45 0 0 200 210 ' +
'L 175 260 A 25 75 -60 0 0 150 310 L 125 360 A 25 100 -75 0 0 100 410 L 75 460 L 125 460 L 150 410 ' +
'A 25 25 -50.7685 1 0 175 360 L 200 310 A 25 50 45 1 0 225 260 L 250 210 A 25 75 60 1 0 275 160 L 300 110 ' +
'A 25 100 75 1 0 325 60 L 350 10 ';
t.equals(inverted, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Testing rotate transform, elliptical paths
test('rotateTransformEllipticalPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="600px" height="600px" viewBox="0 0 600 600"> ` +
`<path transform="rotate(-255) translate(0,-500)" ` +
`id="path" fill="#0000" stroke="red" stroke-width="5" d="${ellipticalPath}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'rotateTransformEllipticalPath');
const rotated = 'M 190.597 61.4231 L 201.8042 116.1898 A 25 25 -50.7685 0 1 213.0114 170.9566 ' +
'L 224.2186 225.7234 A 25 50 60 0 1 235.4257 280.4901 L 246.6329 335.2569 A 25 75 75 0 1 257.8401 390.0237 ' +
'L 269.0473 444.7904 A 25 100 90 0 1 280.2545 499.5572 L 291.4617 554.324 L 243.1654 541.383 ' +
'L 231.9582 486.6163 A 25 25 -50.7685 1 1 220.751 431.8495 L 209.5438 377.0827 ' +
'A 25 50 -30 1 1 198.3367 322.316 L 187.1295 267.5492 A 25 75 -45 1 1 175.9223 212.7824 L 164.7151 158.0156 ' +
'A 25 100 -60 1 1 153.5079 103.2489 L 142.3007 48.4821 ';
t.equals(rotated, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Testing skewX transform, elliptical paths
test('skewXTransformEllipticalPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="600px" height="350px" viewBox="0 0 600 350"> ` +
`<path transform="skewX(-20)" ` +
`id="path" fill="#0000" stroke="red" stroke-width="5" d="${ellipticalPath}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'skewXTransformEllipticalPath');
const skewed = 'M -99.1911 300 L -40.0918 275 A 20.861 29.9602 50.1571 0 1 19.0074 250 L 78.1067 225 ' +
'A 29.7657 41.9946 -28.5971 0 1 137.206 200 L 196.3052 175 A 28.0557 66.8312 -9.069 0 1 255.4045 150 ' +
'L 314.5037 125 A 25.6458 97.482 6.9831 0 1 373.603 100 L 432.7022 75 L 414.5037 125 L 355.4045 150 ' +
'A 20.861 29.9602 50.1571 1 1 296.3052 175 L 237.206 200 A 20.8982 59.8139 53.2248 1 1 178.1067 225 ' +
'L 119.0074 250 A 21.0102 89.2423 43.6881 1 1 59.9082 275 L 0.8089 300 ' +
'A 21.8464 114.4351 32.9028 1 1 -58.2903 325 L -117.3896 350 ';
t.equals(skewed, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Testing skewY transform, elliptical paths
test('skewYTransformEllipticalPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="600px" height="400px" viewBox="0 0 600 400"> ` +
`<path transform="skewY(-20)" ` +
`id="path" fill="#0000" stroke="red" stroke-width="5" d="${ellipticalPath}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'skewYTransformEllipticalPath');
const skewed = 'M 10 296.3603 L 60 253.1618 A 20.861 29.9602 39.8429 0 1 110 209.9633 L 160 166.7648 ' +
'A 29.7657 41.9946 -61.4029 0 1 210 123.5663 L 260 80.3677 A 29.4429 63.6825 -34.2139 0 1 310 37.1692 ' +
'L 360 -6.0293 A 27.3833 91.2964 -14.925 0 1 410 -49.2278 L 460 -92.4263 L 460 -42.4263 L 410 0.7722 ' +
'A 20.861 29.9602 39.8429 1 1 360 43.9707 L 310 87.1692 A 20.8982 59.8139 36.7752 1 1 260 130.3677 ' +
'L 210 173.5663 A 21.4899 87.2503 26.3947 1 1 160 216.7648 L 110 259.9633 ' +
'A 22.8454 109.4312 14.6345 1 1 60 303.1618 L 10 346.3603 ';
t.equals(skewed, svgElement.getElementById('path').attributes.d.value);
t.end();
});
// Testing various transforms, elliptical paths
test('variousTransformsEllipticalPath', t => {
const svgString =
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" width="600px" height="600px" viewBox="0 -200 600 300"> ` +
`<path transform="skewX(10) rotate(-25) translate(-50 -200)" ` +
`id="path" fill="#0000" stroke="red" stroke-width="5" d="${ellipticalPath}"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window);
comparisonFileAppend(svgString, svgElement, 'variousTransformsEllipticalPath');
const transformed = 'M 24.9709 107.5355 L 51.9997 63.7469 A 22.8929 27.3011 -47.5192 0 1 79.0286 19.9583 ' +
'L 106.0574 -23.8303 A 23.5927 52.9826 -69.05 0 1 133.0862 -67.6189 L 160.115 -111.4075 ' +
'A 23.0486 81.3499 -57.6917 0 1 187.1438 -155.1961 L 214.1727 -198.9847 ' +
'A 22.8991 109.1745 -45.4789 0 1 241.2015 -242.7734 L 268.2303 -286.562 L 297.3515 -241.2466 ' +
'L 270.3227 -197.458 A 22.8929 27.3011 -47.5192 1 1 243.2939 -153.6694 L 216.2651 -109.8807 ' +
'A 26.0319 48.0181 7.1283 1 1 189.2363 -66.0921 L 162.2074 -22.3035 ' +
'A 24.9487 75.1544 -6.3334 1 1 135.1786 21.4851 L 108.1498 65.2737 ' +
'A 23.9235 104.4996 -19.9344 1 1 81.121 109.0623 L 54.0922 152.8509 ';
t.equals(transformed, svgElement.getElementById('path').attributes.d.value);
t.end();
});
test('linearGradientTransformSquareSkewY', t => {
const svgString =
`<svg version="1.1" width="200" height="200" viewBox="-100 0 100 200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">` +
`<defs>` +
`<linearGradient id="grad_a" x2="0" y2="1">` +
`<stop offset="0" stop-color="green" stop-opacity="1"/>` +
`<stop offset="1" stop-color="red" stop-opacity="1"/>` +
`</linearGradient>` +
`</defs>` +
`<path id="path" fill="url(#grad_a)" stroke="#000000" stroke-width="2" d="M0,0 0,100 100,100 100,0 z" ` +
`transform="translate(-50, 50) scale(-.75, 1) skewY(-15)"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, {width: 100, height: 100, x: 0, y: 0});
comparisonFileAppend(svgString, svgElement, 'linearGradientTransformSquareSkewY');
t.equals('-50', svgElement.getElementById('grad_a-0.75,-0.2679491924311227,0,1,-50,50').attributes.x1.value);
t.equals('-81.6826', svgElement.getElementById('grad_a-0.75,-0.2679491924311227,0,1,-50,50').attributes.x2.value);
t.equals('50', svgElement.getElementById('grad_a-0.75,-0.2679491924311227,0,1,-50,50').attributes.y1.value);
t.equals('138.6809', svgElement.getElementById('grad_a-0.75,-0.2679491924311227,0,1,-50,50').attributes.y2.value);
t.end();
});
test('linearGradientTransformSquareSkewX', t => {
const svgString =
`<svg version="1.1" width="200" height="200" viewBox="-100 0 100 200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">` +
`<defs>` +
`<linearGradient id="grad_b" x2="0" y2="1">` +
`<stop offset="0" stop-color="green" stop-opacity="1"/>` +
`<stop offset="1" stop-color="red" stop-opacity="1"/>` +
`</linearGradient>` +
`</defs>` +
`<path id="path" fill="url(#grad_b)" stroke="#000000" stroke-width="2" d="M0,0 0,100 100,100 100,0 z" ` +
`transform="translate(-50, 50) scale(-.75, 1) skewX(-15)"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, {width: 100, height: 100, x: 0, y: 0});
comparisonFileAppend(svgString, svgElement, 'linearGradientTransformSquareSkewX');
t.equals('-50', svgElement.getElementById('grad_b-0.75,0,0.20096189432334202,1,-50,50').attributes.x1.value);
t.equals('-50', svgElement.getElementById('grad_b-0.75,0,0.20096189432334202,1,-50,50').attributes.x2.value);
t.equals('50', svgElement.getElementById('grad_b-0.75,0,0.20096189432334202,1,-50,50').attributes.y1.value);
t.equals('150', svgElement.getElementById('grad_b-0.75,0,0.20096189432334202,1,-50,50').attributes.y2.value);
t.end();
});
test('linearGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">` +
`<defs>` +
`<linearGradient id="grad_c">` +
`<stop offset="0" stop-color="green" stop-opacity="1"/>` +
`<stop offset="1" stop-color="red" stop-opacity="1"/>` +
`</linearGradient>` +
`</defs>` +
`<path id="path" fill="url(#grad_c)" stroke="#000000" stroke-width="2" d="${trickyBoundsPath}" ` +
`transform="scale(.75) skewX(-15)"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'linearGradientTransform');
t.equals('26.9399', svgElement.getElementById('grad_c-.75,0,-0.20096189432334202,0.75,0,0').attributes.x1.value);
t.equals('84.6436', svgElement.getElementById('grad_c-.75,0,-0.20096189432334202,0.75,0,0').attributes.x2.value);
t.equals('0.9571', svgElement.getElementById('grad_c-.75,0,-0.20096189432334202,0.75,0,0').attributes.y1.value);
t.equals('16.4187', svgElement.getElementById('grad_c-.75,0,-0.20096189432334202,0.75,0,0').attributes.y2.value);
t.end();
});
test('reusedLinearGradientTransform', t => {
const svgString =
`<svg version="1.1" width="200" height="150" viewBox="0 0 200 150" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">` +
`<defs>` +
`<linearGradient id="grad_1">` +
`<stop offset="0" stop-color="green" stop-opacity="1"/>` +
`<stop offset="1" stop-color="red" stop-opacity="1"/>` +
`</linearGradient>` +
`</defs>` +
`<path id="path" fill="url(#grad_1)" stroke="#000000" stroke-width="2" d="${trickyBoundsPath}" ` +
`transform="scale(.75) skewX(-15)"/>` +
`<path id="path2" fill="url(#grad_1)" stroke="#000000" stroke-width="2" d="${trickyBoundsPath}" ` +
`transform="translate(150, 150) rotate(180)"/>` +
`<path id="path3" fill="url(#grad_1)" stroke="#000000" stroke-width="2" d="${trickyBoundsPath}" ` +
`transform="translate(150, 150) rotate(180)"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'reusedLinearGradientTransform');
t.equals('26.9399', svgElement.getElementById('grad_1-.75,0,-0.20096189432334202,0.75,0,0').attributes.x1.value);
t.equals('84.6436', svgElement.getElementById('grad_1-.75,0,-0.20096189432334202,0.75,0,0').attributes.x2.value);
t.equals('0.9571', svgElement.getElementById('grad_1-.75,0,-0.20096189432334202,0.75,0,0').attributes.y1.value);
t.equals('16.4187', svgElement.getElementById('grad_1-.75,0,-0.20096189432334202,0.75,0,0').attributes.y2.value);
t.equals('113.7382', svgElement.getElementById('grad_1-1,1.2246467991473532e-16,-1.2246467991473532e-16,-1,150,150')
.attributes.x1.value);
t.equals('31.2761', svgElement.getElementById('grad_1-1,1.2246467991473532e-16,-1.2246467991473532e-16,-1,150,150')
.attributes.x2.value);
t.equals('148.7239', svgElement.getElementById('grad_1-1,1.2246467991473532e-16,-1.2246467991473532e-16,-1,150,150')
.attributes.y1.value);
t.equals('148.7239', svgElement.getElementById('grad_1-1,1.2246467991473532e-16,-1.2246467991473532e-16,-1,150,150')
.attributes.y2.value);
t.end();
});
test('nestedLinearGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="grad_2" x2="0" y2="1">
<stop offset="0" stop-color="#7F00FF" stop-opacity="1"/>
<stop offset="1" stop-color="#FF9400" stop-opacity="1"/>
</linearGradient>
</defs>
<g transform="scale(.75) skewX(-15)">
<path id="path" fill="url(#grad_2)" stroke="#003FFF" stroke-width="5" d="${trickyBoundsPath}" />
</g>
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'nestedLinearGradientTransform');
t.equals('26.9399', svgElement.getElementById('grad_2-.75,0,-0.20096189432334202,0.75,0,0').attributes.x1.value);
t.equals('26.9399', svgElement.getElementById('grad_2-.75,0,-0.20096189432334202,0.75,0,0').attributes.x2.value);
t.equals('0.9571', svgElement.getElementById('grad_2-.75,0,-0.20096189432334202,0.75,0,0').attributes.y1.value);
t.equals('62.8036', svgElement.getElementById('grad_2-.75,0,-0.20096189432334202,0.75,0,0').attributes.y2.value);
t.end();
});
test('percentLinearGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="grad_3" x2="50%" y2="50%">
<stop offset="0" stop-color="#7F00FF" stop-opacity="1"/>
<stop offset="1" stop-color="#FF9400" stop-opacity="1"/>
</linearGradient>
</defs>
<path id="path" fill="url(#grad_3)" stroke="#003FFF" stroke-width="5" d="${trickyBoundsPath}"
transform="scale(.75) skewX(-15)" />
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'percentLinearGradientTransform');
t.equals('26.9399', svgElement.getElementById('grad_3-.75,0,-0.20096189432334202,0.75,0,0').attributes.x1.value);
t.equals('50.6569', svgElement.getElementById('grad_3-.75,0,-0.20096189432334202,0.75,0,0').attributes.x2.value);
t.equals('0.9571', svgElement.getElementById('grad_3-.75,0,-0.20096189432334202,0.75,0,0').attributes.y1.value);
t.equals('31.029', svgElement.getElementById('grad_3-.75,0,-0.20096189432334202,0.75,0,0').attributes.y2.value);
t.end();
});
test('userSpaceLinearGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="grad_4" x1="20" x2="80" y1="20" y2="80" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7F00FF" stop-opacity="1"/>
<stop offset="1" stop-color="#FF9400" stop-opacity="1"/>
</linearGradient>
</defs>
<path id="path" fill="url(#grad_4)" stroke="#003FFF" stroke-width="5" d="${trickyBoundsPath}"
transform="scale(.75) skewX(-15)" />
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'userSpaceLinearGradientTransform');
t.equals('10.9808', svgElement.getElementById('grad_4-.75,0,-0.20096189432334202,0.75,0,0').attributes.x1.value);
t.equals('45.494', svgElement.getElementById('grad_4-.75,0,-0.20096189432334202,0.75,0,0').attributes.x2.value);
t.equals('15', svgElement.getElementById('grad_4-.75,0,-0.20096189432334202,0.75,0,0').attributes.y1.value);
t.equals('58.761', svgElement.getElementById('grad_4-.75,0,-0.20096189432334202,0.75,0,0').attributes.y2.value);
t.end();
});
test('degenerateLinearGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">` +
`<defs>` +
`<linearGradient id="grad_d" x1="50%" x2="50%" y1="50%" y2="50%">` +
`<stop offset="0" stop-color="green" stop-opacity="1"/>` +
`<stop offset="1" stop-color="red" stop-opacity="1"/>` +
`</linearGradient>` +
`</defs>` +
`<path id="path" fill="url(#grad_d)" stroke="#000000" stroke-width="2" d="${trickyBoundsPath}" ` +
`transform="scale(.75) skewX(-15)"/>` +
`</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'linearGradientTransform');
t.end();
});
test('nestedRadialGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<radialGradient id="grad_5">
<stop offset="0" stop-color="#7F00FF" stop-opacity="1"/>
<stop offset="1" stop-color="#FF9400" stop-opacity="1"/>
</radialGradient>
</defs>
<g transform="scale(.75) skewX(-15)">
<path id="path" fill="url(#grad_5)" stroke="#003FFF" stroke-width="5" d="${trickyBoundsPath}" />
</g>
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement,
'nestedRadialGradientTransform. Note that radial gradients are not expected to match exactly.');
t.equals('49.5773', svgElement.getElementById('grad_5-.75,0,-0.20096189432334202,0.75,0,0').attributes.cx.value);
t.equals('31.8804', svgElement.getElementById('grad_5-.75,0,-0.20096189432334202,0.75,0,0').attributes.cy.value);
t.equals('30.9233', svgElement.getElementById('grad_5-.75,0,-0.20096189432334202,0.75,0,0').attributes.r.value);
t.end();
});
test('focalRadialGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<radialGradient id="grad_6" fx=".75" fy=".75">
<stop offset="0" stop-color="#7F00FF" stop-opacity="1"/>
<stop offset="1" stop-color="#FF9400" stop-opacity="1"/>
</radialGradient>
</defs>
<g transform="scale(.75) skewX(-15)">
<path id="path" fill="url(#grad_6)" stroke="#003FFF" stroke-width="5" d="${trickyBoundsPath}" />
</g>
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'focalRadialGradientTransform');
t.equals('49.5773', svgElement.getElementById('grad_6-.75,0,-0.20096189432334202,0.75,0,0').attributes.cx.value);
t.equals('31.8804', svgElement.getElementById('grad_6-.75,0,-0.20096189432334202,0.75,0,0').attributes.cy.value);
t.equals('30.9233', svgElement.getElementById('grad_6-.75,0,-0.20096189432334202,0.75,0,0').attributes.r.value);
t.equals('60.896', svgElement.getElementById('grad_6-.75,0,-0.20096189432334202,0.75,0,0').attributes.fx.value);
t.equals('47.342', svgElement.getElementById('grad_6-.75,0,-0.20096189432334202,0.75,0,0').attributes.fy.value);
t.end();
});
test('percentRadialGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<radialGradient id="grad_7" cx="60%" cy="80%" fx="75%" fy="85%">
<stop offset="0" stop-color="#7F00FF" stop-opacity="1"/>
<stop offset="1" stop-color="#FF9400" stop-opacity="1"/>
</radialGradient>
</defs>
<g transform="scale(.75) skewX(-15)">
<path id="path" fill="url(#grad_7)" stroke="#003FFF" stroke-width="5" d="${trickyBoundsPath}" />
</g>
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'percentRadialGradientTransform');
t.equals('50.7905', svgElement.getElementById('grad_7-.75,0,-0.20096189432334202,0.75,0,0').attributes.cx.value);
t.equals('50.4343', svgElement.getElementById('grad_7-.75,0,-0.20096189432334202,0.75,0,0').attributes.cy.value);
t.equals('30.9233', svgElement.getElementById('grad_7-.75,0,-0.20096189432334202,0.75,0,0').attributes.r.value);
t.equals('59.2389', svgElement.getElementById('grad_7-.75,0,-0.20096189432334202,0.75,0,0').attributes.fx.value);
t.equals('53.5267', svgElement.getElementById('grad_7-.75,0,-0.20096189432334202,0.75,0,0').attributes.fy.value);
t.end();
});
test('userSpaceRadialGradientTransform', t => {
const svgString =
`<svg version="1.1" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<radialGradient id="grad_8" cx="80" r="10" cy="60" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7F00FF" stop-opacity="1"/>
<stop offset="1" stop-color="#FF9400" stop-opacity="1"/>
</radialGradient>
</defs>
<path id="path" fill="url(#grad_8)" stroke="#003FFF" stroke-width="5" d="${trickyBoundsPath}"
transform="scale(.75) skewX(-15)" />
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window, trickyBoundsPathBounds);
comparisonFileAppend(svgString, svgElement, 'userSpaceRadialGradientTransform');
t.equals('47.9423', svgElement.getElementById('grad_8-.75,0,-0.20096189432334202,0.75,0,0').attributes.cx.value);
t.equals('45', svgElement.getElementById('grad_8-.75,0,-0.20096189432334202,0.75,0,0').attributes.cy.value);
t.equals('7.5', svgElement.getElementById('grad_8-.75,0,-0.20096189432334202,0.75,0,0').attributes.r.value);
t.end();
});
test('blackFillsBugFix', t => {
const svgString =
`<svg width="26px" height="14px" viewBox="0 0 26 14" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<g id="Page-1" stroke="none" stroke-width="5" fill="none" fill-rule="evenodd">
<path d="M23.87,0.75 C20.19,0.75 16.38,4.75 16.38,4.75 L16.38,9.25 C16.38,9.25 20.19,13.25 23.87,13.25
C24.6811343,11.269062 25.0661499,9.139551 25,7 C25.0661499,4.860449 24.6811343,2.73093802 23.87,0.75 Z"
id="Shape" stroke="#149948"/>
</g>
</g>
</svg>`;
const svgElement = parser.parseFromString(svgString, 'text/xml').documentElement;
transformStrokeWidths(svgElement, window,
{
height: 12.5,
width: 24.020904541015625,
x: 0.9896308183670044,
y: 0.75
});
comparisonFileAppend(svgString, svgElement, 'blackFillsBugFix');
t.equals('none', svgElement.getElementById('Shape').attributes.fill.value);
t.equals('5', svgElement.getElementById('Shape').attributes['stroke-width'].value);
t.end();
});
outputComparisonFile();