diff --git a/lib/svgo.test.js b/lib/svgo.test.js index 303a30757..28d83466a 100644 --- a/lib/svgo.test.js +++ b/lib/svgo.test.js @@ -385,7 +385,7 @@ test('encode as datauri', () => { plugins: ['convertTransform'], }); expect(dataSinglePass).toMatchInlineSnapshot( - `"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20transform%3D%22scale(2)rotate(-45%20130.898%20-126.14)%22%2F%3E%3C%2Fsvg%3E"`, + `"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20transform%3D%22rotate(-45%20261.757%20-252.243)scale(2)%22%2F%3E%3C%2Fsvg%3E"`, ); const { data: dataMultiPass } = optimize(input, { multipass: true, @@ -393,6 +393,6 @@ test('encode as datauri', () => { plugins: ['convertTransform'], }); expect(dataMultiPass).toMatchInlineSnapshot( - `"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20transform%3D%22rotate(-45%20261.796%20-252.28)scale(2)%22%2F%3E%3C%2Fsvg%3E"`, + `"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20transform%3D%22rotate(-45%20261.757%20-252.243)scale(2)%22%2F%3E%3C%2Fsvg%3E"`, ); }); diff --git a/plugins/_transforms.js b/plugins/_transforms.js index b4ce055f3..e545095fc 100644 --- a/plugins/_transforms.js +++ b/plugins/_transforms.js @@ -1,9 +1,10 @@ -import { toFixed } from '../lib/svgo/tools.js'; +import { cleanupOutData, toFixed } from '../lib/svgo/tools.js'; /** * @typedef {{ name: string, data: number[] }} TransformItem * @typedef {{ * convertToShorts: boolean, + * degPrecision?: number, * floatPrecision: number, * transformPrecision: number, * matrixToTransform: boolean, @@ -163,115 +164,325 @@ const mth = { }; /** - * Decompose matrix into simple transforms. - * - * @param {TransformItem} transform - * @param {TransformParams} params - * @returns {TransformItem[]} - * @see https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html + * @param {TransformItem} matrix + * @returns {TransformItem[][]} */ -export const matrixToTransform = (transform, params) => { - const floatPrecision = params.floatPrecision; - const data = transform.data; - const transforms = []; +const getDecompositions = (matrix) => { + const decompositions = []; + const qrab = decomposeQRAB(matrix); + const qrcd = decomposeQRCD(matrix); + + if (qrab) { + decompositions.push(qrab); + } + if (qrcd) { + decompositions.push(qrcd); + } + return decompositions; +}; + +/** + * @param {TransformItem} matrix + * @returns {TransformItem[]|undefined} + * @see {@link https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html} Where applicable, variables are named in accordance with this document. + */ +const decomposeQRAB = (matrix) => { + const data = matrix.data; + + const [a, b, c, d, e, f] = data; + const delta = a * d - b * c; + if (delta === 0) { + return; + } + const r = Math.hypot(a, b); + + if (r === 0) { + return; + } + + const decomposition = []; + const cosOfRotationAngle = a / r; // [..., ..., ..., ..., tx, ty] → translate(tx, ty) - if (data[4] || data[5]) { - transforms.push({ + if (e || f) { + decomposition.push({ name: 'translate', - data: data.slice(4, data[5] ? 6 : 5), + data: [e, f], }); } - let sx = toFixed(Math.hypot(data[0], data[1]), params.transformPrecision); - let sy = toFixed( - (data[0] * data[3] - data[1] * data[2]) / sx, - params.transformPrecision, - ); - const colsSum = data[0] * data[2] + data[1] * data[3]; - const rowsSum = data[0] * data[1] + data[2] * data[3]; - const scaleBefore = rowsSum !== 0 || sx === sy; - - // [sx, 0, tan(a)·sy, sy, 0, 0] → skewX(a)·scale(sx, sy) - if (!data[1] && data[2]) { - transforms.push({ + if (cosOfRotationAngle !== 1) { + const rotationAngleRads = Math.acos(cosOfRotationAngle); + decomposition.push({ + name: 'rotate', + data: [mth.deg(b < 0 ? -rotationAngleRads : rotationAngleRads), 0, 0], + }); + } + + const sx = r; + const sy = delta / sx; + if (sx !== 1 || sy !== 1) { + decomposition.push({ name: 'scale', data: [sx, sy] }); + } + + const ac_plus_bd = a * c + b * d; + if (ac_plus_bd) { + decomposition.push({ name: 'skewX', - data: [mth.atan(data[2] / sy, floatPrecision)], + data: [mth.deg(Math.atan(ac_plus_bd / (a * a + b * b)))], + }); + } + + return decomposition; +}; + +/** + * @param {TransformItem} matrix + * @returns {TransformItem[]|undefined} + * @see {@link https://frederic-wang.fr/decomposition-of-2d-transform-matrices.html} Where applicable, variables are named in accordance with this document. + */ +const decomposeQRCD = (matrix) => { + const data = matrix.data; + + const [a, b, c, d, e, f] = data; + const delta = a * d - b * c; + if (delta === 0) { + return; + } + const s = Math.hypot(c, d); + if (s === 0) { + return; + } + + const decomposition = []; + + if (e || f) { + decomposition.push({ + name: 'translate', + data: [e, f], }); + } - // [sx, sx·tan(a), 0, sy, 0, 0] → skewY(a)·scale(sx, sy) - } else if (data[1] && !data[2]) { - transforms.push({ + const rotationAngleRads = Math.PI / 2 - (d < 0 ? -1 : 1) * Math.acos(-c / s); + decomposition.push({ + name: 'rotate', + data: [mth.deg(rotationAngleRads), 0, 0], + }); + + const sx = delta / s; + const sy = s; + if (sx !== 1 || sy !== 1) { + decomposition.push({ name: 'scale', data: [sx, sy] }); + } + + const ac_plus_bd = a * c + b * d; + if (ac_plus_bd) { + decomposition.push({ name: 'skewY', - data: [mth.atan(data[1] / data[0], floatPrecision)], + data: [mth.deg(Math.atan(ac_plus_bd / (c * c + d * d)))], }); - sx = data[0]; - sy = data[3]; - - // [sx·cos(a), sx·sin(a), sy·-sin(a), sy·cos(a), x, y] → rotate(a[, cx, cy])·(scale or skewX) or - // [sx·cos(a), sy·sin(a), sx·-sin(a), sy·cos(a), x, y] → scale(sx, sy)·rotate(a[, cx, cy]) (if !scaleBefore) - } else if (!colsSum || (sx === 1 && sy === 1) || !scaleBefore) { - if (!scaleBefore) { - sx = Math.hypot(data[0], data[2]); - sy = Math.hypot(data[1], data[3]); - - if (toFixed(data[0], params.transformPrecision) < 0) { - sx = -sx; - } + } - if ( - data[3] < 0 || - (Math.sign(data[1]) === Math.sign(data[2]) && - toFixed(data[3], params.transformPrecision) === 0) - ) { - sy = -sy; - } + return decomposition; +}; + +/** + * Convert translate(tx,ty)rotate(a) to rotate(a,cx,cy). + * @param {number} tx + * @param {number} ty + * @param {number} a + * @returns {TransformItem} + */ +const mergeTranslateAndRotate = (tx, ty, a) => { + // From https://www.w3.org/TR/SVG11/coords.html#TransformAttribute: + // We have translate(tx,ty) rotate(a). This is equivalent to [cos(a) sin(a) -sin(a) cos(a) tx ty]. + // + // rotate(a,cx,cy) is equivalent to translate(cx, cy) rotate(a) translate(-cx, -cy). + // Multiplying the right side gives the matrix + // [cos(a) sin(a) -sin(a) cos(a) + // -cx * cos(a) + cy * sin(a) + cx + // -cx * sin(a) - cy * cos(a) + cy + // ] + // + // We need cx and cy such that + // tx = -cx * cos(a) + cy * sin(a) + cx + // ty = -cx * sin(a) - cy * cos(a) + cy + // + // Solving these for cx and cy gives + // cy = (d * ty + e * tx)/(d^2 + e^2) + // cx = (tx - e * cy) / d + // where d = 1 - cos(a) and e = sin(a) + + const rotationAngleRads = mth.rad(a); + const d = 1 - Math.cos(rotationAngleRads); + const e = Math.sin(rotationAngleRads); + const cy = (d * ty + e * tx) / (d * d + e * e); + const cx = (tx - e * cy) / d; + return { name: 'rotate', data: [a, cx, cy] }; +}; - transforms.push({ name: 'scale', data: [sx, sy] }); +/** + * @param {TransformItem} t + * @returns {Boolean} + */ +const isIdentityTransform = (t) => { + switch (t.name) { + case 'rotate': + case 'skewX': + case 'skewY': + return t.data[0] === 0; + case 'scale': + return t.data[0] === 1 && t.data[1] === 1; + case 'translate': + return t.data[0] === 0 && t.data[1] === 0; + } + return false; +}; + +/** + * Optimize matrix of simple transforms. + * @param {TransformItem[]} roundedTransforms + * @param {TransformItem[]} rawTransforms + * @returns {TransformItem[]} + */ +const optimize = (roundedTransforms, rawTransforms) => { + const optimizedTransforms = []; + + for (let index = 0; index < roundedTransforms.length; index++) { + const roundedTransform = roundedTransforms[index]; + + // Don't include any identity transforms. + if (isIdentityTransform(roundedTransform)) { + continue; } - const angle = Math.min(Math.max(-1, data[0] / sx), 1); - const rotate = [ - mth.acos(angle, floatPrecision) * - ((scaleBefore ? 1 : sy) * data[1] < 0 ? -1 : 1), - ]; - - if (rotate[0]) { - transforms.push({ name: 'rotate', data: rotate }); + const data = roundedTransform.data; + switch (roundedTransform.name) { + case 'rotate': + switch (data[0]) { + case 180: + case -180: + { + // If the next element is a scale, invert it, and don't add the rotate to the optimized array. + const next = roundedTransforms[index + 1]; + if (next && next.name === 'scale') { + optimizedTransforms.push( + createScaleTransform(next.data.map((v) => -v)), + ); + index++; + } else { + // Otherwise replace the rotate with a scale(-1). + optimizedTransforms.push({ + name: 'scale', + data: [-1], + }); + } + } + continue; + } + optimizedTransforms.push({ + name: 'rotate', + data: data.slice(0, data[1] || data[2] ? 3 : 1), + }); + break; + + case 'scale': + optimizedTransforms.push(createScaleTransform(data)); + break; + + case 'skewX': + case 'skewY': + optimizedTransforms.push({ + name: roundedTransform.name, + data: [data[0]], + }); + break; + + case 'translate': + { + // If the next item is a rotate(a,0,0), merge the translate and rotate. + // If the rotation angle is +/-180, assume it will be optimized out, and don't do the merge. + const next = roundedTransforms[index + 1]; + if ( + next && + next.name === 'rotate' && + next.data[0] !== 180 && + next.data[0] !== -180 && + next.data[0] !== 0 && + next.data[1] === 0 && + next.data[2] === 0 + ) { + // Use the un-rounded data to do the merge. + const data = rawTransforms[index].data; + optimizedTransforms.push( + mergeTranslateAndRotate( + data[0], + data[1], + rawTransforms[index + 1].data[0], + ), + ); + // Skip over the rotate. + index++; + continue; + } + } + optimizedTransforms.push({ + name: 'translate', + data: data.slice(0, data[1] ? 2 : 1), + }); + break; } + } - if (rowsSum && colsSum) - transforms.push({ - name: 'skewX', - data: [mth.atan(colsSum / (sx * sx), floatPrecision)], - }); - - // rotate(a, cx, cy) can consume translate() within optional arguments cx, cy (rotation point) - if (rotate[0] && (data[4] || data[5])) { - transforms.shift(); - const oneOverCos = 1 - data[0] / sx; - const sin = data[1] / (scaleBefore ? sx : sy); - const x = data[4] * (scaleBefore ? 1 : sy); - const y = data[5] * (scaleBefore ? 1 : sx); - const denom = (oneOverCos ** 2 + sin ** 2) * (scaleBefore ? 1 : sx * sy); - rotate.push( - (oneOverCos * x - sin * y) / denom, - (oneOverCos * y + sin * x) / denom, - ); - } + // If everything was optimized out, reture identity transform scale(1). + return optimizedTransforms.length + ? optimizedTransforms + : [{ name: 'scale', data: [1] }]; +}; - // Too many transformations, return original matrix if it isn't just a scale/translate - } else if (data[1] || data[2]) { - return [transform]; - } +/** + * @param {number[]} data + * @returns {TransformItem} + */ +const createScaleTransform = (data) => { + const scaleData = data.slice(0, data[0] === data[1] ? 1 : 2); + return { + name: 'scale', + data: scaleData, + }; +}; - if ((scaleBefore && (sx != 1 || sy != 1)) || !transforms.length) { - transforms.push({ - name: 'scale', - data: sx == sy ? [sx] : [sx, sy], +/** + * Decompose matrix into simple transforms and optimize. + * @param {TransformItem} origMatrix + * @param {TransformParams} params + * @returns {TransformItem[]} + */ +export const matrixToTransform = (origMatrix, params) => { + const decomposed = getDecompositions(origMatrix); + + let shortest; + let shortestLen = Number.MAX_VALUE; + + for (const decomposition of decomposed) { + // Make a copy of the decomposed matrix, and round all data. We need to keep the original decomposition, + // at full precision, to perform some optimizations. + const roundedTransforms = decomposition.map((transformItem) => { + const transformCopy = { + name: transformItem.name, + data: [...transformItem.data], + }; + return roundTransform(transformCopy, params); }); + + const optimized = optimize(roundedTransforms, decomposition); + const len = js2transform(optimized, params).length; + if (len < shortestLen) { + shortest = optimized; + shortestLen = len; + } } - return transforms; + return shortest ?? [origMatrix]; }; /** @@ -405,3 +616,126 @@ const multiplyTransformMatrices = (a, b) => { a[1] * b[4] + a[3] * b[5] + a[5], ]; }; + +/** + * @type {(transform: TransformItem, params: TransformParams) => TransformItem} + */ +export const roundTransform = (transform, params) => { + switch (transform.name) { + case 'translate': + transform.data = floatRound(transform.data, params); + break; + case 'rotate': + transform.data = [ + ...degRound(transform.data.slice(0, 1), params), + ...floatRound(transform.data.slice(1), params), + ]; + break; + case 'skewX': + case 'skewY': + transform.data = degRound(transform.data, params); + break; + case 'scale': + transform.data = transformRound(transform.data, params); + break; + case 'matrix': + transform.data = [ + ...transformRound(transform.data.slice(0, 4), params), + ...floatRound(transform.data.slice(4), params), + ]; + break; + } + return transform; +}; + +/** + * @type {(data: number[], params: TransformParams) => number[]} + */ +const degRound = (data, params) => { + if ( + params.degPrecision != null && + params.degPrecision >= 1 && + params.floatPrecision < 20 + ) { + return smartRound(params.degPrecision, data); + } else { + return round(data); + } +}; + +/** + * @type {(data: number[], params: TransformParams) => number[]} + */ +const floatRound = (data, params) => { + if (params.floatPrecision >= 1 && params.floatPrecision < 20) { + return smartRound(params.floatPrecision, data); + } else { + return round(data); + } +}; + +/** + * @type {(data: number[], params: TransformParams) => number[]} + */ +const transformRound = (data, params) => { + if (params.transformPrecision >= 1 && params.floatPrecision < 20) { + return smartRound(params.transformPrecision, data); + } else { + return round(data); + } +}; + +/** + * Rounds numbers in array. + * + * @type {(data: number[]) => number[]} + */ +const round = (data) => { + return data.map(Math.round); +}; + +/** + * Decrease accuracy of floating-point numbers + * in transforms keeping a specified number of decimals. + * Smart rounds values like 2.349 to 2.35. + * + * @param {number} precision + * @param {number[]} data + * @returns {number[]} + */ +const smartRound = (precision, data) => { + for ( + var i = data.length, + tolerance = +Math.pow(0.1, precision).toFixed(precision); + i--; + + ) { + if (toFixed(data[i], precision) !== data[i]) { + var rounded = +data[i].toFixed(precision - 1); + data[i] = + +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance + ? +data[i].toFixed(precision) + : rounded; + } + } + + return data; +}; + +/** + * Convert transforms JS representation to string. + * + * @param {TransformItem[]} transformJS + * @param {TransformParams} params + * @returns {string} + */ +export const js2transform = (transformJS, params) => { + const transformString = transformJS + .map((transform) => { + roundTransform(transform, params); + return `${transform.name}(${cleanupOutData(transform.data, params)})`; + }) + .join(''); + + return transformString; +}; diff --git a/plugins/convertTransform.js b/plugins/convertTransform.js index 6104b4218..cdfb81e91 100644 --- a/plugins/convertTransform.js +++ b/plugins/convertTransform.js @@ -1,8 +1,9 @@ -import { cleanupOutData, toFixed } from '../lib/svgo/tools.js'; import { + js2transform, + matrixToTransform, + roundTransform, transform2js, transformsMultiply, - matrixToTransform, } from './_transforms.js'; /** @@ -170,42 +171,6 @@ const definePrecision = (data, { ...newParams }) => { return newParams; }; -/** - * @type {(data: number[], params: TransformParams) => number[]} - */ -const degRound = (data, params) => { - if ( - params.degPrecision != null && - params.degPrecision >= 1 && - params.floatPrecision < 20 - ) { - return smartRound(params.degPrecision, data); - } else { - return round(data); - } -}; -/** - * @type {(data: number[], params: TransformParams) => number[]} - */ -const floatRound = (data, params) => { - if (params.floatPrecision >= 1 && params.floatPrecision < 20) { - return smartRound(params.floatPrecision, data); - } else { - return round(data); - } -}; - -/** - * @type {(data: number[], params: TransformParams) => number[]} - */ -const transformRound = (data, params) => { - if (params.transformPrecision >= 1 && params.floatPrecision < 20) { - return smartRound(params.transformPrecision, data); - } else { - return round(data); - } -}; - /** * Returns number of digits after the point. 0.125 → 3 * @@ -329,89 +294,3 @@ const removeUseless = (transforms) => { return true; }); }; - -/** - * Convert transforms JS representation to string. - * - * @param {TransformItem[]} transformJS - * @param {TransformParams} params - * @returns {string} - */ -const js2transform = (transformJS, params) => { - const transformString = transformJS - .map((transform) => { - roundTransform(transform, params); - return `${transform.name}(${cleanupOutData(transform.data, params)})`; - }) - .join(''); - - return transformString; -}; - -/** - * @type {(transform: TransformItem, params: TransformParams) => TransformItem} - */ -const roundTransform = (transform, params) => { - switch (transform.name) { - case 'translate': - transform.data = floatRound(transform.data, params); - break; - case 'rotate': - transform.data = [ - ...degRound(transform.data.slice(0, 1), params), - ...floatRound(transform.data.slice(1), params), - ]; - break; - case 'skewX': - case 'skewY': - transform.data = degRound(transform.data, params); - break; - case 'scale': - transform.data = transformRound(transform.data, params); - break; - case 'matrix': - transform.data = [ - ...transformRound(transform.data.slice(0, 4), params), - ...floatRound(transform.data.slice(4), params), - ]; - break; - } - return transform; -}; - -/** - * Rounds numbers in array. - * - * @type {(data: number[]) => number[]} - */ -const round = (data) => { - return data.map(Math.round); -}; - -/** - * Decrease accuracy of floating-point numbers - * in transforms keeping a specified number of decimals. - * Smart rounds values like 2.349 to 2.35. - * - * @param {number} precision - * @param {number[]} data - * @returns {number[]} - */ -const smartRound = (precision, data) => { - for ( - var i = data.length, - tolerance = +Math.pow(0.1, precision).toFixed(precision); - i--; - - ) { - if (toFixed(data[i], precision) !== data[i]) { - var rounded = +data[i].toFixed(precision - 1); - data[i] = - +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance - ? +data[i].toFixed(precision) - : rounded; - } - } - - return data; -}; diff --git a/test/plugins/_transforms.test.js b/test/plugins/_transforms.test.js index 0441d17cd..f60e54f65 100644 --- a/test/plugins/_transforms.test.js +++ b/test/plugins/_transforms.test.js @@ -31,14 +31,14 @@ describe('should correctly simplify transforms', () => { }; expect(matrixToTransform(matrix, params)).toStrictEqual([ - { - name: 'scale', - data: [99, 1], - }, { name: 'rotate', data: [-90], }, + { + name: 'scale', + data: [1, 99], + }, ]); }); @@ -50,12 +50,12 @@ describe('should correctly simplify transforms', () => { expect(matrixToTransform(matrix, params)).toStrictEqual([ { - name: 'scale', - data: [1, -1], + name: 'rotate', + data: [90], }, { - name: 'rotate', - data: [-90], + name: 'scale', + data: [1, -1], }, ]); }); diff --git a/test/plugins/convertTransform.01.svg.txt b/test/plugins/convertTransform.01.svg.txt index b7721db28..bcfd801d9 100644 --- a/test/plugins/convertTransform.01.svg.txt +++ b/test/plugins/convertTransform.01.svg.txt @@ -1,43 +1,43 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/test/plugins/convertTransform.02.svg.txt b/test/plugins/convertTransform.02.svg.txt index 2b3a35bf3..02081c6f8 100644 --- a/test/plugins/convertTransform.02.svg.txt +++ b/test/plugins/convertTransform.02.svg.txt @@ -17,7 +17,7 @@ - + diff --git a/test/plugins/convertTransform.03.svg.txt b/test/plugins/convertTransform.03.svg.txt index 4e4a547fb..65065f79c 100644 --- a/test/plugins/convertTransform.03.svg.txt +++ b/test/plugins/convertTransform.03.svg.txt @@ -17,5 +17,5 @@ - + diff --git a/test/plugins/convertTransform.05.svg.txt b/test/plugins/convertTransform.05.svg.txt index c3a82a83e..ac2cdde8b 100644 --- a/test/plugins/convertTransform.05.svg.txt +++ b/test/plugins/convertTransform.05.svg.txt @@ -9,5 +9,5 @@ Correctly optimize transform with same sign non-zero shears and. @@@ - + diff --git a/test/plugins/convertTransform.06.svg.txt b/test/plugins/convertTransform.06.svg.txt new file mode 100644 index 000000000..9df0d1dbd --- /dev/null +++ b/test/plugins/convertTransform.06.svg.txt @@ -0,0 +1,23 @@ +Test matrices which are identities after rounding. + +=== + + + uwu + uwu + uwu + uwu + + +@@@ + + + uwu + uwu + uwu + uwu + + +@@@ + +{"degPrecision":1} \ No newline at end of file diff --git a/test/plugins/convertTransform.07.svg.txt b/test/plugins/convertTransform.07.svg.txt new file mode 100644 index 000000000..73ffc7d71 --- /dev/null +++ b/test/plugins/convertTransform.07.svg.txt @@ -0,0 +1,17 @@ +Test with skewX and sx != sy + +=== + + + uwu + + +@@@ + + + uwu + + +@@@ + +{"degPrecision":3} \ No newline at end of file diff --git a/test/plugins/convertTransform.10.svg.txt b/test/plugins/convertTransform.10.svg.txt new file mode 100644 index 000000000..2433d2b7c --- /dev/null +++ b/test/plugins/convertTransform.10.svg.txt @@ -0,0 +1,16 @@ +Make sure translate(n,n) and translate(n) work. + +=== + + + + + + + +@@@ + + + + + diff --git a/test/plugins/convertTransform.11.svg.txt b/test/plugins/convertTransform.11.svg.txt new file mode 100644 index 000000000..a3af5cade --- /dev/null +++ b/test/plugins/convertTransform.11.svg.txt @@ -0,0 +1,15 @@ +Test with 180 degree rotation, translation, and no scaling in matrix. Matrix not changed, +since it is shorter than translate(5,7)scale(-1). + +=== + + + + + + +@@@ + + + + diff --git a/test/plugins/convertTransform.12.svg.txt b/test/plugins/convertTransform.12.svg.txt new file mode 100644 index 000000000..9225268dc --- /dev/null +++ b/test/plugins/convertTransform.12.svg.txt @@ -0,0 +1,14 @@ +Test with 180 degree rotation and no scaling in matrix. + +=== + + + + + + +@@@ + + + + diff --git a/test/plugins/convertTransform.13.svg.txt b/test/plugins/convertTransform.13.svg.txt new file mode 100644 index 000000000..b2dce26cb --- /dev/null +++ b/test/plugins/convertTransform.13.svg.txt @@ -0,0 +1,16 @@ +Test rotate()scale(), rotate()skewX() when starting with matrix. + +=== + + + + + + + +@@@ + + + + + \ No newline at end of file diff --git a/test/plugins/convertTransform.14.svg.txt b/test/plugins/convertTransform.14.svg.txt new file mode 100644 index 000000000..3932bba5d --- /dev/null +++ b/test/plugins/convertTransform.14.svg.txt @@ -0,0 +1,22 @@ +Test to make sure rotate(180) inverts scale(1). + +=== + + + + + + +@@@ + + + + + +@@@ + +{ + "degPrecision":4, + "floatPrecision":6, + "transformPrecision":8 +} \ No newline at end of file