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