123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- 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();
|