FontManager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import createNS from './helpers/svg_elements';
  2. import createTag from './helpers/html_elements';
  3. import getFontProperties from './getFontProperties';
  4. const FontManager = (function () {
  5. var maxWaitingTime = 5000;
  6. var emptyChar = {
  7. w: 0,
  8. size: 0,
  9. shapes: [],
  10. data: {
  11. shapes: [],
  12. },
  13. };
  14. var combinedCharacters = [];
  15. // Hindi characters
  16. combinedCharacters = combinedCharacters.concat([2304, 2305, 2306, 2307, 2362, 2363, 2364, 2364, 2366,
  17. 2367, 2368, 2369, 2370, 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2379,
  18. 2380, 2381, 2382, 2383, 2387, 2388, 2389, 2390, 2391, 2402, 2403]);
  19. var BLACK_FLAG_CODE_POINT = 127988;
  20. var CANCEL_TAG_CODE_POINT = 917631;
  21. var A_TAG_CODE_POINT = 917601;
  22. var Z_TAG_CODE_POINT = 917626;
  23. var VARIATION_SELECTOR_16_CODE_POINT = 65039;
  24. var ZERO_WIDTH_JOINER_CODE_POINT = 8205;
  25. var REGIONAL_CHARACTER_A_CODE_POINT = 127462;
  26. var REGIONAL_CHARACTER_Z_CODE_POINT = 127487;
  27. var surrogateModifiers = [
  28. 'd83cdffb',
  29. 'd83cdffc',
  30. 'd83cdffd',
  31. 'd83cdffe',
  32. 'd83cdfff',
  33. ];
  34. function trimFontOptions(font) {
  35. var familyArray = font.split(',');
  36. var i;
  37. var len = familyArray.length;
  38. var enabledFamilies = [];
  39. for (i = 0; i < len; i += 1) {
  40. if (familyArray[i] !== 'sans-serif' && familyArray[i] !== 'monospace') {
  41. enabledFamilies.push(familyArray[i]);
  42. }
  43. }
  44. return enabledFamilies.join(',');
  45. }
  46. function setUpNode(font, family) {
  47. var parentNode = createTag('span');
  48. // Node is invisible to screen readers.
  49. parentNode.setAttribute('aria-hidden', true);
  50. parentNode.style.fontFamily = family;
  51. var node = createTag('span');
  52. // Characters that vary significantly among different fonts
  53. node.innerText = 'giItT1WQy@!-/#';
  54. // Visible - so we can measure it - but not on the screen
  55. parentNode.style.position = 'absolute';
  56. parentNode.style.left = '-10000px';
  57. parentNode.style.top = '-10000px';
  58. // Large font size makes even subtle changes obvious
  59. parentNode.style.fontSize = '300px';
  60. // Reset any font properties
  61. parentNode.style.fontVariant = 'normal';
  62. parentNode.style.fontStyle = 'normal';
  63. parentNode.style.fontWeight = 'normal';
  64. parentNode.style.letterSpacing = '0';
  65. parentNode.appendChild(node);
  66. document.body.appendChild(parentNode);
  67. // Remember width with no applied web font
  68. var width = node.offsetWidth;
  69. node.style.fontFamily = trimFontOptions(font) + ', ' + family;
  70. return { node: node, w: width, parent: parentNode };
  71. }
  72. function checkLoadedFonts() {
  73. var i;
  74. var len = this.fonts.length;
  75. var node;
  76. var w;
  77. var loadedCount = len;
  78. for (i = 0; i < len; i += 1) {
  79. if (this.fonts[i].loaded) {
  80. loadedCount -= 1;
  81. } else if (this.fonts[i].fOrigin === 'n' || this.fonts[i].origin === 0) {
  82. this.fonts[i].loaded = true;
  83. } else {
  84. node = this.fonts[i].monoCase.node;
  85. w = this.fonts[i].monoCase.w;
  86. if (node.offsetWidth !== w) {
  87. loadedCount -= 1;
  88. this.fonts[i].loaded = true;
  89. } else {
  90. node = this.fonts[i].sansCase.node;
  91. w = this.fonts[i].sansCase.w;
  92. if (node.offsetWidth !== w) {
  93. loadedCount -= 1;
  94. this.fonts[i].loaded = true;
  95. }
  96. }
  97. if (this.fonts[i].loaded) {
  98. this.fonts[i].sansCase.parent.parentNode.removeChild(this.fonts[i].sansCase.parent);
  99. this.fonts[i].monoCase.parent.parentNode.removeChild(this.fonts[i].monoCase.parent);
  100. }
  101. }
  102. }
  103. if (loadedCount !== 0 && Date.now() - this.initTime < maxWaitingTime) {
  104. setTimeout(this.checkLoadedFontsBinded, 20);
  105. } else {
  106. setTimeout(this.setIsLoadedBinded, 10);
  107. }
  108. }
  109. function createHelper(fontData, def) {
  110. var engine = (document.body && def) ? 'svg' : 'canvas';
  111. var helper;
  112. var fontProps = getFontProperties(fontData);
  113. if (engine === 'svg') {
  114. var tHelper = createNS('text');
  115. tHelper.style.fontSize = '100px';
  116. // tHelper.style.fontFamily = fontData.fFamily;
  117. tHelper.setAttribute('font-family', fontData.fFamily);
  118. tHelper.setAttribute('font-style', fontProps.style);
  119. tHelper.setAttribute('font-weight', fontProps.weight);
  120. tHelper.textContent = '1';
  121. if (fontData.fClass) {
  122. tHelper.style.fontFamily = 'inherit';
  123. tHelper.setAttribute('class', fontData.fClass);
  124. } else {
  125. tHelper.style.fontFamily = fontData.fFamily;
  126. }
  127. def.appendChild(tHelper);
  128. helper = tHelper;
  129. } else {
  130. var tCanvasHelper = new OffscreenCanvas(500, 500).getContext('2d');
  131. tCanvasHelper.font = fontProps.style + ' ' + fontProps.weight + ' 100px ' + fontData.fFamily;
  132. helper = tCanvasHelper;
  133. }
  134. function measure(text) {
  135. if (engine === 'svg') {
  136. helper.textContent = text;
  137. return helper.getComputedTextLength();
  138. }
  139. return helper.measureText(text).width;
  140. }
  141. return {
  142. measureText: measure,
  143. };
  144. }
  145. function addFonts(fontData, defs) {
  146. if (!fontData) {
  147. this.isLoaded = true;
  148. return;
  149. }
  150. if (this.chars) {
  151. this.isLoaded = true;
  152. this.fonts = fontData.list;
  153. return;
  154. }
  155. if (!document.body) {
  156. this.isLoaded = true;
  157. fontData.list.forEach((data) => {
  158. data.helper = createHelper(data);
  159. data.cache = {};
  160. });
  161. this.fonts = fontData.list;
  162. return;
  163. }
  164. var fontArr = fontData.list;
  165. var i;
  166. var len = fontArr.length;
  167. var _pendingFonts = len;
  168. for (i = 0; i < len; i += 1) {
  169. var shouldLoadFont = true;
  170. var loadedSelector;
  171. var j;
  172. fontArr[i].loaded = false;
  173. fontArr[i].monoCase = setUpNode(fontArr[i].fFamily, 'monospace');
  174. fontArr[i].sansCase = setUpNode(fontArr[i].fFamily, 'sans-serif');
  175. if (!fontArr[i].fPath) {
  176. fontArr[i].loaded = true;
  177. _pendingFonts -= 1;
  178. } else if (fontArr[i].fOrigin === 'p' || fontArr[i].origin === 3) {
  179. loadedSelector = document.querySelectorAll('style[f-forigin="p"][f-family="' + fontArr[i].fFamily + '"], style[f-origin="3"][f-family="' + fontArr[i].fFamily + '"]');
  180. if (loadedSelector.length > 0) {
  181. shouldLoadFont = false;
  182. }
  183. if (shouldLoadFont) {
  184. var s = createTag('style');
  185. s.setAttribute('f-forigin', fontArr[i].fOrigin);
  186. s.setAttribute('f-origin', fontArr[i].origin);
  187. s.setAttribute('f-family', fontArr[i].fFamily);
  188. s.type = 'text/css';
  189. s.innerText = '@font-face {font-family: ' + fontArr[i].fFamily + "; font-style: normal; src: url('" + fontArr[i].fPath + "');}";
  190. defs.appendChild(s);
  191. }
  192. } else if (fontArr[i].fOrigin === 'g' || fontArr[i].origin === 1) {
  193. loadedSelector = document.querySelectorAll('link[f-forigin="g"], link[f-origin="1"]');
  194. for (j = 0; j < loadedSelector.length; j += 1) {
  195. if (loadedSelector[j].href.indexOf(fontArr[i].fPath) !== -1) {
  196. // Font is already loaded
  197. shouldLoadFont = false;
  198. }
  199. }
  200. if (shouldLoadFont) {
  201. var l = createTag('link');
  202. l.setAttribute('f-forigin', fontArr[i].fOrigin);
  203. l.setAttribute('f-origin', fontArr[i].origin);
  204. l.type = 'text/css';
  205. l.rel = 'stylesheet';
  206. l.href = fontArr[i].fPath;
  207. document.body.appendChild(l);
  208. }
  209. } else if (fontArr[i].fOrigin === 't' || fontArr[i].origin === 2) {
  210. loadedSelector = document.querySelectorAll('script[f-forigin="t"], script[f-origin="2"]');
  211. for (j = 0; j < loadedSelector.length; j += 1) {
  212. if (fontArr[i].fPath === loadedSelector[j].src) {
  213. // Font is already loaded
  214. shouldLoadFont = false;
  215. }
  216. }
  217. if (shouldLoadFont) {
  218. var sc = createTag('link');
  219. sc.setAttribute('f-forigin', fontArr[i].fOrigin);
  220. sc.setAttribute('f-origin', fontArr[i].origin);
  221. sc.setAttribute('rel', 'stylesheet');
  222. sc.setAttribute('href', fontArr[i].fPath);
  223. defs.appendChild(sc);
  224. }
  225. }
  226. fontArr[i].helper = createHelper(fontArr[i], defs);
  227. fontArr[i].cache = {};
  228. this.fonts.push(fontArr[i]);
  229. }
  230. if (_pendingFonts === 0) {
  231. this.isLoaded = true;
  232. } else {
  233. // On some cases even if the font is loaded, it won't load correctly when measuring text on canvas.
  234. // Adding this timeout seems to fix it
  235. setTimeout(this.checkLoadedFonts.bind(this), 100);
  236. }
  237. }
  238. function addChars(chars) {
  239. if (!chars) {
  240. return;
  241. }
  242. if (!this.chars) {
  243. this.chars = [];
  244. }
  245. var i;
  246. var len = chars.length;
  247. var j;
  248. var jLen = this.chars.length;
  249. var found;
  250. for (i = 0; i < len; i += 1) {
  251. j = 0;
  252. found = false;
  253. while (j < jLen) {
  254. if (this.chars[j].style === chars[i].style && this.chars[j].fFamily === chars[i].fFamily && this.chars[j].ch === chars[i].ch) {
  255. found = true;
  256. }
  257. j += 1;
  258. }
  259. if (!found) {
  260. this.chars.push(chars[i]);
  261. jLen += 1;
  262. }
  263. }
  264. }
  265. function getCharData(char, style, font) {
  266. var i = 0;
  267. var len = this.chars.length;
  268. while (i < len) {
  269. if (this.chars[i].ch === char && this.chars[i].style === style && this.chars[i].fFamily === font) {
  270. return this.chars[i];
  271. }
  272. i += 1;
  273. }
  274. if (((typeof char === 'string' && char.charCodeAt(0) !== 13) || !char)
  275. && console
  276. && console.warn // eslint-disable-line no-console
  277. && !this._warned
  278. ) {
  279. this._warned = true;
  280. console.warn('Missing character from exported characters list: ', char, style, font); // eslint-disable-line no-console
  281. }
  282. return emptyChar;
  283. }
  284. function measureText(char, fontName, size) {
  285. var fontData = this.getFontByName(fontName);
  286. // Using the char instead of char.charCodeAt(0)
  287. // to avoid collisions between equal chars
  288. var index = char;
  289. if (!fontData.cache[index]) {
  290. var tHelper = fontData.helper;
  291. if (char === ' ') {
  292. var doubleSize = tHelper.measureText('|' + char + '|');
  293. var singleSize = tHelper.measureText('||');
  294. fontData.cache[index] = (doubleSize - singleSize) / 100;
  295. } else {
  296. fontData.cache[index] = tHelper.measureText(char) / 100;
  297. }
  298. }
  299. return fontData.cache[index] * size;
  300. }
  301. function getFontByName(name) {
  302. var i = 0;
  303. var len = this.fonts.length;
  304. while (i < len) {
  305. if (this.fonts[i].fName === name) {
  306. return this.fonts[i];
  307. }
  308. i += 1;
  309. }
  310. return this.fonts[0];
  311. }
  312. function getCodePoint(string) {
  313. var codePoint = 0;
  314. var first = string.charCodeAt(0);
  315. if (first >= 0xD800 && first <= 0xDBFF) {
  316. var second = string.charCodeAt(1);
  317. if (second >= 0xDC00 && second <= 0xDFFF) {
  318. codePoint = (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
  319. }
  320. }
  321. return codePoint;
  322. }
  323. // Skin tone modifiers
  324. function isModifier(firstCharCode, secondCharCode) {
  325. var sum = firstCharCode.toString(16) + secondCharCode.toString(16);
  326. return surrogateModifiers.indexOf(sum) !== -1;
  327. }
  328. function isZeroWidthJoiner(charCode) {
  329. return charCode === ZERO_WIDTH_JOINER_CODE_POINT;
  330. }
  331. // This codepoint may change the appearance of the preceding character.
  332. // If that is a symbol, dingbat or emoji, U+FE0F forces it to be rendered
  333. // as a colorful image as compared to a monochrome text variant.
  334. function isVariationSelector(charCode) {
  335. return charCode === VARIATION_SELECTOR_16_CODE_POINT;
  336. }
  337. // The regional indicator symbols are a set of 26 alphabetic Unicode
  338. /// characters (A–Z) intended to be used to encode ISO 3166-1 alpha-2
  339. // two-letter country codes in a way that allows optional special treatment.
  340. function isRegionalCode(string) {
  341. var codePoint = getCodePoint(string);
  342. if (codePoint >= REGIONAL_CHARACTER_A_CODE_POINT && codePoint <= REGIONAL_CHARACTER_Z_CODE_POINT) {
  343. return true;
  344. }
  345. return false;
  346. }
  347. // Some Emoji implementations represent combinations of
  348. // two “regional indicator” letters as a single flag symbol.
  349. function isFlagEmoji(string) {
  350. return isRegionalCode(string.substr(0, 2)) && isRegionalCode(string.substr(2, 2));
  351. }
  352. function isCombinedCharacter(char) {
  353. return combinedCharacters.indexOf(char) !== -1;
  354. }
  355. // Regional flags start with a BLACK_FLAG_CODE_POINT
  356. // folowed by 5 chars in the TAG range
  357. // and end with a CANCEL_TAG_CODE_POINT
  358. function isRegionalFlag(text, index) {
  359. var codePoint = getCodePoint(text.substr(index, 2));
  360. if (codePoint !== BLACK_FLAG_CODE_POINT) {
  361. return false;
  362. }
  363. var count = 0;
  364. index += 2;
  365. while (count < 5) {
  366. codePoint = getCodePoint(text.substr(index, 2));
  367. if (codePoint < A_TAG_CODE_POINT || codePoint > Z_TAG_CODE_POINT) {
  368. return false;
  369. }
  370. count += 1;
  371. index += 2;
  372. }
  373. return getCodePoint(text.substr(index, 2)) === CANCEL_TAG_CODE_POINT;
  374. }
  375. function setIsLoaded() {
  376. this.isLoaded = true;
  377. }
  378. var Font = function () {
  379. this.fonts = [];
  380. this.chars = null;
  381. this.typekitLoaded = 0;
  382. this.isLoaded = false;
  383. this._warned = false;
  384. this.initTime = Date.now();
  385. this.setIsLoadedBinded = this.setIsLoaded.bind(this);
  386. this.checkLoadedFontsBinded = this.checkLoadedFonts.bind(this);
  387. };
  388. Font.isModifier = isModifier;
  389. Font.isZeroWidthJoiner = isZeroWidthJoiner;
  390. Font.isFlagEmoji = isFlagEmoji;
  391. Font.isRegionalCode = isRegionalCode;
  392. Font.isCombinedCharacter = isCombinedCharacter;
  393. Font.isRegionalFlag = isRegionalFlag;
  394. Font.isVariationSelector = isVariationSelector;
  395. Font.BLACK_FLAG_CODE_POINT = BLACK_FLAG_CODE_POINT;
  396. var fontPrototype = {
  397. addChars: addChars,
  398. addFonts: addFonts,
  399. getCharData: getCharData,
  400. getFontByName: getFontByName,
  401. measureText: measureText,
  402. checkLoadedFonts: checkLoadedFonts,
  403. setIsLoaded: setIsLoaded,
  404. };
  405. Font.prototype = fontPrototype;
  406. return Font;
  407. }());
  408. export default FontManager;