UNPKG

10.9 kBJavaScriptView Raw
1'use strict';
2
3exports.type = 'perItem';
4
5exports.active = true;
6
7exports.description = 'collapses multiple transformations and optimizes it';
8
9exports.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
24var 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 */
46exports.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 */
76function 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 */
112function 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 */
154function 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 */
161function 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 */
172function 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 */
256function 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 */
298function 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
312function 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 */
340function 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 */
353function 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}