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