1 |
|
2 |
|
3 |
|
4 | 'use strict';
|
5 |
|
6 | const { ucs2 } = require('punycode');
|
7 | const { Transform } = require('stream');
|
8 | const Sax = require('sax');
|
9 | const { SVGPathData } = require('svg-pathdata');
|
10 | const svgShapesToPath = require('./svgshapes2svgpath');
|
11 | const { Matrix } = require('./Matrix');
|
12 |
|
13 |
|
14 | function parseTransforms(value) {
|
15 | return value
|
16 | .match(/(rotate|translate|scale|skewX|skewY|matrix)\s*\(([^)]*)\)\s*/g)
|
17 | .map((transform) => transform.match(/[\w.-]+/g));
|
18 | }
|
19 | function 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 |
|
46 | function 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 |
|
79 |
|
80 |
|
81 | function 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 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | }
|
107 |
|
108 | return fillVal;
|
109 | }
|
110 |
|
111 | class SVGIcons2SVGFontStream extends Transform {
|
112 | constructor(options) {
|
113 | super({ objectMode: true });
|
114 |
|
115 |
|
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);
|
130 | }
|
131 |
|
132 | _transform(svgIconStream, _unused, svgIconStreamCallback) {
|
133 |
|
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 |
|
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 |
|
227 | if (!tagShouldRender(tag, parents)) {
|
228 | return;
|
229 | }
|
230 |
|
231 |
|
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 |
|
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 |
|
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 |
|
314 |
|
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 |
|
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 |
|
398 |
|
399 |
|
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 | );
|
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 |
|
510 | module.exports = SVGIcons2SVGFontStream;
|