You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
6.7 KiB
245 lines
6.7 KiB
2 years ago
|
'use strict';
|
||
|
|
||
|
var _ = require('lodash');
|
||
|
var cubic2quad = require('cubic2quad');
|
||
|
var svgpath = require('svgpath');
|
||
|
var DOMParser = require('@xmldom/xmldom').DOMParser;
|
||
|
var ucs2 = require('./ucs2');
|
||
|
|
||
|
function getGlyph(glyphElem, fontInfo) {
|
||
|
var glyph = {};
|
||
|
|
||
|
if (glyphElem.hasAttribute('d')) {
|
||
|
glyph.d = glyphElem.getAttribute('d').trim();
|
||
|
} else {
|
||
|
// try nested <path>
|
||
|
var pathElem = glyphElem.getElementsByTagName('path')[0];
|
||
|
|
||
|
if (pathElem.hasAttribute('d')) {
|
||
|
// <path> has reversed Y axis
|
||
|
glyph.d = svgpath(pathElem.getAttribute('d'))
|
||
|
.scale(1, -1)
|
||
|
.translate(0, fontInfo.ascent)
|
||
|
.toString();
|
||
|
} else {
|
||
|
throw new Error("Can't find 'd' attribute of <glyph> tag.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
glyph.unicode = [];
|
||
|
|
||
|
if (glyphElem.getAttribute('unicode')) {
|
||
|
glyph.character = glyphElem.getAttribute('unicode');
|
||
|
var unicode = ucs2.decode(glyph.character);
|
||
|
|
||
|
// If more than one code point is involved, the glyph is a ligature glyph
|
||
|
if (unicode.length > 1) {
|
||
|
glyph.ligature = glyph.character;
|
||
|
glyph.ligatureCodes = unicode;
|
||
|
} else {
|
||
|
glyph.unicode.push(unicode[0]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
glyph.name = glyphElem.getAttribute('glyph-name');
|
||
|
|
||
|
if (glyphElem.getAttribute('horiz-adv-x')) {
|
||
|
glyph.width = parseInt(glyphElem.getAttribute('horiz-adv-x'), 10);
|
||
|
}
|
||
|
|
||
|
return glyph;
|
||
|
}
|
||
|
|
||
|
function deduplicateGlyps(glyphs, ligatures) {
|
||
|
// Result (the list of unique glyphs)
|
||
|
var result = [];
|
||
|
|
||
|
_.forEach(glyphs, function (glyph) {
|
||
|
// Search for glyphs with the same properties (width and d)
|
||
|
var canonical = _.find(result, { width: glyph.width, d: glyph.d });
|
||
|
|
||
|
if (canonical) {
|
||
|
// Add the code points to the unicode array.
|
||
|
// The fields “name” and “character” are not that important so we leave them how we first enounter them and throw the rest away
|
||
|
canonical.unicode = canonical.unicode.concat(glyph.unicode);
|
||
|
glyph.canonical = canonical;
|
||
|
} else {
|
||
|
result.push(glyph);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Update ligatures to point to the canonical version
|
||
|
_.forEach(ligatures, function (ligature) {
|
||
|
while (_.has(ligature.glyph, 'canonical')) {
|
||
|
ligature.glyph = ligature.glyph.canonical;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
function load(str) {
|
||
|
var attrs;
|
||
|
|
||
|
var doc = (new DOMParser()).parseFromString(str, 'application/xml');
|
||
|
|
||
|
var metadata, fontElem, fontFaceElem;
|
||
|
|
||
|
metadata = doc.getElementsByTagName('metadata')[0];
|
||
|
fontElem = doc.getElementsByTagName('font')[0];
|
||
|
|
||
|
if (!fontElem) {
|
||
|
throw new Error("Can't find <font> tag. Make sure you SVG file is font, not image.");
|
||
|
}
|
||
|
|
||
|
fontFaceElem = fontElem.getElementsByTagName('font-face')[0];
|
||
|
|
||
|
var familyName = fontFaceElem.getAttribute('font-family') || 'fontello';
|
||
|
var subfamilyName = fontFaceElem.getAttribute('font-style') || 'Regular';
|
||
|
var id = fontElem.getAttribute('id') || (familyName + '-' + subfamilyName).replace(/[\s\(\)\[\]<>%\/]/g, '').substr(0, 62);
|
||
|
|
||
|
var font = {
|
||
|
id: id,
|
||
|
familyName: familyName,
|
||
|
subfamilyName: subfamilyName,
|
||
|
stretch: fontFaceElem.getAttribute('font-stretch') || 'normal'
|
||
|
};
|
||
|
|
||
|
// Doesn't work with complex content like <strong>Copyright:></strong><em>Fontello</em>
|
||
|
if (metadata && metadata.textContent) {
|
||
|
font.metadata = metadata.textContent;
|
||
|
}
|
||
|
|
||
|
// Get <font> numeric attributes
|
||
|
attrs = {
|
||
|
width: 'horiz-adv-x',
|
||
|
//height: 'vert-adv-y',
|
||
|
horizOriginX: 'horiz-origin-x',
|
||
|
horizOriginY: 'horiz-origin-y',
|
||
|
vertOriginX: 'vert-origin-x',
|
||
|
vertOriginY: 'vert-origin-y'
|
||
|
};
|
||
|
_.forEach(attrs, function (val, key) {
|
||
|
if (fontElem.hasAttribute(val)) { font[key] = parseInt(fontElem.getAttribute(val), 10); }
|
||
|
});
|
||
|
|
||
|
// Get <font-face> numeric attributes
|
||
|
attrs = {
|
||
|
ascent: 'ascent',
|
||
|
descent: 'descent',
|
||
|
unitsPerEm: 'units-per-em',
|
||
|
capHeight: 'cap-height',
|
||
|
xHeight: 'x-height',
|
||
|
underlineThickness: 'underline-thickness',
|
||
|
underlinePosition: 'underline-position'
|
||
|
};
|
||
|
_.forEach(attrs, function (val, key) {
|
||
|
if (fontFaceElem.hasAttribute(val)) { font[key] = parseInt(fontFaceElem.getAttribute(val), 10); }
|
||
|
});
|
||
|
|
||
|
if (fontFaceElem.hasAttribute('font-weight')) {
|
||
|
font.weightClass = fontFaceElem.getAttribute('font-weight');
|
||
|
}
|
||
|
|
||
|
var missingGlyphElem = fontElem.getElementsByTagName('missing-glyph')[0];
|
||
|
|
||
|
if (missingGlyphElem) {
|
||
|
|
||
|
font.missingGlyph = {};
|
||
|
font.missingGlyph.d = missingGlyphElem.getAttribute('d') || '';
|
||
|
|
||
|
if (missingGlyphElem.getAttribute('horiz-adv-x')) {
|
||
|
font.missingGlyph.width = parseInt(missingGlyphElem.getAttribute('horiz-adv-x'), 10);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var glyphs = [];
|
||
|
var ligatures = [];
|
||
|
|
||
|
_.forEach(fontElem.getElementsByTagName('glyph'), function (glyphElem) {
|
||
|
var glyph = getGlyph(glyphElem, font);
|
||
|
|
||
|
if (_.has(glyph, 'ligature')) {
|
||
|
ligatures.push({
|
||
|
ligature: glyph.ligature,
|
||
|
unicode: glyph.ligatureCodes,
|
||
|
glyph: glyph
|
||
|
});
|
||
|
}
|
||
|
|
||
|
glyphs.push(glyph);
|
||
|
});
|
||
|
|
||
|
glyphs = deduplicateGlyps(glyphs, ligatures);
|
||
|
|
||
|
font.glyphs = glyphs;
|
||
|
font.ligatures = ligatures;
|
||
|
|
||
|
return font;
|
||
|
}
|
||
|
|
||
|
|
||
|
function cubicToQuad(segment, index, x, y, accuracy) {
|
||
|
if (segment[0] === 'C') {
|
||
|
var quadCurves = cubic2quad(
|
||
|
x, y,
|
||
|
segment[1], segment[2],
|
||
|
segment[3], segment[4],
|
||
|
segment[5], segment[6],
|
||
|
accuracy
|
||
|
);
|
||
|
|
||
|
var res = [];
|
||
|
|
||
|
for (var i = 2; i < quadCurves.length; i += 4) {
|
||
|
res.push([ 'Q', quadCurves[i], quadCurves[i + 1], quadCurves[i + 2], quadCurves[i + 3] ]);
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Converts svg points to contours. All points must be converted
|
||
|
// to relative ones, smooth curves must be converted to generic ones
|
||
|
// before this conversion.
|
||
|
//
|
||
|
function toSfntCoutours(svgPath) {
|
||
|
var resContours = [];
|
||
|
var resContour = [];
|
||
|
|
||
|
svgPath.iterate(function (segment, index, x, y) {
|
||
|
|
||
|
//start new contour
|
||
|
if (index === 0 || segment[0] === 'M') {
|
||
|
resContour = [];
|
||
|
resContours.push(resContour);
|
||
|
}
|
||
|
|
||
|
var name = segment[0];
|
||
|
|
||
|
if (name === 'Q') {
|
||
|
//add control point of quad spline, it is not on curve
|
||
|
resContour.push({ x: segment[1], y: segment[2], onCurve: false });
|
||
|
}
|
||
|
|
||
|
// add on-curve point
|
||
|
if (name === 'H') {
|
||
|
// vertical line has Y coordinate only, X remains the same
|
||
|
resContour.push({ x: segment[1], y: y, onCurve: true });
|
||
|
} else if (name === 'V') {
|
||
|
// horizontal line has X coordinate only, Y remains the same
|
||
|
resContour.push({ x: x, y: segment[1], onCurve: true });
|
||
|
} else if (name !== 'Z') {
|
||
|
// for all commands (except H and V) X and Y are placed in the end of the segment
|
||
|
resContour.push({ x: segment[segment.length - 2], y: segment[segment.length - 1], onCurve: true });
|
||
|
}
|
||
|
|
||
|
});
|
||
|
return resContours;
|
||
|
}
|
||
|
|
||
|
|
||
|
module.exports.load = load;
|
||
|
module.exports.cubicToQuad = cubicToQuad;
|
||
|
module.exports.toSfntCoutours = toSfntCoutours;
|