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.
244 lines
6.7 KiB
244 lines
6.7 KiB
'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;
|
|
|