470 lines
13 KiB
JavaScript
470 lines
13 KiB
JavaScript
var debug = require('debug')('ylt:fontAnalyzer');
|
|
|
|
var Q = require('q');
|
|
var fontkit = require('fontkit');
|
|
var woffTools = require('woff-tools');
|
|
var ttf2woff2 = require('ttf2woff2');
|
|
|
|
var FontAnalyzer = function() {
|
|
|
|
function analyzeFont(entry, charsListOnPage) {
|
|
var deferred = Q.defer();
|
|
|
|
if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
|
|
// No valid file available
|
|
deferred.resolve(entry);
|
|
return deferred.promise;
|
|
}
|
|
|
|
if (entry.isWebFont) {
|
|
debug('File %s is a font. Let\'s have a look inside!', entry.url);
|
|
|
|
convertToWoff2(entry)
|
|
|
|
.then(function(entry) {
|
|
return getMetricsFromFont(entry, charsListOnPage);
|
|
})
|
|
|
|
.then(function(fontMetrics) {
|
|
entry.fontMetrics = fontMetrics;
|
|
})
|
|
|
|
.fail(function(error) {
|
|
debug('Could not open the font: %s', error);
|
|
});
|
|
|
|
}
|
|
|
|
deferred.resolve(entry);
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
function convertToWoff2(entry) {
|
|
var deferred = Q.defer();
|
|
|
|
debug('Entering font format converter...');
|
|
|
|
var fileSize = entry.weightCheck.bodySize;
|
|
var ttf;
|
|
var woff2;
|
|
var newFileSize;
|
|
|
|
if (entry.isWoff2) {
|
|
|
|
debug('File is already a woff2.');
|
|
deferred.resolve(entry);
|
|
|
|
} else if (fileSize > 1024 * 1024) {
|
|
|
|
// Don't try to convert huge font files, it would block the server for several minutes
|
|
debug('Font is too big, skipping conversion.');
|
|
deferred.resolve(entry);
|
|
|
|
} else if (entry.isWoff) {
|
|
|
|
debug('File is a woff. Let\'s convert to woff2');
|
|
|
|
try {
|
|
|
|
debug('Current file size is %d', fileSize);
|
|
|
|
ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer);
|
|
woff2 = ttf2woff2(ttf);
|
|
newFileSize = woff2.length;
|
|
|
|
debug('New font size is %d', newFileSize);
|
|
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
|
entry.weightCheck.sizeAsWoff2 = newFileSize;
|
|
|
|
deferred.resolve(entry);
|
|
|
|
} catch(error) {
|
|
deferred.reject(error);
|
|
}
|
|
|
|
} else if (entry.isTTF) {
|
|
|
|
debug('File is a TTF. Let\'s convert to woff2');
|
|
|
|
try {
|
|
|
|
debug('Current file size is %d', fileSize);
|
|
|
|
woff2 = ttf2woff2(entry.weightCheck.bodyBuffer);
|
|
newFileSize = woff2.length;
|
|
|
|
debug('New image size is %d', newFileSize);
|
|
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
|
entry.weightCheck.sizeAsWoff2 = newFileSize;
|
|
|
|
deferred.resolve(entry);
|
|
|
|
} catch(error) {
|
|
deferred.reject(error);
|
|
}
|
|
|
|
} else {
|
|
// Other font formats are not handled
|
|
deferred.resolve(entry);
|
|
}
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
// The gain is estimated of enough value if it's over 1KB or over 20%,
|
|
// but it's ignored if is below 100 bytes
|
|
function gainIsEnough(oldWeight, newWeight) {
|
|
var gain = oldWeight - newWeight;
|
|
var ratio = gain / oldWeight;
|
|
return (gain > 2048 || (ratio > 0.2 && gain > 100));
|
|
}
|
|
|
|
function getMetricsFromFont(entry, charsListOnPage) {
|
|
var deferred = Q.defer();
|
|
|
|
try {
|
|
var startTime = Date.now();
|
|
var font = fontkit.create(entry.weightCheck.bodyBuffer);
|
|
|
|
var result = {
|
|
name: font.fullName || font.postscriptName || font.familyName,
|
|
numGlyphs: font.numGlyphs,
|
|
averageGlyphComplexity: getAverageGlyphComplexity(font),
|
|
compressedWeight: entry.weightCheck.afterGzipCompression || entry.weightCheck.bodySize,
|
|
unicodeRanges: readUnicodeRanges(font.characterSet, charsListOnPage),
|
|
numGlyphsInCommonWithPageContent: countPossiblyUsedGlyphs(getCharacterSetAsString(font.characterSet), charsListOnPage)
|
|
};
|
|
|
|
var endTime = Date.now();
|
|
debug('Font analysis took %dms', endTime - startTime);
|
|
|
|
// Mark fonts that are not used on the page (#224)
|
|
var fontIsUsed = false;
|
|
for (var range in result.unicodeRanges) {
|
|
if (result.unicodeRanges[range].numGlyphsInCommonWithPageContent > 0) {
|
|
fontIsUsed = true;
|
|
break;
|
|
}
|
|
}
|
|
result.isUsed = fontIsUsed;
|
|
|
|
deferred.resolve(result);
|
|
} catch(error) {
|
|
deferred.reject(error);
|
|
}
|
|
|
|
return deferred.promise;
|
|
}
|
|
|
|
// Reads the number of vector commands (complexity) needed to render glyphs and
|
|
// returns the average. Only first 100 glyphes are tested, otherwise it would take tool long;
|
|
function getAverageGlyphComplexity(font) {
|
|
var max = Math.min(font.numGlyphs, 100);
|
|
|
|
var totalPathsCommands = 0;
|
|
for (var i = 0; i < max; i++) {
|
|
totalPathsCommands += font.getGlyph(i).path.commands.length;
|
|
}
|
|
return Math.round(totalPathsCommands / max * 10) / 10;
|
|
}
|
|
|
|
function readUnicodeRanges(charsetInFont, charsListOnPage) {
|
|
var ranges = {};
|
|
|
|
// Assign a range to each char found in the font
|
|
charsetInFont.forEach(function(char) {
|
|
|
|
var currentRange = getUnicodeRangeFromChar(char);
|
|
var currentRangeName = currentRange.name;
|
|
|
|
if (!ranges[currentRangeName]) {
|
|
// Cloning the object
|
|
ranges[currentRangeName] = Object.assign({}, currentRange);
|
|
}
|
|
|
|
if (!ranges[currentRangeName].charset) {
|
|
ranges[currentRangeName].charset = '';
|
|
}
|
|
|
|
ranges[currentRangeName].charset += String.fromCharCode(char);
|
|
});
|
|
|
|
var range;
|
|
var expectedLength;
|
|
var actualLength;
|
|
|
|
for (var rangeName in ranges) {
|
|
/*jshint loopfunc: true */
|
|
|
|
range = ranges[rangeName];
|
|
|
|
// Estimate if range is used, based on the characters found in the page
|
|
range.numGlyphsInCommonWithPageContent = countPossiblyUsedGlyphs(range.charset, charsListOnPage);
|
|
|
|
// Calculate coverage
|
|
if (rangeName !== 'Others') {
|
|
expectedLength = range.rangeEnd - range.rangeStart + 1;
|
|
actualLength = range.charset.length;
|
|
range.coverage = Math.min(actualLength / expectedLength, 1);
|
|
}
|
|
}
|
|
|
|
return ranges;
|
|
}
|
|
|
|
function countPossiblyUsedGlyphs(charsetInFont, charsListOnPage) {
|
|
var count = 0;
|
|
charsListOnPage.split('').forEach(function(char) {
|
|
if (charsetInFont.indexOf(char) >= 0) {
|
|
count ++;
|
|
}
|
|
});
|
|
return count;
|
|
}
|
|
|
|
function getCharacterSetAsString(characterSet) {
|
|
var str = '';
|
|
characterSet.forEach(function(charCode) {
|
|
str += String.fromCharCode(charCode);
|
|
});
|
|
return str;
|
|
}
|
|
|
|
function getUnicodeRangeFromChar(char) {
|
|
return UNICODE_RANGES.find(function(range) {
|
|
return (char >= range.rangeStart && char <= range.rangeEnd);
|
|
}) || {name: 'Others'};
|
|
}
|
|
|
|
|
|
var UNICODE_RANGES = [
|
|
{
|
|
name: 'Basic Latin',
|
|
rangeStart: 0x0020,
|
|
rangeEnd: 0x007F
|
|
},
|
|
{
|
|
name: 'Latin-1 Supplement',
|
|
rangeStart: 0x00A0,
|
|
rangeEnd: 0x00FF
|
|
},
|
|
{
|
|
name: 'Latin Extended',
|
|
rangeStart: 0x0100,
|
|
rangeEnd: 0x024F
|
|
},
|
|
{
|
|
name: 'IPA Extensions',
|
|
rangeStart: 0x0250,
|
|
rangeEnd: 0x02AF
|
|
},
|
|
{
|
|
name: 'Greek and Coptic',
|
|
rangeStart: 0x0370,
|
|
rangeEnd: 0x03FF
|
|
},
|
|
{
|
|
name: 'Cyrillic',
|
|
rangeStart: 0x0400,
|
|
rangeEnd: 0x052F
|
|
},
|
|
{
|
|
name: 'Armenian',
|
|
rangeStart: 0x0530,
|
|
rangeEnd: 0x058F
|
|
},
|
|
{
|
|
name: 'Hebrew',
|
|
rangeStart: 0x0590,
|
|
rangeEnd: 0x05FF
|
|
},
|
|
{
|
|
name: 'Arabic',
|
|
rangeStart: 0x0600,
|
|
rangeEnd: 0x06FF
|
|
},
|
|
{
|
|
name: 'Syriac',
|
|
rangeStart: 0x0700,
|
|
rangeEnd: 0x074F
|
|
},
|
|
{
|
|
name: 'Thaana',
|
|
rangeStart: 0x0780,
|
|
rangeEnd: 0x07BF
|
|
},
|
|
{
|
|
name: 'Devanagari',
|
|
rangeStart: 0x0900,
|
|
rangeEnd: 0x097F
|
|
},
|
|
{
|
|
name: 'Bengali',
|
|
rangeStart: 0x0980,
|
|
rangeEnd: 0x09FF
|
|
},
|
|
{
|
|
name: 'Gurmukhi',
|
|
rangeStart: 0x0A00,
|
|
rangeEnd: 0x0A7F
|
|
},
|
|
{
|
|
name: 'Gujarati',
|
|
rangeStart: 0x0A80,
|
|
rangeEnd: 0x0AFF
|
|
},
|
|
{
|
|
name: 'Oriya',
|
|
rangeStart: 0x0B00,
|
|
rangeEnd: 0x0B7F
|
|
},
|
|
{
|
|
name: 'Tamil',
|
|
rangeStart: 0x0B80,
|
|
rangeEnd: 0x0BFF
|
|
},
|
|
{
|
|
name: 'Telugu',
|
|
rangeStart: 0x0C00,
|
|
rangeEnd: 0x0C7F
|
|
},
|
|
{
|
|
name: 'Kannada',
|
|
rangeStart: 0x0C80,
|
|
rangeEnd: 0x0CFF
|
|
},
|
|
{
|
|
name: 'Malayalam',
|
|
rangeStart: 0x0D00,
|
|
rangeEnd: 0x0D7F
|
|
},
|
|
{
|
|
name: 'Sinhala',
|
|
rangeStart: 0x0D80,
|
|
rangeEnd: 0x0DFF
|
|
},
|
|
{
|
|
name: 'Thai',
|
|
rangeStart: 0x0E00,
|
|
rangeEnd: 0x0E7F
|
|
},
|
|
{
|
|
name: 'Lao',
|
|
rangeStart: 0x0E80,
|
|
rangeEnd: 0x0EFF
|
|
},
|
|
{
|
|
name: 'Tibetan',
|
|
rangeStart: 0x0F00,
|
|
rangeEnd: 0x0FFF
|
|
},
|
|
{
|
|
name: 'Myanmar',
|
|
rangeStart: 0x1000,
|
|
rangeEnd: 0x109F
|
|
},
|
|
{
|
|
name: 'Georgian',
|
|
rangeStart: 0x10A0,
|
|
rangeEnd: 0x10FF
|
|
},
|
|
{
|
|
name: 'Hangul Jamo',
|
|
rangeStart: 0x1100,
|
|
rangeEnd: 0x11FF
|
|
},
|
|
{
|
|
name: 'Ethiopic',
|
|
rangeStart: 0x1200,
|
|
rangeEnd: 0x137F
|
|
},
|
|
{
|
|
name: 'Cherokee',
|
|
rangeStart: 0x13A0,
|
|
rangeEnd: 0x13FF
|
|
},
|
|
{
|
|
name: 'Unified Canadian Aboriginal Syllabics',
|
|
rangeStart: 0x1400,
|
|
rangeEnd: 0x167F
|
|
},
|
|
{
|
|
name: 'Ogham',
|
|
rangeStart: 0x1680,
|
|
rangeEnd: 0x169F
|
|
},
|
|
{
|
|
name: 'Runic',
|
|
rangeStart: 0x16A0,
|
|
rangeEnd: 0x16FF
|
|
},
|
|
{
|
|
name: 'Tagalog',
|
|
rangeStart: 0x1700,
|
|
rangeEnd: 0x171F
|
|
},
|
|
{
|
|
name: 'Hanunoo',
|
|
rangeStart: 0x1720,
|
|
rangeEnd: 0x173F
|
|
},
|
|
{
|
|
name: 'Buhid',
|
|
rangeStart: 0x1740,
|
|
rangeEnd: 0x175F
|
|
},
|
|
{
|
|
name: 'Tagbanwa',
|
|
rangeStart: 0x1760,
|
|
rangeEnd: 0x177F
|
|
},
|
|
{
|
|
name: 'Khmer',
|
|
rangeStart: 0x1780,
|
|
rangeEnd: 0x17FF
|
|
},
|
|
{
|
|
name: 'Mongolian',
|
|
rangeStart: 0x1800,
|
|
rangeEnd: 0x18AF
|
|
},
|
|
{
|
|
name: 'Limbu',
|
|
rangeStart: 0x1900,
|
|
rangeEnd: 0x194F
|
|
},
|
|
{
|
|
name: 'Tai Le',
|
|
rangeStart: 0x1950,
|
|
rangeEnd: 0x197F
|
|
},
|
|
{
|
|
name: 'Hiragana',
|
|
rangeStart: 0x3040,
|
|
rangeEnd: 0x309F
|
|
},
|
|
{
|
|
name: 'Katakana',
|
|
rangeStart: 0x30A0,
|
|
rangeEnd: 0x30FF
|
|
},
|
|
{
|
|
name: 'Bopomofo',
|
|
rangeStart: 0x3100,
|
|
rangeEnd: 0x312F
|
|
}
|
|
];
|
|
|
|
return {
|
|
analyzeFont: analyzeFont,
|
|
getMetricsFromFont: getMetricsFromFont,
|
|
readUnicodeRanges: readUnicodeRanges,
|
|
getAverageGlyphComplexity: getAverageGlyphComplexity,
|
|
countPossiblyUsedGlyphs: countPossiblyUsedGlyphs,
|
|
getCharacterSetAsString: getCharacterSetAsString,
|
|
getUnicodeRangeFromChar: getUnicodeRangeFromChar
|
|
};
|
|
};
|
|
|
|
module.exports = new FontAnalyzer(); |