| 1 | 'use strict';
|
|---|
| 2 |
|
|---|
| 3 | exports.type = 'perItem';
|
|---|
| 4 |
|
|---|
| 5 | exports.active = true;
|
|---|
| 6 |
|
|---|
| 7 | exports.description = 'collapses multiple transformations and optimizes it';
|
|---|
| 8 |
|
|---|
| 9 | exports.params = {
|
|---|
| 10 | convertToShorts: true,
|
|---|
| 11 | // degPrecision: 3, // transformPrecision (or matrix precision) - 2 by default
|
|---|
| 12 | floatPrecision: 3,
|
|---|
| 13 | transformPrecision: 5,
|
|---|
| 14 | matrixToTransform: true,
|
|---|
| 15 | shortTranslate: true,
|
|---|
| 16 | shortScale: true,
|
|---|
| 17 | shortRotate: true,
|
|---|
| 18 | removeUseless: true,
|
|---|
| 19 | collapseIntoOne: true,
|
|---|
| 20 | leadingZero: true,
|
|---|
| 21 | negativeExtraSpace: false
|
|---|
| 22 | };
|
|---|
| 23 |
|
|---|
| 24 | var cleanupOutData = require('../lib/svgo/tools').cleanupOutData,
|
|---|
| 25 | transform2js = require('./_transforms.js').transform2js,
|
|---|
| 26 | transformsMultiply = require('./_transforms.js').transformsMultiply,
|
|---|
| 27 | matrixToTransform = require('./_transforms.js').matrixToTransform,
|
|---|
| 28 | degRound,
|
|---|
| 29 | floatRound,
|
|---|
| 30 | transformRound;
|
|---|
| 31 |
|
|---|
| 32 | /**
|
|---|
| 33 | * Convert matrices to the short aliases,
|
|---|
| 34 | * convert long translate, scale or rotate transform notations to the shorts ones,
|
|---|
| 35 | * convert transforms to the matrices and multiply them all into one,
|
|---|
| 36 | * remove useless transforms.
|
|---|
| 37 | *
|
|---|
| 38 | * @see http://www.w3.org/TR/SVG/coords.html#TransformMatrixDefined
|
|---|
| 39 | *
|
|---|
| 40 | * @param {Object} item current iteration item
|
|---|
| 41 | * @param {Object} params plugin params
|
|---|
| 42 | * @return {Boolean} if false, item will be filtered out
|
|---|
| 43 | *
|
|---|
| 44 | * @author Kir Belevich
|
|---|
| 45 | */
|
|---|
| 46 | exports.fn = function(item, params) {
|
|---|
| 47 |
|
|---|
| 48 | if (item.elem) {
|
|---|
| 49 |
|
|---|
| 50 | // transform
|
|---|
| 51 | if (item.hasAttr('transform')) {
|
|---|
| 52 | convertTransform(item, 'transform', params);
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | // gradientTransform
|
|---|
| 56 | if (item.hasAttr('gradientTransform')) {
|
|---|
| 57 | convertTransform(item, 'gradientTransform', params);
|
|---|
| 58 | }
|
|---|
| 59 |
|
|---|
| 60 | // patternTransform
|
|---|
| 61 | if (item.hasAttr('patternTransform')) {
|
|---|
| 62 | convertTransform(item, 'patternTransform', params);
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | };
|
|---|
| 68 |
|
|---|
| 69 | /**
|
|---|
| 70 | * Main function.
|
|---|
| 71 | *
|
|---|
| 72 | * @param {Object} item input item
|
|---|
| 73 | * @param {String} attrName attribute name
|
|---|
| 74 | * @param {Object} params plugin params
|
|---|
| 75 | */
|
|---|
| 76 | function convertTransform(item, attrName, params) {
|
|---|
| 77 | var data = transform2js(item.attr(attrName).value);
|
|---|
| 78 | params = definePrecision(data, params);
|
|---|
| 79 |
|
|---|
| 80 | if (params.collapseIntoOne && data.length > 1) {
|
|---|
| 81 | data = [transformsMultiply(data)];
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | if (params.convertToShorts) {
|
|---|
| 85 | data = convertToShorts(data, params);
|
|---|
| 86 | } else {
|
|---|
| 87 | data.forEach(roundTransform);
|
|---|
| 88 | }
|
|---|
| 89 |
|
|---|
| 90 | if (params.removeUseless) {
|
|---|
| 91 | data = removeUseless(data);
|
|---|
| 92 | }
|
|---|
| 93 |
|
|---|
| 94 | if (data.length) {
|
|---|
| 95 | item.attr(attrName).value = js2transform(data, params);
|
|---|
| 96 | } else {
|
|---|
| 97 | item.removeAttr(attrName);
|
|---|
| 98 | }
|
|---|
| 99 | }
|
|---|
| 100 |
|
|---|
| 101 | /**
|
|---|
| 102 | * Defines precision to work with certain parts.
|
|---|
| 103 | * transformPrecision - for scale and four first matrix parameters (needs a better precision due to multiplying),
|
|---|
| 104 | * floatPrecision - for translate including two last matrix and rotate parameters,
|
|---|
| 105 | * degPrecision - for rotate and skew. By default it's equal to (rougly)
|
|---|
| 106 | * transformPrecision - 2 or floatPrecision whichever is lower. Can be set in params.
|
|---|
| 107 | *
|
|---|
| 108 | * @param {Array} transforms input array
|
|---|
| 109 | * @param {Object} params plugin params
|
|---|
| 110 | * @return {Array} output array
|
|---|
| 111 | */
|
|---|
| 112 | function definePrecision(data, params) {
|
|---|
| 113 | /* jshint validthis: true */
|
|---|
| 114 | var matrixData = data.reduce(getMatrixData, []),
|
|---|
| 115 | significantDigits = params.transformPrecision;
|
|---|
| 116 |
|
|---|
| 117 | // Clone params so it don't affect other elements transformations.
|
|---|
| 118 | params = Object.assign({}, params);
|
|---|
| 119 |
|
|---|
| 120 | // Limit transform precision with matrix one. Calculating with larger precision doesn't add any value.
|
|---|
| 121 | if (matrixData.length) {
|
|---|
| 122 | params.transformPrecision = Math.min(params.transformPrecision,
|
|---|
| 123 | Math.max.apply(Math, matrixData.map(floatDigits)) || params.transformPrecision);
|
|---|
| 124 |
|
|---|
| 125 | significantDigits = Math.max.apply(Math, matrixData.map(function(n) {
|
|---|
| 126 | return String(n).replace(/\D+/g, '').length; // Number of digits in a number. 123.45 → 5
|
|---|
| 127 | }));
|
|---|
| 128 | }
|
|---|
| 129 | // No sense in angle precision more then number of significant digits in matrix.
|
|---|
| 130 | if (!('degPrecision' in params)) {
|
|---|
| 131 | params.degPrecision = Math.max(0, Math.min(params.floatPrecision, significantDigits - 2));
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | floatRound = params.floatPrecision >= 1 && params.floatPrecision < 20 ?
|
|---|
| 135 | smartRound.bind(this, params.floatPrecision) :
|
|---|
| 136 | round;
|
|---|
| 137 | degRound = params.degPrecision >= 1 && params.floatPrecision < 20 ?
|
|---|
| 138 | smartRound.bind(this, params.degPrecision) :
|
|---|
| 139 | round;
|
|---|
| 140 | transformRound = params.transformPrecision >= 1 && params.floatPrecision < 20 ?
|
|---|
| 141 | smartRound.bind(this, params.transformPrecision) :
|
|---|
| 142 | round;
|
|---|
| 143 |
|
|---|
| 144 | return params;
|
|---|
| 145 | }
|
|---|
| 146 |
|
|---|
| 147 | /**
|
|---|
| 148 | * Gathers four first matrix parameters.
|
|---|
| 149 | *
|
|---|
| 150 | * @param {Array} a array of data
|
|---|
| 151 | * @param {Object} transform
|
|---|
| 152 | * @return {Array} output array
|
|---|
| 153 | */
|
|---|
| 154 | function getMatrixData(a, b) {
|
|---|
| 155 | return b.name == 'matrix' ? a.concat(b.data.slice(0, 4)) : a;
|
|---|
| 156 | }
|
|---|
| 157 |
|
|---|
| 158 | /**
|
|---|
| 159 | * Returns number of digits after the point. 0.125 → 3
|
|---|
| 160 | */
|
|---|
| 161 | function floatDigits(n) {
|
|---|
| 162 | return (n = String(n)).slice(n.indexOf('.')).length - 1;
|
|---|
| 163 | }
|
|---|
| 164 |
|
|---|
| 165 | /**
|
|---|
| 166 | * Convert transforms to the shorthand alternatives.
|
|---|
| 167 | *
|
|---|
| 168 | * @param {Array} transforms input array
|
|---|
| 169 | * @param {Object} params plugin params
|
|---|
| 170 | * @return {Array} output array
|
|---|
| 171 | */
|
|---|
| 172 | function convertToShorts(transforms, params) {
|
|---|
| 173 |
|
|---|
| 174 | for(var i = 0; i < transforms.length; i++) {
|
|---|
| 175 |
|
|---|
| 176 | var transform = transforms[i];
|
|---|
| 177 |
|
|---|
| 178 | // convert matrix to the short aliases
|
|---|
| 179 | if (
|
|---|
| 180 | params.matrixToTransform &&
|
|---|
| 181 | transform.name === 'matrix'
|
|---|
| 182 | ) {
|
|---|
| 183 | var decomposed = matrixToTransform(transform, params);
|
|---|
| 184 | if (decomposed != transform &&
|
|---|
| 185 | js2transform(decomposed, params).length <= js2transform([transform], params).length) {
|
|---|
| 186 |
|
|---|
| 187 | transforms.splice.apply(transforms, [i, 1].concat(decomposed));
|
|---|
| 188 | }
|
|---|
| 189 | transform = transforms[i];
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | // fixed-point numbers
|
|---|
| 193 | // 12.754997 → 12.755
|
|---|
| 194 | roundTransform(transform);
|
|---|
| 195 |
|
|---|
| 196 | // convert long translate transform notation to the shorts one
|
|---|
| 197 | // translate(10 0) → translate(10)
|
|---|
| 198 | if (
|
|---|
| 199 | params.shortTranslate &&
|
|---|
| 200 | transform.name === 'translate' &&
|
|---|
| 201 | transform.data.length === 2 &&
|
|---|
| 202 | !transform.data[1]
|
|---|
| 203 | ) {
|
|---|
| 204 | transform.data.pop();
|
|---|
| 205 | }
|
|---|
| 206 |
|
|---|
| 207 | // convert long scale transform notation to the shorts one
|
|---|
| 208 | // scale(2 2) → scale(2)
|
|---|
| 209 | if (
|
|---|
| 210 | params.shortScale &&
|
|---|
| 211 | transform.name === 'scale' &&
|
|---|
| 212 | transform.data.length === 2 &&
|
|---|
| 213 | transform.data[0] === transform.data[1]
|
|---|
| 214 | ) {
|
|---|
| 215 | transform.data.pop();
|
|---|
| 216 | }
|
|---|
| 217 |
|
|---|
| 218 | // convert long rotate transform notation to the short one
|
|---|
| 219 | // translate(cx cy) rotate(a) translate(-cx -cy) → rotate(a cx cy)
|
|---|
| 220 | if (
|
|---|
| 221 | params.shortRotate &&
|
|---|
| 222 | transforms[i - 2] &&
|
|---|
| 223 | transforms[i - 2].name === 'translate' &&
|
|---|
| 224 | transforms[i - 1].name === 'rotate' &&
|
|---|
| 225 | transforms[i].name === 'translate' &&
|
|---|
| 226 | transforms[i - 2].data[0] === -transforms[i].data[0] &&
|
|---|
| 227 | transforms[i - 2].data[1] === -transforms[i].data[1]
|
|---|
| 228 | ) {
|
|---|
| 229 | transforms.splice(i - 2, 3, {
|
|---|
| 230 | name: 'rotate',
|
|---|
| 231 | data: [
|
|---|
| 232 | transforms[i - 1].data[0],
|
|---|
| 233 | transforms[i - 2].data[0],
|
|---|
| 234 | transforms[i - 2].data[1]
|
|---|
| 235 | ]
|
|---|
| 236 | });
|
|---|
| 237 |
|
|---|
| 238 | // splice compensation
|
|---|
| 239 | i -= 2;
|
|---|
| 240 |
|
|---|
| 241 | transform = transforms[i];
|
|---|
| 242 | }
|
|---|
| 243 |
|
|---|
| 244 | }
|
|---|
| 245 |
|
|---|
| 246 | return transforms;
|
|---|
| 247 |
|
|---|
| 248 | }
|
|---|
| 249 |
|
|---|
| 250 | /**
|
|---|
| 251 | * Remove useless transforms.
|
|---|
| 252 | *
|
|---|
| 253 | * @param {Array} transforms input array
|
|---|
| 254 | * @return {Array} output array
|
|---|
| 255 | */
|
|---|
| 256 | function removeUseless(transforms) {
|
|---|
| 257 |
|
|---|
| 258 | return transforms.filter(function(transform) {
|
|---|
| 259 |
|
|---|
| 260 | // translate(0), rotate(0[, cx, cy]), skewX(0), skewY(0)
|
|---|
| 261 | if (
|
|---|
| 262 | ['translate', 'rotate', 'skewX', 'skewY'].indexOf(transform.name) > -1 &&
|
|---|
| 263 | (transform.data.length == 1 || transform.name == 'rotate') &&
|
|---|
| 264 | !transform.data[0] ||
|
|---|
| 265 |
|
|---|
| 266 | // translate(0, 0)
|
|---|
| 267 | transform.name == 'translate' &&
|
|---|
| 268 | !transform.data[0] &&
|
|---|
| 269 | !transform.data[1] ||
|
|---|
| 270 |
|
|---|
| 271 | // scale(1)
|
|---|
| 272 | transform.name == 'scale' &&
|
|---|
| 273 | transform.data[0] == 1 &&
|
|---|
| 274 | (transform.data.length < 2 || transform.data[1] == 1) ||
|
|---|
| 275 |
|
|---|
| 276 | // matrix(1 0 0 1 0 0)
|
|---|
| 277 | transform.name == 'matrix' &&
|
|---|
| 278 | transform.data[0] == 1 &&
|
|---|
| 279 | transform.data[3] == 1 &&
|
|---|
| 280 | !(transform.data[1] || transform.data[2] || transform.data[4] || transform.data[5])
|
|---|
| 281 | ) {
|
|---|
| 282 | return false;
|
|---|
| 283 | }
|
|---|
| 284 |
|
|---|
| 285 | return true;
|
|---|
| 286 |
|
|---|
| 287 | });
|
|---|
| 288 |
|
|---|
| 289 | }
|
|---|
| 290 |
|
|---|
| 291 | /**
|
|---|
| 292 | * Convert transforms JS representation to string.
|
|---|
| 293 | *
|
|---|
| 294 | * @param {Array} transformJS JS representation array
|
|---|
| 295 | * @param {Object} params plugin params
|
|---|
| 296 | * @return {String} output string
|
|---|
| 297 | */
|
|---|
| 298 | function js2transform(transformJS, params) {
|
|---|
| 299 |
|
|---|
| 300 | var transformString = '';
|
|---|
| 301 |
|
|---|
| 302 | // collect output value string
|
|---|
| 303 | transformJS.forEach(function(transform) {
|
|---|
| 304 | roundTransform(transform);
|
|---|
| 305 | transformString += (transformString && ' ') + transform.name + '(' + cleanupOutData(transform.data, params) + ')';
|
|---|
| 306 | });
|
|---|
| 307 |
|
|---|
| 308 | return transformString;
|
|---|
| 309 |
|
|---|
| 310 | }
|
|---|
| 311 |
|
|---|
| 312 | function roundTransform(transform) {
|
|---|
| 313 | switch (transform.name) {
|
|---|
| 314 | case 'translate':
|
|---|
| 315 | transform.data = floatRound(transform.data);
|
|---|
| 316 | break;
|
|---|
| 317 | case 'rotate':
|
|---|
| 318 | transform.data = degRound(transform.data.slice(0, 1)).concat(floatRound(transform.data.slice(1)));
|
|---|
| 319 | break;
|
|---|
| 320 | case 'skewX':
|
|---|
| 321 | case 'skewY':
|
|---|
| 322 | transform.data = degRound(transform.data);
|
|---|
| 323 | break;
|
|---|
| 324 | case 'scale':
|
|---|
| 325 | transform.data = transformRound(transform.data);
|
|---|
| 326 | break;
|
|---|
| 327 | case 'matrix':
|
|---|
| 328 | transform.data = transformRound(transform.data.slice(0, 4)).concat(floatRound(transform.data.slice(4)));
|
|---|
| 329 | break;
|
|---|
| 330 | }
|
|---|
| 331 | return transform;
|
|---|
| 332 | }
|
|---|
| 333 |
|
|---|
| 334 | /**
|
|---|
| 335 | * Rounds numbers in array.
|
|---|
| 336 | *
|
|---|
| 337 | * @param {Array} data input data array
|
|---|
| 338 | * @return {Array} output data array
|
|---|
| 339 | */
|
|---|
| 340 | function round(data) {
|
|---|
| 341 | return data.map(Math.round);
|
|---|
| 342 | }
|
|---|
| 343 |
|
|---|
| 344 | /**
|
|---|
| 345 | * Decrease accuracy of floating-point numbers
|
|---|
| 346 | * in transforms keeping a specified number of decimals.
|
|---|
| 347 | * Smart rounds values like 2.349 to 2.35.
|
|---|
| 348 | *
|
|---|
| 349 | * @param {Number} fixed number of decimals
|
|---|
| 350 | * @param {Array} data input data array
|
|---|
| 351 | * @return {Array} output data array
|
|---|
| 352 | */
|
|---|
| 353 | function smartRound(precision, data) {
|
|---|
| 354 | for (var i = data.length, tolerance = +Math.pow(.1, precision).toFixed(precision); i--;) {
|
|---|
| 355 | if (data[i].toFixed(precision) != data[i]) {
|
|---|
| 356 | var rounded = +data[i].toFixed(precision - 1);
|
|---|
| 357 | data[i] = +Math.abs(rounded - data[i]).toFixed(precision + 1) >= tolerance ?
|
|---|
| 358 | +data[i].toFixed(precision) :
|
|---|
| 359 | rounded;
|
|---|
| 360 | }
|
|---|
| 361 | }
|
|---|
| 362 | return data;
|
|---|
| 363 | }
|
|---|