UNPKG

15.7 kBJavaScriptView Raw
1/* eslint-disable complexity,prefer-reflect,max-len,newline-after-var */
2/* eslint no-multi-str:0 */
3
4'use strict';
5
6const { ucs2 } = require('punycode');
7const { Transform } = require('stream');
8const Sax = require('sax');
9const { SVGPathData } = require('svg-pathdata');
10const svgShapesToPath = require('./svgshapes2svgpath');
11const { Matrix } = require('transformation-matrix-js');
12
13require('string.prototype.codepointat');
14
15// Transform helpers (will move elsewhere later)
16function parseTransforms(value) {
17 return value
18 .match(/(rotate|translate|scale|skewX|skewY|matrix)\s*\(([^)]*)\)\s*/g)
19 .map(transform => transform.match(/[\w.-]+/g));
20}
21function matrixFromTransformAttribute(transformAttributeString) {
22 const transformations = {
23 matrix: (result, ...args) => result.transform(...args),
24 translate: (result, x, y = 0) => result.translate(x, y),
25 scale: (result, x, y = x) => result.scale(x, y),
26 rotate: (result, a, x = 0, y = 0) => {
27 if (0 === x && 0 === y) {
28 result.rotateDeg(a);
29 } else {
30 result
31 .translate(x, y)
32 .rotateDeg(a)
33 .translate(-x, -y);
34 }
35 },
36 skewX: (result, a) => result.skewX(a * Math.PI / 180),
37 skewY: (result, a) => result.skewY(a * Math.PI / 180),
38 };
39
40 const result = new Matrix();
41 for (const transform of parseTransforms(transformAttributeString)) {
42 transformations[transform[0]](
43 result,
44 ...transform.slice(1).map(parseFloat)
45 );
46 }
47 return result;
48}
49
50// Rendering
51function tagShouldRender(curTag, parents) {
52 let values;
53
54 return !parents.some(tag => {
55 if (
56 'undefined' !== typeof tag.attributes.display &&
57 'none' === tag.attributes.display.toLowerCase()
58 ) {
59 return true;
60 }
61 if (
62 'undefined' !== typeof tag.attributes.width &&
63 0 === parseFloat(tag.attributes.width)
64 ) {
65 return true;
66 }
67 if (
68 'undefined' !== typeof tag.attributes.height &&
69 0 === parseFloat(tag.attributes.height)
70 ) {
71 return true;
72 }
73 if ('undefined' !== typeof tag.attributes.viewBox) {
74 values = tag.attributes.viewBox.split(/\s*,*\s|\s,*\s*|,/);
75 if (0 === parseFloat(values[2]) || 0 === parseFloat(values[3])) {
76 return true;
77 }
78 }
79 return false;
80 });
81}
82
83// According to the document (http://www.w3.org/TR/SVG/painting.html#FillProperties)
84// fill <paint> none|currentColor|inherit|<color>
85// [<icccolor>]|<funciri> (not support yet)
86function getTagColor(currTag, parents) {
87 const defaultColor = 'black';
88 const fillVal = currTag.attributes.fill;
89 let color;
90 const parentsLength = parents.length;
91
92 if ('none' === fillVal) {
93 return color;
94 }
95 if ('currentColor' === fillVal) {
96 return defaultColor;
97 }
98 if ('inherit' === fillVal) {
99 if (0 === parentsLength) {
100 return defaultColor;
101 }
102 return getTagColor(
103 parents[parentsLength - 1],
104 parents.slice(0, parentsLength - 1)
105 );
106 // this might be null.
107 // For example: <svg ><path fill="inherit" /> </svg>
108 // in this case getTagColor should return null
109 // recursive call, the bottom element should be svg,
110 // and svg didn't fill color, so just return null
111 }
112
113 return fillVal;
114}
115
116class SVGIcons2SVGFontStream extends Transform {
117 constructor(options) {
118 super({ objectMode: true });
119
120 // Setting objectMode separately
121 this._writableState.objectMode = true;
122 this._readableState.objectMode = false;
123
124 this.glyphs = [];
125
126 this._options = options || {};
127 this._options.fontName = this._options.fontName || 'iconfont';
128 this._options.fontId = this._options.fontId || this._options.fontName;
129 this._options.fixedWidth = this._options.fixedWidth || false;
130 this._options.descent = this._options.descent || 0;
131 this._options.round = this._options.round || 10e12;
132 this._options.metadata = this._options.metadata || '';
133
134 this.log = this._options.log || console.log.bind(console); // eslint-disable-line
135 }
136
137 _transform(svgIconStream, _unused, svgIconStreamCallback) {
138 // Parsing each icons asynchronously
139 const saxStream = Sax.createStream(true);
140 const parents = [];
141 const transformStack = [new Matrix()];
142 function applyTransform(d) {
143 return new SVGPathData(d).matrix(
144 ...transformStack[transformStack.length - 1].toArray()
145 );
146 }
147 const glyph = svgIconStream.metadata || {};
148
149 // init width and height os they aren't undefined if <svg> isn't renderable
150 glyph.width = 0;
151 glyph.height = 1;
152
153 glyph.paths = [];
154 this.glyphs.push(glyph);
155
156 if ('string' !== typeof glyph.name) {
157 this.emit(
158 'error',
159 new Error(
160 `Please provide a name for the glyph at index ${this.glyphs.length -
161 1}`
162 )
163 );
164 }
165 if (
166 this.glyphs.some(
167 anotherGlyph =>
168 anotherGlyph !== glyph && anotherGlyph.name === glyph.name
169 )
170 ) {
171 this.emit(
172 'error',
173 new Error(`The glyph name "${glyph.name}" must be unique.`)
174 );
175 }
176 if (
177 glyph.unicode &&
178 glyph.unicode instanceof Array &&
179 glyph.unicode.length
180 ) {
181 if (
182 glyph.unicode.some((unicodeA, i) =>
183 glyph.unicode.some((unicodeB, j) => i !== j && unicodeA === unicodeB)
184 )
185 ) {
186 this.emit(
187 'error',
188 new Error(
189 `Given codepoints for the glyph "${glyph.name}" contain duplicates.`
190 )
191 );
192 }
193 } else if ('string' !== typeof glyph.unicode) {
194 this.emit(
195 'error',
196 new Error(`Please provide a codepoint for the glyph "${glyph.name}"`)
197 );
198 }
199
200 if (
201 this.glyphs.some(
202 anotherGlyph =>
203 anotherGlyph !== glyph && anotherGlyph.unicode === glyph.unicode
204 )
205 ) {
206 this.emit(
207 'error',
208 new Error(
209 `The glyph "${
210 glyph.name
211 }" codepoint seems to be used already elsewhere.`
212 )
213 );
214 }
215
216 saxStream.on('opentag', tag => {
217 let values;
218 let color;
219
220 parents.push(tag);
221 try {
222 const currentTransform = transformStack[transformStack.length - 1];
223
224 if ('undefined' !== typeof tag.attributes.transform) {
225 const transform = matrixFromTransformAttribute(
226 tag.attributes.transform
227 );
228 transformStack.push(currentTransform.clone().multiply(transform));
229 } else {
230 transformStack.push(currentTransform);
231 }
232 // Checking if any parent rendering is disabled and exit if so
233 if (!tagShouldRender(tag, parents)) {
234 return;
235 }
236
237 // Save the view size
238 if ('svg' === tag.name) {
239 if ('viewBox' in tag.attributes) {
240 values = tag.attributes.viewBox.split(/\s*,*\s|\s,*\s*|,/);
241 const dX = parseFloat(values[0]);
242 const dY = parseFloat(values[1]);
243 const width = parseFloat(values[2]);
244 const height = parseFloat(values[3]);
245
246 // use the viewBox width/height if not specified explictly
247 glyph.width =
248 'width' in tag.attributes
249 ? parseFloat(tag.attributes.width)
250 : width;
251 glyph.height =
252 'height' in tag.attributes
253 ? parseFloat(tag.attributes.height)
254 : height;
255
256 transformStack[transformStack.length - 1]
257 .translate(-dX, -dY)
258 .scale(glyph.width / width, glyph.height / height);
259 } else {
260 if ('width' in tag.attributes) {
261 glyph.width = parseFloat(tag.attributes.width);
262 } else {
263 this.log(
264 `Glyph "${
265 glyph.name
266 }" has no width attribute, defaulting to 150.`
267 );
268 glyph.width = 150;
269 }
270 if ('height' in tag.attributes) {
271 glyph.height = parseFloat(tag.attributes.height);
272 } else {
273 this.log(
274 `Glyph "${
275 glyph.name
276 }" has no height attribute, defaulting to 150.`
277 );
278 glyph.height = 150;
279 }
280 }
281 } else if ('clipPath' === tag.name) {
282 // Clipping path unsupported
283 this.log(
284 `Found a clipPath element in the icon "${
285 glyph.name
286 }" the result may be different than expected.`
287 );
288 } else if ('rect' === tag.name && 'none' !== tag.attributes.fill) {
289 glyph.paths.push(
290 applyTransform(svgShapesToPath.rectToPath(tag.attributes))
291 );
292 } else if ('line' === tag.name && 'none' !== tag.attributes.fill) {
293 this.log(
294 `Found a line element in the icon "${
295 glyph.name
296 }" the result could be different than expected.`
297 );
298 glyph.paths.push(
299 applyTransform(svgShapesToPath.lineToPath(tag.attributes))
300 );
301 } else if ('polyline' === tag.name && 'none' !== tag.attributes.fill) {
302 this.log(
303 `Found a polyline element in the icon "${
304 glyph.name
305 }" the result could be different than expected.`
306 );
307 glyph.paths.push(
308 applyTransform(svgShapesToPath.polylineToPath(tag.attributes))
309 );
310 } else if ('polygon' === tag.name && 'none' !== tag.attributes.fill) {
311 glyph.paths.push(
312 applyTransform(svgShapesToPath.polygonToPath(tag.attributes))
313 );
314 } else if (
315 ['circle', 'ellipse'].includes(tag.name) &&
316 'none' !== tag.attributes.fill
317 ) {
318 glyph.paths.push(
319 applyTransform(svgShapesToPath.circleToPath(tag.attributes))
320 );
321 } else if (
322 'path' === tag.name &&
323 tag.attributes.d &&
324 'none' !== tag.attributes.fill
325 ) {
326 glyph.paths.push(applyTransform(tag.attributes.d));
327 }
328
329 // According to http://www.w3.org/TR/SVG/painting.html#SpecifyingPaint
330 // Map attribute fill to color property
331 if ('none' !== tag.attributes.fill) {
332 color = getTagColor(tag, parents);
333 if ('undefined' !== typeof color) {
334 glyph.color = color;
335 }
336 }
337 } catch (err) {
338 this.emit(
339 'error',
340 new Error(
341 `Got an error parsing the glyph "${glyph.name}": ${err.message}.`
342 )
343 );
344 }
345 });
346
347 saxStream.on('error', err => {
348 this.emit('error', err);
349 });
350
351 saxStream.on('closetag', () => {
352 transformStack.pop();
353 parents.pop();
354 });
355
356 saxStream.on('end', () => {
357 svgIconStreamCallback();
358 });
359
360 svgIconStream.pipe(saxStream);
361 }
362
363 _flush(svgFontFlushCallback) {
364 const maxGlyphHeight = this.glyphs.reduce(
365 (curMax, glyph) => Math.max(curMax, glyph.height),
366 0
367 );
368 const maxGlyphWidth = this.glyphs.reduce(
369 (curMax, glyph) => Math.max(curMax, glyph.width),
370 0
371 );
372 const fontHeight = this._options.fontHeight || maxGlyphHeight;
373 let fontWidth = maxGlyphWidth;
374 if (this._options.normalize) {
375 fontWidth = this.glyphs.reduce(
376 (curMax, glyph) =>
377 Math.max(curMax, fontHeight / glyph.height * glyph.width),
378 0
379 );
380 } else if (this._options.fontHeight) {
381 // even if normalize is off, we need to scale the fontWidth if we have a custom fontHeight
382 fontWidth *= fontHeight / maxGlyphHeight;
383 }
384
385 this._options.ascent =
386 'undefined' !== typeof this._options.ascent
387 ? this._options.ascent
388 : fontHeight - this._options.descent;
389
390 if (
391 !this._options.normalize &&
392 fontHeight >
393 (1 < this.glyphs.length
394 ? this.glyphs.reduce(
395 (curMin, glyph) => Math.min(curMin, glyph.height),
396 Infinity
397 )
398 : this.glyphs[0].height)
399 ) {
400 this.log(
401 'The provided icons do not have the same heights. This could lead' +
402 ' to unexpected results. Using the normalize option may help.'
403 );
404 }
405 if (1000 > fontHeight) {
406 this.log(
407 'A fontHeight of at least than 1000 is recommended, otherwise ' +
408 'further steps (rounding in svg2ttf) could lead to ugly results.' +
409 ' Use the fontHeight option to scale icons.'
410 );
411 }
412
413 // Output the SVG file
414 // (find a SAX parser that allows modifying SVG on the fly)
415 /* eslint-disable prefer-template */
416 this.push(
417 '<?xml version="1.0" standalone="no"?>\n' +
418 '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >\n' +
419 '<svg xmlns="http://www.w3.org/2000/svg">\n' +
420 (this._options.metadata
421 ? '<metadata>' + this._options.metadata + '</metadata>\n'
422 : '') +
423 '<defs>\n' +
424 ' <font id="' +
425 this._options.fontId +
426 '" horiz-adv-x="' +
427 fontWidth +
428 '">\n' +
429 ' <font-face font-family="' +
430 this._options.fontName +
431 '"\n' +
432 ' units-per-em="' +
433 fontHeight +
434 '" ascent="' +
435 this._options.ascent +
436 '"\n' +
437 ' descent="' +
438 this._options.descent +
439 '"' +
440 (this._options.fontWeight
441 ? '\n font-weight="' + this._options.fontWeight + '"'
442 : '') +
443 (this._options.fontStyle
444 ? '\n font-style="' + this._options.fontStyle + '"'
445 : '') +
446 ' />\n' +
447 ' <missing-glyph horiz-adv-x="0" />\n'
448 );
449
450 this.glyphs.forEach(glyph => {
451 const ratio = this._options.normalize
452 ? fontHeight / glyph.height
453 : fontHeight / maxGlyphHeight;
454 if (!isFinite(ratio)) throw new Error('foo');
455 glyph.width *= ratio;
456 glyph.height *= ratio;
457 const glyphPath = new SVGPathData('');
458
459 if (this._options.fixedWidth) {
460 glyph.width = fontWidth;
461 }
462 const yOffset = glyph.height - this._options.descent;
463 const glyphPathTransform = new Matrix().transform(
464 1,
465 0,
466 0,
467 -1,
468 0,
469 yOffset
470 ); // ySymmetry
471 if (1 !== ratio) {
472 glyphPathTransform.scale(ratio, ratio);
473 }
474 glyph.paths.forEach(path => {
475 glyphPath.commands.push(
476 ...path.toAbs().matrix(...glyphPathTransform.toArray()).commands
477 );
478 });
479 if (this._options.centerHorizontally) {
480 const bounds = glyphPath.getBounds();
481
482 glyphPath.translate(
483 (glyph.width - (bounds.maxX - bounds.minX)) / 2 - bounds.minX
484 );
485 }
486 delete glyph.paths;
487 glyph.unicode.forEach((unicode, i) => {
488 const unicodeStr = ucs2
489 .decode(unicode)
490 .map(point => '&#x' + point.toString(16).toUpperCase() + ';')
491 .join('');
492 const d = glyphPath.round(this._options.round).encode();
493
494 this.push(
495 ' <glyph glyph-name="' +
496 glyph.name +
497 (0 === i ? '' : '-' + i) +
498 '"\n' +
499 ' unicode="' +
500 unicodeStr +
501 '"\n' +
502 ' horiz-adv-x="' +
503 glyph.width +
504 '" d="' +
505 d +
506 '" />\n'
507 );
508 });
509 });
510 this.push(' </font>\n' + '</defs>\n' + '</svg>\n');
511 this.log('Font created');
512 if ('function' === typeof this._options.callback) {
513 this._options.callback(this.glyphs);
514 }
515 svgFontFlushCallback();
516 }
517}
518
519module.exports = SVGIcons2SVGFontStream;