diff --git a/lib/path.js b/lib/path.js index 8fb4e3fe0..10f466b79 100644 --- a/lib/path.js +++ b/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 @@ -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 @@ -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; diff --git a/lib/svgo/tools.js b/lib/svgo/tools.js index f56e8d939..1fdd17250 100644 --- a/lib/svgo/tools.js +++ b/lib/svgo/tools.js @@ -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; +}; diff --git a/plugins/applyTransforms.js b/plugins/applyTransforms.js index 6b90acf50..0746d08dd 100644 --- a/plugins/applyTransforms.js +++ b/plugins/applyTransforms.js @@ -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 @@ -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) { diff --git a/plugins/cleanupListOfValues.js b/plugins/cleanupListOfValues.js index 5bf8fa0ee..a167b29be 100644 --- a/plugins/cleanupListOfValues.js +++ b/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'; @@ -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} */ @@ -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 diff --git a/plugins/cleanupNumericValues.js b/plugins/cleanupNumericValues.js index 03714097a..959afeff1 100644 --- a/plugins/cleanupNumericValues.js +++ b/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 = @@ -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(' '); } @@ -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} */ @@ -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