'use strict'; exports.type = 'perItem'; exports.active = true; exports.description = 'optimizes path data: writes in shorter form, applies transformations'; exports.params = { applyTransforms: true, applyTransformsStroked: true, straightCurves: true, lineShorthands: true, curveSmoothShorthands: true, floatPrecision: 3, transformPrecision: 5, removeUseless: true, collapseRepeated: true, utilizeAbsolute: true, leadingZero: true, negativeExtraSpace: true }; var pathElems = require('./_collections.js').pathElems, path2js = require('./_path.js').path2js, js2path = require('./_path.js').js2path, applyTransforms = require('./_path.js').applyTransforms, cleanupOutData = require('../lib/svgo/tools').cleanupOutData, precision, error, hasMarkerMid; /** * Convert absolute Path to relative, * collapse repeated instructions, * detect and convert Lineto shorthands, * remove useless instructions like "l0,0", * trim useless delimiters and leading zeros, * decrease accuracy of floating-point numbers. * * @see http://www.w3.org/TR/SVG/paths.html#PathData * * @param {Object} item current iteration item * @param {Object} params plugin params * @return {Boolean} if false, item will be filtered out * * @author Kir Belevich */ exports.fn = function(item, params) { if (item.isElem(pathElems) && item.hasAttr('d')) { precision = params.floatPrecision; error = precision !== false ? +Math.pow(.1, precision).toFixed(precision) : 1e-2; hasMarkerMid = item.hasAttr('marker-mid'); var data = path2js(item); // TODO: get rid of functions returns if (data.length) { convertToRelative(data); if (params.applyTransforms) { data = applyTransforms(item, data, params); } data = filters(data, params); if (params.utilizeAbsolute) { data = convertToMixed(data, params); } js2path(item, data, params); } } }; /** * Convert absolute path data coordinates to relative. * * @param {Array} path input path data * @param {Object} params plugin params * @return {Array} output path data */ function convertToRelative(path) { var point = [0, 0], subpathPoint = [0, 0], baseItem; path.forEach(function(item, index) { var instruction = item.instruction, data = item.data; // data !== !z if (data) { // already relative // recalculate current point if ('mcslqta'.indexOf(instruction) > -1) { point[0] += data[data.length - 2]; point[1] += data[data.length - 1]; if (instruction === 'm') { subpathPoint[0] = point[0]; subpathPoint[1] = point[1]; baseItem = item; } } else if (instruction === 'h') { point[0] += data[0]; } else if (instruction === 'v') { point[1] += data[0]; } // convert absolute path data coordinates to relative // if "M" was not transformed from "m" // M → m if (instruction === 'M') { if (index > 0) instruction = 'm'; data[0] -= point[0]; data[1] -= point[1]; subpathPoint[0] = point[0] += data[0]; subpathPoint[1] = point[1] += data[1]; baseItem = item; } // L → l // T → t else if ('LT'.indexOf(instruction) > -1) { instruction = instruction.toLowerCase(); // x y // 0 1 data[0] -= point[0]; data[1] -= point[1]; point[0] += data[0]; point[1] += data[1]; // C → c } else if (instruction === 'C') { instruction = 'c'; // x1 y1 x2 y2 x y // 0 1 2 3 4 5 data[0] -= point[0]; data[1] -= point[1]; data[2] -= point[0]; data[3] -= point[1]; data[4] -= point[0]; data[5] -= point[1]; point[0] += data[4]; point[1] += data[5]; // S → s // Q → q } else if ('SQ'.indexOf(instruction) > -1) { instruction = instruction.toLowerCase(); // x1 y1 x y // 0 1 2 3 data[0] -= point[0]; data[1] -= point[1]; data[2] -= point[0]; data[3] -= point[1]; point[0] += data[2]; point[1] += data[3]; // A → a } else if (instruction === 'A') { instruction = 'a'; // rx ry x-axis-rotation large-arc-flag sweep-flag x y // 0 1 2 3 4 5 6 data[5] -= point[0]; data[6] -= point[1]; point[0] += data[5]; point[1] += data[6]; // H → h } else if (instruction === 'H') { instruction = 'h'; data[0] -= point[0]; point[0] += data[0]; // V → v } else if (instruction === 'V') { instruction = 'v'; data[0] -= point[1]; point[1] += data[0]; } item.instruction = instruction; item.data = data; // store absolute coordinates for later use item.coords = point.slice(-2); } // !data === z, reset current point else if (instruction == 'z') { if (baseItem) { item.coords = baseItem.coords; } point[0] = subpathPoint[0]; point[1] = subpathPoint[1]; } item.base = index > 0 ? path[index - 1].coords : [0, 0]; }); return path; } /** * Main filters loop. * * @param {Array} path input path data * @param {Object} params plugin params * @return {Array} output path data */ function filters(path, params) { var relSubpoint = [0, 0], pathBase = [0, 0], prev = {}; path = path.filter(function(item, index) { var instruction = item.instruction, data = item.data; if (data) { var sdata; if (instruction === 's') { sdata = [0, 0].concat(data); if ('cs'.indexOf(prev.instruction) > -1) { var pdata = prev.data, n = pdata.length; // (-x, -y) of the prev tangent point relative to the current point sdata[0] = pdata[n - 2] - pdata[n - 4]; sdata[1] = pdata[n - 1] - pdata[n - 3]; } } // Rounding relative coordinates, taking in account accummulating error // to get closer to absolute coordinates. Sum of rounded value remains same: // l .25 3 .25 2 .25 3 .25 2 -> l .3 3 .2 2 .3 3 .2 2 if (precision !== false) { if ('mltqsc'.indexOf(instruction) > -1) { for (var i = data.length; i--;) { data[i] += item.base[i % 2] - relSubpoint[i % 2]; } } else if (instruction == 'h') { data[0] += item.base[0] - relSubpoint[0]; } else if (instruction == 'v') { data[0] += item.base[1] - relSubpoint[1]; } else if (instruction == 'a') { data[5] += item.base[0] - relSubpoint[0]; data[6] += item.base[1] - relSubpoint[1]; } roundData(data); if (instruction == 'h') relSubpoint[0] += data[0]; else if (instruction == 'v') relSubpoint[1] += data[0]; else { relSubpoint[0] += data[data.length - 2]; relSubpoint[1] += data[data.length - 1]; } roundData(relSubpoint); if (instruction.toLowerCase() == 'm') { pathBase[0] = relSubpoint[0]; pathBase[1] = relSubpoint[1]; } } // convert straight curves into lines segments if (params.straightCurves) { // c if ( instruction === 'c' && isCurveStraightLine( [ 0, data[0], data[2], data[4] ], [ 0, data[1], data[3], data[5] ] ) ) { instruction = 'l'; data = data.slice(-2); } // s else if ( instruction === 's' && isCurveStraightLine( [ 0, sdata[0], sdata[2], sdata[4] ], [ 0, sdata[1], sdata[3], sdata[5] ] ) ) { instruction = 'l'; data = data.slice(-2); } // q else if ( instruction === 'q' && isCurveStraightLine( [ 0, data[0], data[2] ], [ 0, data[1], data[3] ] ) ) { // save the original one for the future potential q + t conversion item.original = { instruction: instruction, data: data }; instruction = 'l'; data = data.slice(-2); } else if (instruction === 't') { // q (original) + t if ( prev.original && prev.original.instruction === 'q' ) { if (isCurveStraightLine( [ prev.original.data[0], prev.original.data[2], data[0] ], [ prev.original.data[1], prev.original.data[3], data[1] ] )) { instruction = 'l'; data = data.slice(-2); } else { prev.instruction = 'q'; prev.data = prev.original.data; } } // [^qt] + t else if ('qt'.indexOf(prev.instruction) < 0) { instruction = 'l'; data = data.slice(-2); } } // a else if ( instruction === 'a' && (data[0] === 0 || data[1] === 0) ) { instruction = 'l'; data = data.slice(-2); } } // horizontal and vertical line shorthands // l 50 0 → h 50 // l 0 50 → v 50 if ( params.lineShorthands && instruction === 'l' ) { if (data[1] === 0) { instruction = 'h'; data.pop(); } else if (data[0] === 0) { instruction = 'v'; data.shift(); } } // collapse repeated commands // h 20 h 30 -> h 50 if ( params.collapseRepeated && !hasMarkerMid && ('mhv'.indexOf(instruction) > -1) && prev.instruction && instruction == prev.instruction.toLowerCase() && ( (instruction != 'h' && instruction != 'v') || (prev.data[0] >= 0) == (item.data[0] >= 0) )) { prev.data[0] += data[0]; if (instruction != 'h' && instruction != 'v') { prev.data[1] += data[1]; } prev.coords = item.coords; if (prev.original) prev.original = null; path[index] = prev; return false; } // convert curves into smooth shorthands if (params.curveSmoothShorthands && prev.instruction) { // curveto if (instruction === 'c') { // c + c → c + s if ( prev.instruction === 'c' && data[0] === -(prev.data[2] - prev.data[4]) && data[1] === -(prev.data[3] - prev.data[5]) ) { instruction = 's'; data = data.slice(2); } // s + c → s + s else if ( prev.instruction === 's' && data[0] === -(prev.data[0] - prev.data[2]) && data[1] === -(prev.data[1] - prev.data[3]) ) { instruction = 's'; data = data.slice(2); } // [^cs] + c → [^cs] + s else if ( 'cs'.indexOf(prev.instruction) === -1 && data[0] === 0 && data[1] === 0 ) { instruction = 's'; data = data.slice(2); } } // quadratic Bézier curveto else if (instruction === 'q') { // q + q → q + t if ( prev.instruction === 'q' && data[0] === (prev.data[2] - prev.data[0]) && data[1] === (prev.data[3] - prev.data[1]) ) { instruction = 't'; data = data.slice(2); } // t + q → t + t else if ( prev.instruction === 't' && data[2] === prev.data[0] && data[3] === prev.data[1] ) { instruction = 't'; data = data.slice(2); } } } // remove useless non-first path segments if (params.removeUseless) { // l 0,0 / h 0 / v 0 / q 0,0 0,0 / t 0,0 / c 0,0 0,0 0,0 / s 0,0 0,0 if ( ( 'lhvqtcs'.indexOf(instruction) > -1 ) && data.every(function(i) { return i === 0; }) ) { path[index] = prev; return false; } // a 25,25 -30 0,1 0,0 if ( instruction === 'a' && data[5] === 0 && data[6] === 0 ) { path[index] = prev; return false; } } item.instruction = instruction; item.data = data; prev = item; } else { // z resets coordinates relSubpoint[0] = pathBase[0]; relSubpoint[1] = pathBase[1]; if (prev.instruction == 'z') return false; prev = item; } return true; }); return path; } /** * Writes data in shortest form using absolute or relative coordinates. * * @param {Array} data input path data * @return {Boolean} output */ function convertToMixed(path, params) { var prev = path[0]; path = path.filter(function(item, index) { if (index == 0) return true; if (!item.data) { prev = item; return true; } var instruction = item.instruction, data = item.data, adata = data && data.slice(0); if ('mltqsc'.indexOf(instruction) > -1) { for (var i = adata.length; i--;) { adata[i] += item.base[i % 2]; } } else if (instruction == 'h') { adata[0] += item.base[0]; } else if (instruction == 'v') { adata[0] += item.base[1]; } else if (instruction == 'a') { adata[5] += item.base[0]; adata[6] += item.base[1]; } roundData(adata); var absoluteDataStr = cleanupOutData(adata, params), relativeDataStr = cleanupOutData(data, params); // Convert to absolute coordinates if it's shorter. // v-20 -> V0 // Don't convert if it fits following previous instruction. // l20 30-10-50 instead of l20 30L20 30 if ( absoluteDataStr.length < relativeDataStr.length && !( params.negativeExtraSpace && instruction == prev.instruction && prev.instruction.charCodeAt(0) > 96 && absoluteDataStr.length == relativeDataStr.length - 1 && (data[0] < 0 || 0 < data[0] && data[0] < 1 && prev.data[prev.data.length - 1] % 1) ) ) { item.instruction = instruction.toUpperCase(); item.data = adata; } prev = item; return true; }); return path; } /** * Decrease accuracy of floating-point numbers * in path data keeping a specified number of decimals. * Smart rounds values like 2.349 to 2.35. * * @param {Array} data input data array * @param {Number} fixed number of decimals * @return {Array} output data array */ function roundData(data) { function round(data) { for (var i = data.length; i--;) { data[i] = +data[i].toFixed(precision); } return data; } function strongRound(data) { for (var i = data.length; i--;) { var rounded = +data[i].toFixed(precision - 1); data[i] = +Math.abs(rounded - data[i]).toFixed(precision) > error ? +data[i].toFixed(precision) : rounded; } return data; } roundData = precision > 0 ? strongRound : round; // jshint ignore: line return roundData(data); } /** * Checks if a curve is a straight line by measuring distance * from middle points to the line formed by end points. * * @param {Array} xs array of curve points x-coordinates * @param {Array} ys array of curve points y-coordinates * @return {Boolean} */ function isCurveStraightLine(xs, ys) { // Get line equation a·x + b·y + c = 0 coefficients a, b, c by start and end points. var i = xs.length - 1, a = ys[0] - ys[i], // y1 − y2 b = xs[i] - xs[0], // x2 − x1 c = xs[0] * ys[i] - xs[i] * ys[0], // x1·y2 − x2·y1 d = 1 / (a * a + b * b); // same part for all points if (!isFinite(d)) return false; // curve that ends at start point isn't the case // Distance from point (x0, y0) to the line is sqrt((c − a·x0 − b·y0)² / (a² + b²)) while (--i) { if (Math.sqrt(Math.pow(c - a * xs[i] - b * ys[i], 2) * d) > error) return false; } return true; }