Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: tools/toFixed precision #1950

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
65 changes: 28 additions & 37 deletions lib/path.js
@@ -1,4 +1,4 @@
import { removeLeadingZero, toFixed } from './svgo/tools.js';
import { removeLeadingZero, toFixedStr } from './svgo/tools.js';

/**
* @typedef {import('./types.js').PathDataItem} PathDataItem
Expand Down Expand Up @@ -239,23 +239,6 @@ export const parsePathData = (string) => {
return pathData;
};

/**
* @type {(number: number, precision?: number) => {
* roundedStr: string,
* rounded: number
* }}
*/
const roundAndStringify = (number, precision) => {
if (precision != null) {
number = toFixed(number, precision);
}

return {
roundedStr: removeLeadingZero(number),
rounded: number,
};
};

/**
* Elliptical arc large-arc and sweep flags are rendered with spaces
* because many non-browser environments are not able to parse such paths
Expand All @@ -272,30 +255,38 @@ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => {
let previous;

for (let i = 0; i < args.length; i++) {
const { roundedStr, rounded } = roundAndStringify(args[i], precision);
if (
let number = args[i];
if (precision != null) {
number = toFixedStr(number.toString(), precision);
}
const roundedStr = removeLeadingZero(number);

const skipSpace = (
// avoid space before first
i === 0
) || (
// avoid space before negative numbers
number < 0
) || (
// consider combined arcs
disableSpaceAfterFlags &&
(command === 'A' || command === 'a') &&
// consider combined arcs
(i % 7 === 4 || i % 7 === 5)
) {
result += roundedStr;
} else if (i === 0 || rounded < 0) {
// avoid space before first and negative numbers
result += roundedStr;
} else if (
!Number.isInteger(previous) &&
rounded != 0 &&
rounded < 1 &&
rounded > -1
) {
) || (
// remove space before decimal with zero whole
// only when previous number is also decimal
result += roundedStr;
} else {
result += ` ${roundedStr}`;
previous % 1 && // only when previous number is also decimal
number != 0 &&
number < 1 &&
number > -1
);

if (!skipSpace) {
result += ' ';
}
previous = rounded;

result += roundedStr;

previous = number;
}

return result;
Expand Down
23 changes: 22 additions & 1 deletion lib/svgo/tools.js
Expand Up @@ -226,13 +226,34 @@ export const findReferences = (attribute, value) => {

/**
* Does the same as {@link Number.toFixed} but without casting
* the return value to a string.
* the return value to a string and correctly fix numbers
* like 21.0565 to 21.057 when precision is 3.
*
* @param {number} num
* @param {number} precision
* @returns {number}
*/
export const toFixed = (num, precision) => {
if (precision > 17 || num % 1 == 0) return num;

const pow = 10 ** precision;
return Math.round(num * pow) / pow;
};

/**
* Does the same as {@link Number.toFixed} and correctly
* fix numbers like 2.5845 to 2.585 when precision is 3.
*
* @param {string} numStr
* @param {number} precision
* @returns {string}
*/
export const toFixedStr = (numStr, precision) => {
const pow = 10 ** precision;
const fixed = Math.round(numStr * pow) / pow;
const result = fixed.toString();

// prevent returning more digits than originally given
const isRegression = result.length > numStr.length;
return isRegression ? numStr : result;
};
9 changes: 3 additions & 6 deletions plugins/applyTransforms.js
Expand Up @@ -12,7 +12,7 @@ import {
import { referencesProps, attrsGroupsDefaults } from './_collections.js';
import { collectStylesheet, computeStyle } from '../lib/style.js';

import { removeLeadingZero, includesUrlReference } from '../lib/svgo/tools.js';
import { removeLeadingZero, includesUrlReference, toFixed } from '../lib/svgo/tools.js';

/**
* @typedef {PathDataItem[]} PathData
Expand Down Expand Up @@ -92,11 +92,8 @@ export const applyTransforms = (root, params) => {
return;
}

const scale = Number(
Math.hypot(matrix.data[0], matrix.data[1]).toFixed(
transformPrecision,
),
);
const scale = toFixed(Math.hypot(matrix.data[0], matrix.data[1]),
transformPrecision);

if (stroke && stroke != 'none') {
if (!params.applyTransformsStroked) {
Expand Down
20 changes: 8 additions & 12 deletions plugins/cleanupListOfValues.js
@@ -1,4 +1,4 @@
import { removeLeadingZero } from '../lib/svgo/tools.js';
import { removeLeadingZero, toFixedStr } from '../lib/svgo/tools.js';

export const name = 'cleanupListOfValues';
export const description = 'rounds list of values to the fixed precision';
Expand Down Expand Up @@ -52,8 +52,7 @@ export const fn = (_root, params) => {

// if attribute value matches regNumericValues
if (match) {
// round it to the fixed precision
let num = Number(Number(match[1]).toFixed(floatPrecision));
let strNum = match[1];
/**
* @type {any}
*/
Expand All @@ -65,22 +64,19 @@ export const fn = (_root, params) => {

// convert absolute values to pixels
if (convertToPx && units && units in absoluteLengths) {
const pxNum = Number(
(absoluteLengths[units] * Number(match[1])).toFixed(floatPrecision),
);
const pxNum = (absoluteLengths[units] * strNum).toString();

if (pxNum.toString().length < match[0].length) {
num = pxNum;
if (pxNum.length < match[0].length) {
strNum = pxNum;
units = 'px';
}
}

// round it to the fixed precision
let str = toFixedStr(strNum, floatPrecision);
// and remove leading zero
let str;
if (leadingZero) {
str = removeLeadingZero(num);
} else {
str = num.toString();
str = removeLeadingZero(str);
}

// remove default 'px' units
Expand Down
23 changes: 9 additions & 14 deletions plugins/cleanupNumericValues.js
@@ -1,4 +1,4 @@
import { removeLeadingZero } from '../lib/svgo/tools.js';
import { removeLeadingZero, toFixedStr } from '../lib/svgo/tools.js';

export const name = 'cleanupNumericValues';
export const description =
Expand Down Expand Up @@ -43,7 +43,7 @@ export const fn = (_root, params) => {
const num = Number(value);
return Number.isNaN(num)
? value
: Number(num.toFixed(floatPrecision));
: toFixedStr(value, floatPrecision);
})
.join(' ');
}
Expand All @@ -59,7 +59,7 @@ export const fn = (_root, params) => {
// if attribute value matches regNumericValues
if (match) {
// round it to the fixed precision
let num = Number(Number(match[1]).toFixed(floatPrecision));
let strNum = match[1];
/**
* @type {any}
*/
Expand All @@ -71,23 +71,18 @@ export const fn = (_root, params) => {

// convert absolute values to pixels
if (convertToPx && units !== '' && units in absoluteLengths) {
const pxNum = Number(
(absoluteLengths[units] * Number(match[1])).toFixed(
floatPrecision,
),
);
if (pxNum.toString().length < match[0].length) {
num = pxNum;
const pxNum = (absoluteLengths[units] * strNum).toString();
if (pxNum.length < match[0].length) {
strNum = pxNum;
units = 'px';
}
}

// round it to the fixed precision
let str = toFixedStr(strNum, floatPrecision);
// and remove leading zero
let str;
if (leadingZero) {
str = removeLeadingZero(num);
} else {
str = num.toString();
str = removeLeadingZero(str);
}

// remove default 'px' units
Expand Down