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.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