diff --git a/lib/path.js b/lib/path.js index 9b79550b0..45934329a 100644 --- a/lib/path.js +++ b/lib/path.js @@ -208,3 +208,81 @@ const parsePathData = (string) => { return pathData; }; exports.parsePathData = parsePathData; + +const stringifyNumber = ({ number, precision }) => { + let result; + if (precision == null) { + result = number.toString(); + } else { + result = number.toFixed(precision); + if (result.includes('.')) { + result = result.replace(/\.?0+$/, '') + } + } + // remove zero whole from decimal number + if (result !== '0') { + result = result.replace(/^0/, '').replace(/^-0/, '-'); + } + return result; +}; + +// elliptical arc large-arc and sweep flags are rendered with spaces +// because many non-browser environments are not able to parse such paths +const stringifyArgs = ({ args, precision }) => { + let result = ''; + let prev; + for (let i = 0; i < args.length; i += 1) { + const number = args[i]; + const numberString = stringifyNumber({ number, precision }); + // avoid space before first and negative numbers + if (i === 0 || numberString.startsWith('-')) { + result += numberString; + } else if (prev.includes('.') && numberString.startsWith('.')) { + result += numberString; + } else { + result += ` ${numberString}`; + } + prev = numberString; + } + return result; +}; + +const stringifyPathData = ({ pathData, precision }) => { + // combine sequence of the same commands + let combined = []; + for (let i = 0; i < pathData.length; i += 1) { + const { command, args } = pathData[i]; + if (i === 0) { + combined.push({ command, args }); + } else { + const last = combined[combined.length - 1]; + // match leading moveto with following lineto + if (i === 1) { + if (command === 'L') { + last.command = 'M'; + } + if (command === 'l') { + last.command = 'm'; + } + } + if ( + (last.command === command && + last.command !== 'M' && + last.command !== 'm') || + // combine matching moveto and lineto sequences + (last.command === 'M' && command === 'L') || + (last.command === 'm' && command === 'l') + ) { + last.args = [...last.args, ...args]; + } else { + combined.push({ command, args }); + } + } + } + let result = ''; + for (const { command, args } of combined) { + result += command + stringifyArgs({ args, precision }); + } + return result; +}; +exports.stringifyPathData = stringifyPathData; diff --git a/lib/path.test.js b/lib/path.test.js index bcb1dbd48..a677f8dca 100644 --- a/lib/path.test.js +++ b/lib/path.test.js @@ -1,7 +1,7 @@ 'use strict'; const { expect } = require('chai'); -const { parsePathData } = require('./path.js'); +const { parsePathData, stringifyPathData } = require('./path.js'); describe('parse path data', () => { it('should allow spaces between commands', () => { @@ -74,3 +74,88 @@ describe('parse path data', () => { ]); }); }); + +describe('stringify path data', () => { + it('should combine sequence of the same commands', () => { + expect( + stringifyPathData({ + pathData: [ + { command: 'M', args: [0, 0] }, + { command: 'h', args: [10] }, + { command: 'h', args: [20] }, + { command: 'h', args: [30] }, + { command: 'H', args: [40] }, + { command: 'H', args: [50] }, + ], + }) + ).to.equal('M0 0h10 20 30H40 50'); + }); + it('should not combine sequence of moveto', () => { + expect( + stringifyPathData({ + pathData: [ + { command: 'M', args: [0, 0] }, + { command: 'M', args: [10, 10] }, + { command: 'm', args: [20, 30] }, + { command: 'm', args: [40, 50] }, + ], + }) + ).to.equal('M0 0M10 10m20 30m40 50'); + }); + it('should combine moveto and sequence of lineto', () => { + expect( + stringifyPathData({ + pathData: [ + { command: 'M', args: [0, 0] }, + { command: 'l', args: [10, 10] }, + { command: 'M', args: [0, 0] }, + { command: 'l', args: [10, 10] }, + { command: 'M', args: [0, 0] }, + { command: 'L', args: [10, 10] }, + ], + }) + ).to.equal('m0 0 10 10M0 0l10 10M0 0 10 10'); + expect( + stringifyPathData({ + pathData: [ + { command: 'm', args: [0, 0] }, + { command: 'L', args: [10, 10] }, + ], + }) + ).to.equal('M0 0 10 10'); + }); + it('should avoid space before first, negative and decimals', () => { + expect( + stringifyPathData({ + pathData: [ + { command: 'M', args: [0, -1.2] }, + { command: 'L', args: [0.3, 4] }, + { command: 'L', args: [5, -0.6] }, + { command: 'L', args: [7, 0.8] }, + ], + }) + ).to.equal('M0-1.2.3 4 5-.6 7 .8'); + }); + it('should configure precision', () => { + expect( + stringifyPathData({ + pathData: [ + { command: 'M', args: [0, -1.9876] }, + { command: 'L', args: [0.3, 3.14159265] }, + { command: 'L', args: [100, 200] }, + ], + precision: 3, + }) + ).to.equal('M0-1.988.3 3.142 100 200'); + expect( + stringifyPathData({ + pathData: [ + { command: 'M', args: [0, -1.9876] }, + { command: 'L', args: [0.3, 3.14159265] }, + { command: 'L', args: [100, 200] }, + ], + precision: 0, + }) + ).to.equal('M0-2 0 3 100 200'); + }); +}); diff --git a/plugins/_path.js b/plugins/_path.js index 00ecf5a4e..0dc6912ec 100644 --- a/plugins/_path.js +++ b/plugins/_path.js @@ -1,6 +1,6 @@ 'use strict'; -const { parsePathData } = require('../lib/path.js'); +const { parsePathData, stringifyPathData } = require('../lib/path.js'); var regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g, transform2js = require('./_transforms').transform2js, @@ -9,7 +9,6 @@ var regNumericValues = /[-+]?(\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g, collections = require('./_collections.js'), referencesProps = collections.referencesProps, defaultStrokeWidth = collections.attrsGroupsDefaults.presentation['stroke-width'], - cleanupOutData = require('../lib/svgo/tools').cleanupOutData, removeLeadingZero = require('../lib/svgo/tools').removeLeadingZero, prevCtrlPoint; @@ -500,64 +499,32 @@ function computeQuadraticFirstDerivativeRoot(a, b, c) { */ exports.js2path = function(path, data, params) { - path.pathJS = data; - - if (params.collapseRepeated) { - data = collapseRepeated(data); + path.pathJS = data; + + const pathData = []; + for (const item of data) { + // remove moveto commands which are followed by moveto commands + if ( + pathData.length !== 0 && + (item.instruction === 'M' || item.instruction === 'm') + ) { + const last = pathData[pathData.length - 1]; + if (last.command === 'M' || last.command === 'm') { + pathData.pop(); + } } + pathData.push({ + command: item.instruction, + args: item.data || [], + }); + } - path.attr('d').value = data.reduce(function(pathString, item) { - var strData = ''; - if (item.data) { - strData = cleanupOutData(item.data, params, item.instruction); - } - return pathString += item.instruction + strData; - }, ''); - + path.attr('d').value = stringifyPathData({ + pathData, + precision: params.floatPrecision, + }); }; -/** - * Collapse repeated instructions data - * - * @param {Array} path input path data - * @return {Array} output path data - */ -function collapseRepeated(data) { - - var prev, - prevIndex; - - // copy an array and modifieds item to keep original data untouched - data = data.reduce(function(newPath, item) { - if ( - prev && item.data && - item.instruction == prev.instruction - ) { - // concat previous data with current - if (item.instruction != 'M') { - prev = newPath[prevIndex] = { - instruction: prev.instruction, - data: prev.data.concat(item.data), - coords: item.coords, - base: prev.base - }; - } else { - prev.data = item.data; - prev.coords = item.coords; - } - } else { - newPath.push(item); - prev = item; - prevIndex = newPath.length - 1; - } - - return newPath; - }, []); - - return data; - -} - function set(dest, source) { dest[0] = source[source.length - 2]; dest[1] = source[source.length - 1]; diff --git a/test/plugins/convertPathData.01.svg b/test/plugins/convertPathData.01.svg index ec3387152..43f0970dc 100644 --- a/test/plugins/convertPathData.01.svg +++ b/test/plugins/convertPathData.01.svg @@ -24,8 +24,8 @@ - - + + diff --git a/test/plugins/convertPathData.02.svg b/test/plugins/convertPathData.02.svg index c950860b0..6c1345a67 100644 --- a/test/plugins/convertPathData.02.svg +++ b/test/plugins/convertPathData.02.svg @@ -10,7 +10,7 @@ @@@ - + diff --git a/test/plugins/convertPathData.03.svg b/test/plugins/convertPathData.03.svg index 05cb5e7ad..1a574b4a4 100644 --- a/test/plugins/convertPathData.03.svg +++ b/test/plugins/convertPathData.03.svg @@ -23,9 +23,9 @@ - - - + + + diff --git a/test/plugins/convertPathData.04.svg b/test/plugins/convertPathData.04.svg index 0f0f9ad0e..6b5624477 100644 --- a/test/plugins/convertPathData.04.svg +++ b/test/plugins/convertPathData.04.svg @@ -11,7 +11,7 @@ @@@ - + diff --git a/test/plugins/convertPathData.05.svg b/test/plugins/convertPathData.05.svg index 36639c966..0f2dfa1d4 100644 --- a/test/plugins/convertPathData.05.svg +++ b/test/plugins/convertPathData.05.svg @@ -8,8 +8,8 @@ @@@ - - - - + + + + diff --git a/test/plugins/convertPathData.06.svg b/test/plugins/convertPathData.06.svg index d4612c25e..5ff4e0c6b 100644 --- a/test/plugins/convertPathData.06.svg +++ b/test/plugins/convertPathData.06.svg @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/test/plugins/convertPathData.08.svg b/test/plugins/convertPathData.08.svg index 74646fb67..b863a31c1 100644 --- a/test/plugins/convertPathData.08.svg +++ b/test/plugins/convertPathData.08.svg @@ -20,14 +20,14 @@ - + - - - + + + diff --git a/test/plugins/convertPathData.10.svg b/test/plugins/convertPathData.10.svg index d99c2c9f6..21f39bead 100644 --- a/test/plugins/convertPathData.10.svg +++ b/test/plugins/convertPathData.10.svg @@ -5,5 +5,5 @@ @@@ - + diff --git a/test/plugins/convertPathData.11.svg b/test/plugins/convertPathData.11.svg index 965c2840d..c6cd39abb 100644 --- a/test/plugins/convertPathData.11.svg +++ b/test/plugins/convertPathData.11.svg @@ -36,36 +36,36 @@ @@@ - - - - - - - - + + + + + + + + - + - + - - + + - + - - + + - + - + diff --git a/test/plugins/convertPathData.12.svg b/test/plugins/convertPathData.12.svg index 21801dc8a..fb497b2e1 100644 --- a/test/plugins/convertPathData.12.svg +++ b/test/plugins/convertPathData.12.svg @@ -28,7 +28,7 @@ - + diff --git a/test/plugins/convertPathData.17.svg b/test/plugins/convertPathData.17.svg index dc481b862..3e4a87420 100644 --- a/test/plugins/convertPathData.17.svg +++ b/test/plugins/convertPathData.17.svg @@ -5,7 +5,7 @@ @@@ - + @@@ diff --git a/test/plugins/mergePaths.03.svg b/test/plugins/mergePaths.03.svg index ee0613a2f..3900c3488 100644 --- a/test/plugins/mergePaths.03.svg +++ b/test/plugins/mergePaths.03.svg @@ -27,7 +27,7 @@ - + @@ -39,6 +39,6 @@ - +