forked from enviPath/enviPy
398 lines
13 KiB
JavaScript
Executable File
398 lines
13 KiB
JavaScript
Executable File
/* eslint no-multi-str:0 */
|
|
|
|
'use strict';
|
|
|
|
// Required modules
|
|
var util = require('util');
|
|
var ucs2 = require('punycode').ucs2;
|
|
var Stream = require('readable-stream');
|
|
var Sax = require('sax');
|
|
var SVGPathData = require('svg-pathdata');
|
|
var svgShapesToPath = require('./svgshapes2svgpath');
|
|
|
|
require('string.prototype.codepointat');
|
|
|
|
// Transform helpers (will move elsewhere later)
|
|
function parseTransforms(value) {
|
|
return value.match(
|
|
/(rotate|translate|scale|skewX|skewY|matrix)\s*\(([^\)]*)\)\s*/g
|
|
).map(function(transform) {
|
|
return transform.match(/[\w\.\-]+/g);
|
|
});
|
|
}
|
|
function transformPath(path, transforms) {
|
|
transforms.forEach(function(transform) {
|
|
path[transform[0]].apply(path, transform.slice(1).map(function(n) {
|
|
return parseFloat(n, 10);
|
|
}));
|
|
});
|
|
return path;
|
|
}
|
|
function applyTransforms(d, parents) {
|
|
var transforms = [];
|
|
|
|
parents.forEach(function(parent) {
|
|
if('undefined' !== typeof parent.attributes.transform) {
|
|
transforms = transforms.concat(parseTransforms(parent.attributes.transform));
|
|
}
|
|
});
|
|
return transformPath(new SVGPathData(d), transforms).encode();
|
|
}
|
|
|
|
// Rendering
|
|
function tagShouldRender(curTag, parents) {
|
|
var values;
|
|
|
|
return !parents.some(function(tag) {
|
|
if('undefined' !== typeof tag.attributes.display &&
|
|
'none' === tag.attributes.display.toLowerCase()) {
|
|
return true;
|
|
}
|
|
if('undefined' !== typeof tag.attributes.width &&
|
|
0 === parseFloat(tag.attributes.width, 0)) {
|
|
return true;
|
|
}
|
|
if('undefined' !== typeof tag.attributes.height &&
|
|
0 === parseFloat(tag.attributes.height, 0)) {
|
|
return true;
|
|
}
|
|
if('undefined' !== typeof tag.attributes.viewBox) {
|
|
values = tag.attributes.viewBox.split(/\s*,*\s|\s,*\s*|,/);
|
|
if(0 === parseFloat(values[2]) || 0 === parseFloat(values[3])) {
|
|
return true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// According to the document (http://www.w3.org/TR/SVG/painting.html#FillProperties)
|
|
// fill <paint> none|currentColor|inherit|<color>
|
|
// [<icccolor>]|<funciri> (not support yet)
|
|
function getTagColor(currTag, parents) {
|
|
var defaultColor = 'black';
|
|
var fillVal = currTag.attributes.fill;
|
|
var color;
|
|
var parentsLength = parents.length;
|
|
|
|
if('none' === fillVal) {
|
|
return color;
|
|
}
|
|
if('currentColor' === fillVal) {
|
|
return defaultColor;
|
|
}
|
|
if('inherit' === fillVal) {
|
|
if(0 === parentsLength) {
|
|
return defaultColor;
|
|
}
|
|
return getTagColor(
|
|
parents[parentsLength - 1],
|
|
parents.slice(0, parentsLength - 1)
|
|
);
|
|
// this might be null.
|
|
// For example: <svg ><path fill="inherit" /> </svg>
|
|
// in this case getTagColor should return null
|
|
// recursive call, the bottom element should be svg,
|
|
// and svg didn't fill color, so just return null
|
|
}
|
|
|
|
return fillVal;
|
|
}
|
|
|
|
// Inherit of duplex stream
|
|
util.inherits(SVGIcons2SVGFontStream, Stream.Transform);
|
|
|
|
// Constructor
|
|
function SVGIcons2SVGFontStream(options) {
|
|
var _this = this;
|
|
var glyphs = [];
|
|
var log;
|
|
|
|
options = options || {};
|
|
options.fontName = options.fontName || 'iconfont';
|
|
options.fontId = options.fontId || options.fontName;
|
|
options.fixedWidth = options.fixedWidth || false;
|
|
options.descent = options.descent || 0;
|
|
options.round = options.round || 10e12;
|
|
options.metadata = options.metadata || '';
|
|
|
|
log = options.log || console.log.bind(console); // eslint-disable-line
|
|
|
|
// Ensure new were used
|
|
if(!(this instanceof SVGIcons2SVGFontStream)) {
|
|
return new SVGIcons2SVGFontStream(options);
|
|
}
|
|
|
|
// Parent constructor
|
|
Stream.Transform.call(this, {
|
|
objectMode: true,
|
|
});
|
|
|
|
// Setting objectMode separately
|
|
this._writableState.objectMode = true;
|
|
this._readableState.objectMode = false;
|
|
|
|
// Parse input
|
|
this._transform = function _svgIcons2SVGFontStreamTransform(
|
|
svgIconStream, unused, svgIconStreamCallback
|
|
) {
|
|
// Parsing each icons asynchronously
|
|
var saxStream = Sax.createStream(true);
|
|
var parents = [];
|
|
var glyph = svgIconStream.metadata || {};
|
|
|
|
glyph.d = [];
|
|
glyphs.push(glyph);
|
|
|
|
if('string' !== typeof glyph.name) {
|
|
_this.emit('error', new Error('Please provide a name for the glyph at' +
|
|
' index ' + (glyphs.length - 1)));
|
|
}
|
|
if(glyphs.some(function(anotherGlyph) {
|
|
return (anotherGlyph !== glyph && anotherGlyph.name === glyph.name);
|
|
})) {
|
|
_this.emit('error', new Error('The glyph name "' + glyph.name +
|
|
'" must be unique.'));
|
|
}
|
|
if(glyph.unicode && glyph.unicode instanceof Array && glyph.unicode.length) {
|
|
if(glyph.unicode.some(function(unicodeA, i) {
|
|
return glyph.unicode.some(function(unicodeB, j) {
|
|
return i !== j && unicodeA === unicodeB;
|
|
});
|
|
})) {
|
|
_this.emit('error', new Error('Given codepoints for the glyph "' +
|
|
glyph.name + '" contain duplicates.'));
|
|
}
|
|
} else if('string' !== typeof glyph.unicode) {
|
|
_this.emit('error', new Error('Please provide a codepoint for the glyph "' +
|
|
glyph.name + '"'));
|
|
}
|
|
|
|
if(glyphs.some(function(anotherGlyph) {
|
|
return (anotherGlyph !== glyph && anotherGlyph.unicode === glyph.unicode);
|
|
})) {
|
|
_this.emit('error', new Error('The glyph "' + glyph.name +
|
|
'" codepoint seems to be used already elsewhere.'));
|
|
}
|
|
|
|
saxStream.on('opentag', function(tag) {
|
|
var values;
|
|
var color;
|
|
|
|
parents.push(tag);
|
|
// Checking if any parent rendering is disabled and exit if so
|
|
if(!tagShouldRender(tag, parents)) {
|
|
return;
|
|
}
|
|
try {
|
|
// Save the view size
|
|
if('svg' === tag.name) {
|
|
glyph.dX = 0;
|
|
glyph.dY = 0;
|
|
if('viewBox' in tag.attributes) {
|
|
values = tag.attributes.viewBox.split(/\s*,*\s|\s,*\s*|,/);
|
|
glyph.dX = parseFloat(values[0], 10);
|
|
glyph.dY = parseFloat(values[1], 10);
|
|
glyph.width = parseFloat(values[2], 10);
|
|
glyph.height = parseFloat(values[3], 10);
|
|
}
|
|
if('width' in tag.attributes) {
|
|
glyph.width = parseFloat(tag.attributes.width, 10);
|
|
}
|
|
if('height' in tag.attributes) {
|
|
glyph.height = parseFloat(tag.attributes.height, 10);
|
|
}
|
|
if(!glyph.width || !glyph.height) {
|
|
log('Glyph "' + glyph.name + '" has no size attribute on which to' +
|
|
' get the gylph dimensions (heigh and width or viewBox' +
|
|
' attributes)');
|
|
glyph.width = 150;
|
|
glyph.height = 150;
|
|
}
|
|
// Clipping path unsupported
|
|
} else if('clipPath' === tag.name) {
|
|
log('Found a clipPath element in the icon "' + glyph.name + '" the' +
|
|
'result may be different than expected.');
|
|
// Change rect elements to the corresponding path
|
|
} else if('rect' === tag.name && 'none' !== tag.attributes.fill) {
|
|
glyph.d.push(applyTransforms(svgShapesToPath.rectToPath(tag.attributes), parents));
|
|
} else if('line' === tag.name && 'none' !== tag.attributes.fill) {
|
|
log('Found a line element in the icon "' + glyph.name + '" the result' +
|
|
' could be different than expected.');
|
|
glyph.d.push(applyTransforms(svgShapesToPath.lineToPath(tag.attributes), parents));
|
|
} else if('polyline' === tag.name && 'none' !== tag.attributes.fill) {
|
|
log('Found a polyline element in the icon "' + glyph.name + '" the' +
|
|
' result could be different than expected.');
|
|
glyph.d.push(applyTransforms(svgShapesToPath.polylineToPath(tag.attributes), parents));
|
|
} else if('polygon' === tag.name && 'none' !== tag.attributes.fill) {
|
|
glyph.d.push(applyTransforms(svgShapesToPath.polygonToPath(tag.attributes), parents));
|
|
} else if('circle' === tag.name || 'ellipse' === tag.name &&
|
|
'none' !== tag.attributes.fill) {
|
|
glyph.d.push(applyTransforms(svgShapesToPath.circleToPath(tag.attributes), parents));
|
|
} else if('path' === tag.name && tag.attributes.d &&
|
|
'none' !== tag.attributes.fill) {
|
|
glyph.d.push(applyTransforms(tag.attributes.d, parents));
|
|
}
|
|
|
|
// According to http://www.w3.org/TR/SVG/painting.html#SpecifyingPaint
|
|
// Map attribute fill to color property
|
|
if('none' !== tag.attributes.fill) {
|
|
color = getTagColor(tag, parents);
|
|
if('undefined' !== typeof color) {
|
|
glyph.color = color;
|
|
}
|
|
}
|
|
} catch(err) {
|
|
_this.emit('error', new Error('Got an error parsing the glyph' +
|
|
' "' + glyph.name + '": ' + err.message + '.'));
|
|
}
|
|
});
|
|
|
|
saxStream.on('error', function svgicons2svgfontSaxErrorCb(err) {
|
|
_this.emit('error', err);
|
|
});
|
|
|
|
saxStream.on('closetag', function svgicons2svgfontSaxCloseTagCb() {
|
|
parents.pop();
|
|
});
|
|
|
|
saxStream.on('end', function svgicons2svgfontSaxEnbCb() {
|
|
svgIconStreamCallback();
|
|
});
|
|
|
|
svgIconStream.pipe(saxStream);
|
|
};
|
|
|
|
// Output data
|
|
this._flush = function _svgIcons2SVGFontStreamFlush(svgFontFlushCallback) {
|
|
var fontWidth = (
|
|
1 < glyphs.length ?
|
|
glyphs.reduce(function(curMax, glyph) {
|
|
return Math.max(curMax, glyph.width);
|
|
}, 0) :
|
|
glyphs[0].width);
|
|
var fontHeight = options.fontHeight || (
|
|
1 < glyphs.length ? glyphs.reduce(function(curMax, glyph) {
|
|
return Math.max(curMax, glyph.height);
|
|
}, 0) :
|
|
glyphs[0].height);
|
|
|
|
options.ascent = 'undefined' !== typeof options.ascent ?
|
|
options.ascent :
|
|
fontHeight - options.descent;
|
|
|
|
if(
|
|
(!options.normalize) &&
|
|
fontHeight > (1 < glyphs.length ?
|
|
glyphs.reduce(function(curMin, glyph) {
|
|
return Math.min(curMin, glyph.height);
|
|
}, Infinity) :
|
|
glyphs[0].height
|
|
)
|
|
) {
|
|
log('The provided icons does not have the same height it could lead' +
|
|
' to unexpected results. Using the normalize option could' +
|
|
' solve the problem.');
|
|
}
|
|
if(1000 > fontHeight) {
|
|
log('The fontHeight should larger than 1000 or it will be converted ' +
|
|
'into the wrong shape. Using the "normalize" and "fontHeight"' +
|
|
' options can solve the problem.');
|
|
}
|
|
|
|
// Output the SVG file
|
|
// (find a SAX parser that allows modifying SVG on the fly)
|
|
_this.push('\
|
|
<?xml version="1.0" standalone="no"?> \n\
|
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"' +
|
|
' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >\n\
|
|
<svg xmlns="http://www.w3.org/2000/svg">\n' + (
|
|
options.metadata ? '<metadata>' + options.metadata + '</metadata>\n' : ''
|
|
) + '\
|
|
<defs>\n\
|
|
<font id="' + options.fontId + '" horiz-adv-x="' + fontWidth + '">\n\
|
|
<font-face font-family="' + options.fontName + '"\n\
|
|
units-per-em="' + fontHeight + '" ascent="' + options.ascent + '"\n\
|
|
descent="' + options.descent + '"' + (options.fontWeight ? '\n\
|
|
font-weight="' + options.fontWeight + '"' : '') + (options.fontStyle ? '\n\
|
|
font-style="' + options.fontStyle + '"' : '') + ' />\n\
|
|
<missing-glyph horiz-adv-x="0" />\n');
|
|
glyphs.forEach(function(glyph) {
|
|
var ratio = fontHeight / glyph.height;
|
|
var d = '';
|
|
var bounds;
|
|
var pathData;
|
|
|
|
if(options.fixedWidth) {
|
|
glyph.width = fontWidth;
|
|
}
|
|
if(options.normalize) {
|
|
glyph.height = fontHeight;
|
|
if(!options.fixedWidth) {
|
|
glyph.width *= ratio;
|
|
}
|
|
}
|
|
glyph.d.forEach(function(cD) {
|
|
d += ' ' + new SVGPathData(cD)
|
|
.toAbs()
|
|
.translate(-glyph.dX, -glyph.dY)
|
|
.scale(
|
|
options.normalize ? ratio : 1,
|
|
options.normalize ? ratio : 1)
|
|
.ySymetry(glyph.height - options.descent)
|
|
.round(options.round)
|
|
.encode();
|
|
});
|
|
if(options.centerHorizontally) {
|
|
// Naive bounds calculation (should draw, then calculate bounds...)
|
|
pathData = new SVGPathData(d);
|
|
bounds = {
|
|
x1: Infinity,
|
|
y1: Infinity,
|
|
x2: 0,
|
|
y2: 0,
|
|
};
|
|
pathData.toAbs().commands.forEach(function(command) {
|
|
bounds.x1 = 'undefined' != typeof command.x && command.x < bounds.x1 ?
|
|
command.x :
|
|
bounds.x1;
|
|
bounds.y1 = 'undefined' != typeof command.y && command.y < bounds.y1 ?
|
|
command.y :
|
|
bounds.y1;
|
|
bounds.x2 = 'undefined' != typeof command.x && command.x > bounds.x2 ?
|
|
command.x :
|
|
bounds.x2;
|
|
bounds.y2 = 'undefined' != typeof command.y && command.y > bounds.y2 ?
|
|
command.y :
|
|
bounds.y2;
|
|
});
|
|
d = pathData
|
|
.translate(((glyph.width - (bounds.x2 - bounds.x1)) / 2) - bounds.x1)
|
|
.round(options.round)
|
|
.encode();
|
|
}
|
|
delete glyph.d;
|
|
delete glyph.running;
|
|
glyph.unicode.forEach(function(unicode, i) {
|
|
_this.push('\
|
|
<glyph glyph-name="' + glyph.name + (0 === i ? '' : '-' + i) + '"\n\
|
|
unicode="' + ucs2.decode(unicode).map(function(point) {
|
|
return '&#x' + point.toString(16).toUpperCase() + ';';
|
|
}).join('') + '"\n\
|
|
horiz-adv-x="' + glyph.width + '" d="' + d + '" />\n');
|
|
});
|
|
});
|
|
_this.push('\
|
|
</font>\n\
|
|
</defs>\n\
|
|
</svg>\n');
|
|
log('Font created');
|
|
if('function' === (typeof options.callback)) {
|
|
(options.callback)(glyphs);
|
|
}
|
|
svgFontFlushCallback();
|
|
};
|
|
|
|
}
|
|
|
|
module.exports = SVGIcons2SVGFontStream;
|