444 lines
14 KiB
JavaScript
444 lines
14 KiB
JavaScript
import createNS from './helpers/svg_elements';
|
||
import createTag from './helpers/html_elements';
|
||
import getFontProperties from './getFontProperties';
|
||
|
||
const FontManager = (function () {
|
||
var maxWaitingTime = 5000;
|
||
var emptyChar = {
|
||
w: 0,
|
||
size: 0,
|
||
shapes: [],
|
||
data: {
|
||
shapes: [],
|
||
},
|
||
};
|
||
var combinedCharacters = [];
|
||
// Hindi characters
|
||
combinedCharacters = combinedCharacters.concat([2304, 2305, 2306, 2307, 2362, 2363, 2364, 2364, 2366,
|
||
2367, 2368, 2369, 2370, 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2379,
|
||
2380, 2381, 2382, 2383, 2387, 2388, 2389, 2390, 2391, 2402, 2403]);
|
||
|
||
var BLACK_FLAG_CODE_POINT = 127988;
|
||
var CANCEL_TAG_CODE_POINT = 917631;
|
||
var A_TAG_CODE_POINT = 917601;
|
||
var Z_TAG_CODE_POINT = 917626;
|
||
var VARIATION_SELECTOR_16_CODE_POINT = 65039;
|
||
var ZERO_WIDTH_JOINER_CODE_POINT = 8205;
|
||
var REGIONAL_CHARACTER_A_CODE_POINT = 127462;
|
||
var REGIONAL_CHARACTER_Z_CODE_POINT = 127487;
|
||
|
||
var surrogateModifiers = [
|
||
'd83cdffb',
|
||
'd83cdffc',
|
||
'd83cdffd',
|
||
'd83cdffe',
|
||
'd83cdfff',
|
||
];
|
||
|
||
function trimFontOptions(font) {
|
||
var familyArray = font.split(',');
|
||
var i;
|
||
var len = familyArray.length;
|
||
var enabledFamilies = [];
|
||
for (i = 0; i < len; i += 1) {
|
||
if (familyArray[i] !== 'sans-serif' && familyArray[i] !== 'monospace') {
|
||
enabledFamilies.push(familyArray[i]);
|
||
}
|
||
}
|
||
return enabledFamilies.join(',');
|
||
}
|
||
|
||
function setUpNode(font, family) {
|
||
var parentNode = createTag('span');
|
||
// Node is invisible to screen readers.
|
||
parentNode.setAttribute('aria-hidden', true);
|
||
parentNode.style.fontFamily = family;
|
||
var node = createTag('span');
|
||
// Characters that vary significantly among different fonts
|
||
node.innerText = 'giItT1WQy@!-/#';
|
||
// Visible - so we can measure it - but not on the screen
|
||
parentNode.style.position = 'absolute';
|
||
parentNode.style.left = '-10000px';
|
||
parentNode.style.top = '-10000px';
|
||
// Large font size makes even subtle changes obvious
|
||
parentNode.style.fontSize = '300px';
|
||
// Reset any font properties
|
||
parentNode.style.fontVariant = 'normal';
|
||
parentNode.style.fontStyle = 'normal';
|
||
parentNode.style.fontWeight = 'normal';
|
||
parentNode.style.letterSpacing = '0';
|
||
parentNode.appendChild(node);
|
||
document.body.appendChild(parentNode);
|
||
|
||
// Remember width with no applied web font
|
||
var width = node.offsetWidth;
|
||
node.style.fontFamily = trimFontOptions(font) + ', ' + family;
|
||
return { node: node, w: width, parent: parentNode };
|
||
}
|
||
|
||
function checkLoadedFonts() {
|
||
var i;
|
||
var len = this.fonts.length;
|
||
var node;
|
||
var w;
|
||
var loadedCount = len;
|
||
for (i = 0; i < len; i += 1) {
|
||
if (this.fonts[i].loaded) {
|
||
loadedCount -= 1;
|
||
} else if (this.fonts[i].fOrigin === 'n' || this.fonts[i].origin === 0) {
|
||
this.fonts[i].loaded = true;
|
||
} else {
|
||
node = this.fonts[i].monoCase.node;
|
||
w = this.fonts[i].monoCase.w;
|
||
if (node.offsetWidth !== w) {
|
||
loadedCount -= 1;
|
||
this.fonts[i].loaded = true;
|
||
} else {
|
||
node = this.fonts[i].sansCase.node;
|
||
w = this.fonts[i].sansCase.w;
|
||
if (node.offsetWidth !== w) {
|
||
loadedCount -= 1;
|
||
this.fonts[i].loaded = true;
|
||
}
|
||
}
|
||
if (this.fonts[i].loaded) {
|
||
this.fonts[i].sansCase.parent.parentNode.removeChild(this.fonts[i].sansCase.parent);
|
||
this.fonts[i].monoCase.parent.parentNode.removeChild(this.fonts[i].monoCase.parent);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (loadedCount !== 0 && Date.now() - this.initTime < maxWaitingTime) {
|
||
setTimeout(this.checkLoadedFontsBinded, 20);
|
||
} else {
|
||
setTimeout(this.setIsLoadedBinded, 10);
|
||
}
|
||
}
|
||
|
||
function createHelper(fontData, def) {
|
||
var engine = (document.body && def) ? 'svg' : 'canvas';
|
||
var helper;
|
||
var fontProps = getFontProperties(fontData);
|
||
if (engine === 'svg') {
|
||
var tHelper = createNS('text');
|
||
tHelper.style.fontSize = '100px';
|
||
// tHelper.style.fontFamily = fontData.fFamily;
|
||
tHelper.setAttribute('font-family', fontData.fFamily);
|
||
tHelper.setAttribute('font-style', fontProps.style);
|
||
tHelper.setAttribute('font-weight', fontProps.weight);
|
||
tHelper.textContent = '1';
|
||
if (fontData.fClass) {
|
||
tHelper.style.fontFamily = 'inherit';
|
||
tHelper.setAttribute('class', fontData.fClass);
|
||
} else {
|
||
tHelper.style.fontFamily = fontData.fFamily;
|
||
}
|
||
def.appendChild(tHelper);
|
||
helper = tHelper;
|
||
} else {
|
||
var tCanvasHelper = new OffscreenCanvas(500, 500).getContext('2d');
|
||
tCanvasHelper.font = fontProps.style + ' ' + fontProps.weight + ' 100px ' + fontData.fFamily;
|
||
helper = tCanvasHelper;
|
||
}
|
||
function measure(text) {
|
||
if (engine === 'svg') {
|
||
helper.textContent = text;
|
||
return helper.getComputedTextLength();
|
||
}
|
||
return helper.measureText(text).width;
|
||
}
|
||
return {
|
||
measureText: measure,
|
||
};
|
||
}
|
||
|
||
function addFonts(fontData, defs) {
|
||
if (!fontData) {
|
||
this.isLoaded = true;
|
||
return;
|
||
}
|
||
if (this.chars) {
|
||
this.isLoaded = true;
|
||
this.fonts = fontData.list;
|
||
return;
|
||
}
|
||
if (!document.body) {
|
||
this.isLoaded = true;
|
||
fontData.list.forEach((data) => {
|
||
data.helper = createHelper(data);
|
||
data.cache = {};
|
||
});
|
||
this.fonts = fontData.list;
|
||
return;
|
||
}
|
||
|
||
var fontArr = fontData.list;
|
||
var i;
|
||
var len = fontArr.length;
|
||
var _pendingFonts = len;
|
||
for (i = 0; i < len; i += 1) {
|
||
var shouldLoadFont = true;
|
||
var loadedSelector;
|
||
var j;
|
||
fontArr[i].loaded = false;
|
||
fontArr[i].monoCase = setUpNode(fontArr[i].fFamily, 'monospace');
|
||
fontArr[i].sansCase = setUpNode(fontArr[i].fFamily, 'sans-serif');
|
||
if (!fontArr[i].fPath) {
|
||
fontArr[i].loaded = true;
|
||
_pendingFonts -= 1;
|
||
} else if (fontArr[i].fOrigin === 'p' || fontArr[i].origin === 3) {
|
||
loadedSelector = document.querySelectorAll('style[f-forigin="p"][f-family="' + fontArr[i].fFamily + '"], style[f-origin="3"][f-family="' + fontArr[i].fFamily + '"]');
|
||
|
||
if (loadedSelector.length > 0) {
|
||
shouldLoadFont = false;
|
||
}
|
||
|
||
if (shouldLoadFont) {
|
||
var s = createTag('style');
|
||
s.setAttribute('f-forigin', fontArr[i].fOrigin);
|
||
s.setAttribute('f-origin', fontArr[i].origin);
|
||
s.setAttribute('f-family', fontArr[i].fFamily);
|
||
s.type = 'text/css';
|
||
s.innerText = '@font-face {font-family: ' + fontArr[i].fFamily + "; font-style: normal; src: url('" + fontArr[i].fPath + "');}";
|
||
defs.appendChild(s);
|
||
}
|
||
} else if (fontArr[i].fOrigin === 'g' || fontArr[i].origin === 1) {
|
||
loadedSelector = document.querySelectorAll('link[f-forigin="g"], link[f-origin="1"]');
|
||
|
||
for (j = 0; j < loadedSelector.length; j += 1) {
|
||
if (loadedSelector[j].href.indexOf(fontArr[i].fPath) !== -1) {
|
||
// Font is already loaded
|
||
shouldLoadFont = false;
|
||
}
|
||
}
|
||
|
||
if (shouldLoadFont) {
|
||
var l = createTag('link');
|
||
l.setAttribute('f-forigin', fontArr[i].fOrigin);
|
||
l.setAttribute('f-origin', fontArr[i].origin);
|
||
l.type = 'text/css';
|
||
l.rel = 'stylesheet';
|
||
l.href = fontArr[i].fPath;
|
||
document.body.appendChild(l);
|
||
}
|
||
} else if (fontArr[i].fOrigin === 't' || fontArr[i].origin === 2) {
|
||
loadedSelector = document.querySelectorAll('script[f-forigin="t"], script[f-origin="2"]');
|
||
|
||
for (j = 0; j < loadedSelector.length; j += 1) {
|
||
if (fontArr[i].fPath === loadedSelector[j].src) {
|
||
// Font is already loaded
|
||
shouldLoadFont = false;
|
||
}
|
||
}
|
||
|
||
if (shouldLoadFont) {
|
||
var sc = createTag('link');
|
||
sc.setAttribute('f-forigin', fontArr[i].fOrigin);
|
||
sc.setAttribute('f-origin', fontArr[i].origin);
|
||
sc.setAttribute('rel', 'stylesheet');
|
||
sc.setAttribute('href', fontArr[i].fPath);
|
||
defs.appendChild(sc);
|
||
}
|
||
}
|
||
fontArr[i].helper = createHelper(fontArr[i], defs);
|
||
fontArr[i].cache = {};
|
||
this.fonts.push(fontArr[i]);
|
||
}
|
||
if (_pendingFonts === 0) {
|
||
this.isLoaded = true;
|
||
} else {
|
||
// On some cases even if the font is loaded, it won't load correctly when measuring text on canvas.
|
||
// Adding this timeout seems to fix it
|
||
setTimeout(this.checkLoadedFonts.bind(this), 100);
|
||
}
|
||
}
|
||
|
||
function addChars(chars) {
|
||
if (!chars) {
|
||
return;
|
||
}
|
||
if (!this.chars) {
|
||
this.chars = [];
|
||
}
|
||
var i;
|
||
var len = chars.length;
|
||
var j;
|
||
var jLen = this.chars.length;
|
||
var found;
|
||
for (i = 0; i < len; i += 1) {
|
||
j = 0;
|
||
found = false;
|
||
while (j < jLen) {
|
||
if (this.chars[j].style === chars[i].style && this.chars[j].fFamily === chars[i].fFamily && this.chars[j].ch === chars[i].ch) {
|
||
found = true;
|
||
}
|
||
j += 1;
|
||
}
|
||
if (!found) {
|
||
this.chars.push(chars[i]);
|
||
jLen += 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
function getCharData(char, style, font) {
|
||
var i = 0;
|
||
var len = this.chars.length;
|
||
while (i < len) {
|
||
if (this.chars[i].ch === char && this.chars[i].style === style && this.chars[i].fFamily === font) {
|
||
return this.chars[i];
|
||
}
|
||
i += 1;
|
||
}
|
||
if (((typeof char === 'string' && char.charCodeAt(0) !== 13) || !char)
|
||
&& console
|
||
&& console.warn // eslint-disable-line no-console
|
||
&& !this._warned
|
||
) {
|
||
this._warned = true;
|
||
console.warn('Missing character from exported characters list: ', char, style, font); // eslint-disable-line no-console
|
||
}
|
||
return emptyChar;
|
||
}
|
||
|
||
function measureText(char, fontName, size) {
|
||
var fontData = this.getFontByName(fontName);
|
||
// Using the char instead of char.charCodeAt(0)
|
||
// to avoid collisions between equal chars
|
||
var index = char;
|
||
if (!fontData.cache[index]) {
|
||
var tHelper = fontData.helper;
|
||
if (char === ' ') {
|
||
var doubleSize = tHelper.measureText('|' + char + '|');
|
||
var singleSize = tHelper.measureText('||');
|
||
fontData.cache[index] = (doubleSize - singleSize) / 100;
|
||
} else {
|
||
fontData.cache[index] = tHelper.measureText(char) / 100;
|
||
}
|
||
}
|
||
return fontData.cache[index] * size;
|
||
}
|
||
|
||
function getFontByName(name) {
|
||
var i = 0;
|
||
var len = this.fonts.length;
|
||
while (i < len) {
|
||
if (this.fonts[i].fName === name) {
|
||
return this.fonts[i];
|
||
}
|
||
i += 1;
|
||
}
|
||
return this.fonts[0];
|
||
}
|
||
|
||
function getCodePoint(string) {
|
||
var codePoint = 0;
|
||
var first = string.charCodeAt(0);
|
||
if (first >= 0xD800 && first <= 0xDBFF) {
|
||
var second = string.charCodeAt(1);
|
||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||
codePoint = (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
|
||
}
|
||
}
|
||
return codePoint;
|
||
}
|
||
|
||
// Skin tone modifiers
|
||
function isModifier(firstCharCode, secondCharCode) {
|
||
var sum = firstCharCode.toString(16) + secondCharCode.toString(16);
|
||
return surrogateModifiers.indexOf(sum) !== -1;
|
||
}
|
||
|
||
function isZeroWidthJoiner(charCode) {
|
||
return charCode === ZERO_WIDTH_JOINER_CODE_POINT;
|
||
}
|
||
|
||
// This codepoint may change the appearance of the preceding character.
|
||
// If that is a symbol, dingbat or emoji, U+FE0F forces it to be rendered
|
||
// as a colorful image as compared to a monochrome text variant.
|
||
function isVariationSelector(charCode) {
|
||
return charCode === VARIATION_SELECTOR_16_CODE_POINT;
|
||
}
|
||
|
||
// The regional indicator symbols are a set of 26 alphabetic Unicode
|
||
/// characters (A–Z) intended to be used to encode ISO 3166-1 alpha-2
|
||
// two-letter country codes in a way that allows optional special treatment.
|
||
function isRegionalCode(string) {
|
||
var codePoint = getCodePoint(string);
|
||
if (codePoint >= REGIONAL_CHARACTER_A_CODE_POINT && codePoint <= REGIONAL_CHARACTER_Z_CODE_POINT) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Some Emoji implementations represent combinations of
|
||
// two “regional indicator” letters as a single flag symbol.
|
||
function isFlagEmoji(string) {
|
||
return isRegionalCode(string.substr(0, 2)) && isRegionalCode(string.substr(2, 2));
|
||
}
|
||
|
||
function isCombinedCharacter(char) {
|
||
return combinedCharacters.indexOf(char) !== -1;
|
||
}
|
||
|
||
// Regional flags start with a BLACK_FLAG_CODE_POINT
|
||
// folowed by 5 chars in the TAG range
|
||
// and end with a CANCEL_TAG_CODE_POINT
|
||
function isRegionalFlag(text, index) {
|
||
var codePoint = getCodePoint(text.substr(index, 2));
|
||
if (codePoint !== BLACK_FLAG_CODE_POINT) {
|
||
return false;
|
||
}
|
||
var count = 0;
|
||
index += 2;
|
||
while (count < 5) {
|
||
codePoint = getCodePoint(text.substr(index, 2));
|
||
if (codePoint < A_TAG_CODE_POINT || codePoint > Z_TAG_CODE_POINT) {
|
||
return false;
|
||
}
|
||
count += 1;
|
||
index += 2;
|
||
}
|
||
return getCodePoint(text.substr(index, 2)) === CANCEL_TAG_CODE_POINT;
|
||
}
|
||
|
||
function setIsLoaded() {
|
||
this.isLoaded = true;
|
||
}
|
||
|
||
var Font = function () {
|
||
this.fonts = [];
|
||
this.chars = null;
|
||
this.typekitLoaded = 0;
|
||
this.isLoaded = false;
|
||
this._warned = false;
|
||
this.initTime = Date.now();
|
||
this.setIsLoadedBinded = this.setIsLoaded.bind(this);
|
||
this.checkLoadedFontsBinded = this.checkLoadedFonts.bind(this);
|
||
};
|
||
Font.isModifier = isModifier;
|
||
Font.isZeroWidthJoiner = isZeroWidthJoiner;
|
||
Font.isFlagEmoji = isFlagEmoji;
|
||
Font.isRegionalCode = isRegionalCode;
|
||
Font.isCombinedCharacter = isCombinedCharacter;
|
||
Font.isRegionalFlag = isRegionalFlag;
|
||
Font.isVariationSelector = isVariationSelector;
|
||
Font.BLACK_FLAG_CODE_POINT = BLACK_FLAG_CODE_POINT;
|
||
|
||
var fontPrototype = {
|
||
addChars: addChars,
|
||
addFonts: addFonts,
|
||
getCharData: getCharData,
|
||
getFontByName: getFontByName,
|
||
measureText: measureText,
|
||
checkLoadedFonts: checkLoadedFonts,
|
||
setIsLoaded: setIsLoaded,
|
||
};
|
||
|
||
Font.prototype = fontPrototype;
|
||
|
||
return Font;
|
||
}());
|
||
|
||
export default FontManager;
|