From caf90ff6eeb902ebb93848aea8c248928157ec24 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 16 Aug 2018 20:34:31 +0200 Subject: [PATCH 1/6] Convert `FontFaceObject` to an ES6 class Also changes `var` to `let`/`const` in code already touched in the patch, and makes use of template strings in a few spots. --- src/display/font_loader.js | 176 +++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 94 deletions(-) diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 1e73925d9..5556bc708 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -330,21 +330,21 @@ if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL || CHROME')) { }); } -var IsEvalSupportedCached = { +const IsEvalSupportedCached = { get value() { return shadow(this, 'value', isEvalSupported()); }, }; -var FontFaceObject = (function FontFaceObjectClosure() { - function FontFaceObject(translatedData, { isEvalSupported = true, - disableFontFace = false, - ignoreErrors = false, - onUnsupportedFeature = null, - fontRegistry = null, }) { +class FontFaceObject { + constructor(translatedData, { isEvalSupported = true, + disableFontFace = false, + ignoreErrors = false, + onUnsupportedFeature = null, + fontRegistry = null, }) { this.compiledGlyphs = Object.create(null); // importing translated data - for (var i in translatedData) { + for (let i in translatedData) { this[i] = translatedData[i]; } this.isEvalSupported = isEvalSupported !== false; @@ -353,98 +353,86 @@ var FontFaceObject = (function FontFaceObjectClosure() { this._onUnsupportedFeature = onUnsupportedFeature; this.fontRegistry = fontRegistry; } - FontFaceObject.prototype = { - createNativeFontFace: function FontFaceObject_createNativeFontFace() { - if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) { - throw new Error('Not implemented: createNativeFontFace'); + + createNativeFontFace() { + if (!this.data || this.disableFontFace) { + return null; + } + const nativeFontFace = new FontFace(this.loadedName, this.data, {}); + + if (this.fontRegistry) { + this.fontRegistry.registerFont(this); + } + return nativeFontFace; + } + + createFontFaceRule() { + if (!this.data || this.disableFontFace) { + return null; + } + const data = bytesToString(new Uint8Array(this.data)); + // Add the @font-face rule to the document. + const url = `url(data:${this.mimetype};base64,${btoa(data)});`; + const rule = `@font-face {font-family:"${this.loadedName}";src:${url}}`; + + if (this.fontRegistry) { + this.fontRegistry.registerFont(this, url); + } + return rule; + } + + getPathGenerator(objs, character) { + if (this.compiledGlyphs[character] !== undefined) { + return this.compiledGlyphs[character]; + } + + let cmds, current; + try { + cmds = objs.get(this.loadedName + '_path_' + character); + } catch (ex) { + if (!this.ignoreErrors) { + throw ex; } - - if (!this.data || this.disableFontFace) { - return null; + if (this._onUnsupportedFeature) { + this._onUnsupportedFeature({ featureId: UNSUPPORTED_FEATURES.font, }); } + warn(`getPathGenerator - ignoring character: "${ex}".`); - var nativeFontFace = new FontFace(this.loadedName, this.data, {}); - - if (this.fontRegistry) { - this.fontRegistry.registerFont(this); - } - return nativeFontFace; - }, - - createFontFaceRule: function FontFaceObject_createFontFaceRule() { - if (!this.data || this.disableFontFace) { - return null; - } - - var data = bytesToString(new Uint8Array(this.data)); - var fontName = this.loadedName; - - // Add the font-face rule to the document - var url = ('url(data:' + this.mimetype + ';base64,' + btoa(data) + ');'); - var rule = '@font-face { font-family:"' + fontName + '";src:' + url + '}'; - - if (this.fontRegistry) { - this.fontRegistry.registerFont(this, url); - } - - return rule; - }, - - getPathGenerator(objs, character) { - if (this.compiledGlyphs[character] !== undefined) { - return this.compiledGlyphs[character]; - } - - let cmds, current; - try { - cmds = objs.get(this.loadedName + '_path_' + character); - } catch (ex) { - if (!this.ignoreErrors) { - throw ex; - } - if (this._onUnsupportedFeature) { - this._onUnsupportedFeature({ featureId: UNSUPPORTED_FEATURES.font, }); - } - warn(`getPathGenerator - ignoring character: "${ex}".`); - - return this.compiledGlyphs[character] = function(c, size) { - // No-op function, to allow rendering to continue. - }; - } - - // If we can, compile cmds into JS for MAXIMUM SPEED... - if (this.isEvalSupported && IsEvalSupportedCached.value) { - let args, js = ''; - for (let i = 0, ii = cmds.length; i < ii; i++) { - current = cmds[i]; - - if (current.args !== undefined) { - args = current.args.join(','); - } else { - args = ''; - } - js += 'c.' + current.cmd + '(' + args + ');\n'; - } - // eslint-disable-next-line no-new-func - return this.compiledGlyphs[character] = new Function('c', 'size', js); - } - // ... but fall back on using Function.prototype.apply() if we're - // blocked from using eval() for whatever reason (like CSP policies). return this.compiledGlyphs[character] = function(c, size) { - for (let i = 0, ii = cmds.length; i < ii; i++) { - current = cmds[i]; - - if (current.cmd === 'scale') { - current.args = [size, -size]; - } - c[current.cmd].apply(c, current.args); - } + // No-op function, to allow rendering to continue. }; - }, - }; + } - return FontFaceObject; -})(); + // If we can, compile cmds into JS for MAXIMUM SPEED... + if (this.isEvalSupported && IsEvalSupportedCached.value) { + let args, js = ''; + for (let i = 0, ii = cmds.length; i < ii; i++) { + current = cmds[i]; + + if (current.args !== undefined) { + args = current.args.join(','); + } else { + args = ''; + } + js += 'c.' + current.cmd + '(' + args + ');\n'; + } + // eslint-disable-next-line no-new-func + return this.compiledGlyphs[character] = new Function('c', 'size', js); + } + // ... but fall back on using Function.prototype.apply() if we're + // blocked from using eval() for whatever reason (like CSP policies). + return this.compiledGlyphs[character] = function(c, size) { + for (let i = 0, ii = cmds.length; i < ii; i++) { + current = cmds[i]; + + if (current.cmd === 'scale') { + current.args = [size, -size]; + } + c[current.cmd].apply(c, current.args); + } + }; + } +} export { FontFaceObject, From ad3e937816144cf7cfdac6221620d8a98241db9d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 16 Aug 2018 20:34:37 +0200 Subject: [PATCH 2/6] Replace the `Font.loading` property with, the already existing, `Font.missingFile` property The `Font.loading` property is only ever used *once* in the code, whereas `Font.missingFile` is more widely used. Furthermore the name `loading` feels, at least to me, slight less clear than `missingFile`. Finally, note that these two properties are the inverse of each other. --- src/core/fonts.js | 5 +---- src/display/font_loader.js | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/core/fonts.js b/src/core/fonts.js index a5bfc019a..febbf21c7 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -589,8 +589,6 @@ var Font = (function FontClosure() { this.toUnicode = properties.toUnicode; this.encoding = properties.baseEncoding; this.seacMap = properties.seacMap; - - this.loading = true; } Font.getFontID = (function () { @@ -1261,7 +1259,6 @@ var Font = (function FontClosure() { }); } this.loadedName = fontName.split('-')[0]; - this.loading = false; this.fontType = getFontType(type, subtype); }, @@ -2944,7 +2941,7 @@ var ErrorFont = (function ErrorFontClosure() { function ErrorFont(error) { this.error = error; this.loadedName = 'g_font_error'; - this.loading = false; + this.missingFile = true; } ErrorFont.prototype = { diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 5556bc708..26e569ce7 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -120,7 +120,7 @@ if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { // Add the font to the DOM only once or skip if the font // is already loaded. - if (font.attached || font.loading === false) { + if (font.attached || font.missingFile) { continue; } font.attached = true; From 45d665197665013e6203469d574dedcb8353625d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 16 Aug 2018 20:34:45 +0200 Subject: [PATCH 3/6] Refactor unused `Date.now()` calls in `FontLoader.queueLoadingCallback` The `started` timestamp is completely usused, and the `end` timestamp is currently[1] being used essentially like a boolean value. Hence this code can be simplified to use an actual boolean value instead, which avoids potentially hundreds (or even thousands) of unnecessary `Date.now()` calls. --- [1] Looking briefly at the history of this code, I cannot tell if the timestamps themselves were ever used for anything (except for tracking "boolean" state). --- src/display/font_loader.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 26e569ce7..bed322135 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -156,11 +156,11 @@ if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { FontLoader.prototype.queueLoadingCallback = function FontLoader_queueLoadingCallback(callback) { function LoadLoader_completeRequest() { - assert(!request.end, 'completeRequest() cannot be called twice'); - request.end = Date.now(); + assert(!request.done, 'completeRequest() cannot be called twice.'); + request.done = true; - // sending all completed requests in order how they were queued - while (context.requests.length > 0 && context.requests[0].end) { + // Sending all completed requests in order of how they were queued. + while (context.requests.length > 0 && context.requests[0].done) { var otherRequest = context.requests.shift(); setTimeout(otherRequest.callback, 0); } @@ -170,9 +170,9 @@ if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { var requestId = 'pdfjs-font-loading-' + (context.nextRequestId++); var request = { id: requestId, + done: false, complete: LoadLoader_completeRequest, callback, - started: Date.now(), }; context.requests.push(request); return request; From 05b021bcce546bc8cad7e72000c236b14ccce5ec Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 16 Aug 2018 20:34:51 +0200 Subject: [PATCH 4/6] Refactor the `FontLoader` into proper, build-specific, ES6 classes Also changes `var` to `let`/`const` in code already touched in the patch, and makes use of template strings in a few spots. --- src/display/font_loader.js | 468 ++++++++++++++++++------------------- 1 file changed, 233 insertions(+), 235 deletions(-) diff --git a/src/display/font_loader.js b/src/display/font_loader.js index bed322135..6525e99d0 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -14,125 +14,86 @@ */ import { - assert, bytesToString, isEvalSupported, shadow, string32, + assert, bytesToString, isEvalSupported, shadow, string32, unreachable, UNSUPPORTED_FEATURES, warn } from '../shared/util'; -function FontLoader(docId) { - this.docId = docId; - this.styleElement = null; - if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { +class BaseFontLoader { + constructor(docId) { + if (this.constructor === BaseFontLoader) { + unreachable('Cannot initialize BaseFontLoader.'); + } + this.docId = docId; + this.nativeFontFaces = []; - this.loadTestFontId = 0; + this.styleElement = null; this.loadingContext = { requests: [], nextRequestId: 0, }; } -} -FontLoader.prototype = { - insertRule: function fontLoaderInsertRule(rule) { - var styleElement = this.styleElement; + + addNativeFontFace(nativeFontFace) { + this.nativeFontFaces.push(nativeFontFace); + document.fonts.add(nativeFontFace); + } + + insertRule(rule) { + let styleElement = this.styleElement; if (!styleElement) { styleElement = this.styleElement = document.createElement('style'); - styleElement.id = 'PDFJS_FONT_STYLE_TAG_' + this.docId; + styleElement.id = `PDFJS_FONT_STYLE_TAG_${this.docId}`; document.documentElement.getElementsByTagName('head')[0].appendChild( styleElement); } - var styleSheet = styleElement.sheet; + const styleSheet = styleElement.sheet; styleSheet.insertRule(rule, styleSheet.cssRules.length); - }, + } + + clear() { + this.nativeFontFaces.forEach(function(nativeFontFace) { + document.fonts.delete(nativeFontFace); + }); + this.nativeFontFaces.length = 0; - clear: function fontLoaderClear() { if (this.styleElement) { // Note: ChildNode.remove doesn't throw if the parentNode is undefined. this.styleElement.remove(); this.styleElement = null; } - if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { - this.nativeFontFaces.forEach(function(nativeFontFace) { - document.fonts.delete(nativeFontFace); - }); - this.nativeFontFaces.length = 0; - } - }, -}; + } -if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { - var getLoadTestFont = function () { - // This is a CFF font with 1 glyph for '.' that fills its entire width and - // height. - return atob( - 'T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQ' + - 'AABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwA' + - 'AAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbm' + - 'FtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAA' + - 'AADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6A' + - 'ABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAA' + - 'MQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAA' + - 'AAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAA' + - 'AAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQ' + - 'AAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMA' + - 'AQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAA' + - 'EAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAA' + - 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAA' + - 'AAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgc' + - 'A/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF' + - 'hYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQA' + - 'AAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAg' + - 'ABAAAAAAAAAAAD6AAAAAAAAA=='); - }; - Object.defineProperty(FontLoader.prototype, 'loadTestFont', { - get() { - return shadow(this, 'loadTestFont', getLoadTestFont()); - }, - configurable: true, - }); - - FontLoader.prototype.addNativeFontFace = - function fontLoader_addNativeFontFace(nativeFontFace) { - this.nativeFontFaces.push(nativeFontFace); - document.fonts.add(nativeFontFace); - }; - - FontLoader.prototype.bind = function fontLoaderBind(fonts, callback) { - var rules = []; - var fontsToLoad = []; - var fontLoadPromises = []; - var getNativeFontPromise = function(nativeFontFace) { + bind(fonts, callback) { + const rules = []; + const fontsToLoad = []; + const fontLoadPromises = []; + const getNativeFontPromise = function(nativeFontFace) { // Return a promise that is always fulfilled, even when the font fails to // load. - return nativeFontFace.loaded.catch(function(e) { - warn('Failed to load font "' + nativeFontFace.family + '": ' + e); + return nativeFontFace.loaded.catch(function(reason) { + warn(`Failed to load font "${nativeFontFace.family}": ${reason}`); }); }; // Firefox Font Loading API does not work with mozPrintCallback -- // disabling it in this case. - var isFontLoadingAPISupported = FontLoader.isFontLoadingAPISupported && - !FontLoader.isSyncFontLoadingSupported; - for (var i = 0, ii = fonts.length; i < ii; i++) { - var font = fonts[i]; - - // Add the font to the DOM only once or skip if the font - // is already loaded. + const isFontLoadingAPISupported = this.isFontLoadingAPISupported && + !this.isSyncFontLoadingSupported; + for (const font of fonts) { + // Add the font to the DOM only once; skip if the font is already loaded. if (font.attached || font.missingFile) { continue; } font.attached = true; if (isFontLoadingAPISupported) { - var nativeFontFace = font.createNativeFontFace(); + const nativeFontFace = font.createNativeFontFace(); if (nativeFontFace) { this.addNativeFontFace(nativeFontFace); fontLoadPromises.push(getNativeFontPromise(nativeFontFace)); } } else { - var rule = font.createFontFaceRule(); + const rule = font.createFontFaceRule(); if (rule) { this.insertRule(rule); rules.push(rule); @@ -141,194 +102,231 @@ if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { } } - var request = this.queueLoadingCallback(callback); + const request = this._queueLoadingCallback(callback); if (isFontLoadingAPISupported) { - Promise.all(fontLoadPromises).then(function() { - request.complete(); - }); - } else if (rules.length > 0 && !FontLoader.isSyncFontLoadingSupported) { - this.prepareFontLoadEvent(rules, fontsToLoad, request); + Promise.all(fontLoadPromises).then(request.complete); + } else if (rules.length > 0 && !this.isSyncFontLoadingSupported) { + this._prepareFontLoadEvent(rules, fontsToLoad, request); } else { request.complete(); } - }; + } - FontLoader.prototype.queueLoadingCallback = - function FontLoader_queueLoadingCallback(callback) { - function LoadLoader_completeRequest() { + _queueLoadingCallback(callback) { + function completeRequest() { assert(!request.done, 'completeRequest() cannot be called twice.'); request.done = true; // Sending all completed requests in order of how they were queued. while (context.requests.length > 0 && context.requests[0].done) { - var otherRequest = context.requests.shift(); + const otherRequest = context.requests.shift(); setTimeout(otherRequest.callback, 0); } } - var context = this.loadingContext; - var requestId = 'pdfjs-font-loading-' + (context.nextRequestId++); - var request = { - id: requestId, + const context = this.loadingContext; + const request = { + id: `pdfjs-font-loading-${context.nextRequestId++}`, done: false, - complete: LoadLoader_completeRequest, + complete: completeRequest, callback, }; context.requests.push(request); return request; - }; + } - FontLoader.prototype.prepareFontLoadEvent = - function fontLoaderPrepareFontLoadEvent(rules, fonts, request) { - /** Hack begin */ - // There's currently no event when a font has finished downloading so the - // following code is a dirty hack to 'guess' when a font is - // ready. It's assumed fonts are loaded in order, so add a known test - // font after the desired fonts and then test for the loading of that - // test font. + get isFontLoadingAPISupported() { + unreachable('Abstract method `isFontLoadingAPISupported`.'); + } - function int32(data, offset) { - return (data.charCodeAt(offset) << 24) | - (data.charCodeAt(offset + 1) << 16) | - (data.charCodeAt(offset + 2) << 8) | - (data.charCodeAt(offset + 3) & 0xff); - } + get isSyncFontLoadingSupported() { + unreachable('Abstract method `isSyncFontLoadingSupported`.'); + } - function spliceString(s, offset, remove, insert) { - var chunk1 = s.substring(0, offset); - var chunk2 = s.substring(offset + remove); - return chunk1 + insert + chunk2; - } + get _loadTestFont() { + unreachable('Abstract method `_loadTestFont`.'); + } - var i, ii; + _prepareFontLoadEvent(rules, fontsToLoad, request) { + unreachable('Abstract method `_prepareFontLoadEvent`.'); + } +} - // The temporary canvas is used to determine if fonts are loaded. - var canvas = document.createElement('canvas'); - canvas.width = 1; - canvas.height = 1; - var ctx = canvas.getContext('2d'); +let FontLoader; +if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) { - var called = 0; - function isFontReady(name, callback) { - called++; - // With setTimeout clamping this gives the font ~100ms to load. - if (called > 30) { - warn('Load test font never loaded.'); - callback(); - return; +FontLoader = class MozcentralFontLoader extends BaseFontLoader { + get isFontLoadingAPISupported() { + return shadow(this, 'isFontLoadingAPISupported', + typeof document !== 'undefined' && !!document.fonts); + } + + get isSyncFontLoadingSupported() { + return shadow(this, 'isSyncFontLoadingSupported', true); + } +}; + +} else { // PDFJSDev.test('CHROME || GENERIC') + +FontLoader = class GenericFontLoader extends BaseFontLoader { + constructor(docId) { + super(docId); + this.loadTestFontId = 0; + } + + get isFontLoadingAPISupported() { + let supported = (typeof document !== 'undefined' && !!document.fonts); + return shadow(this, 'isFontLoadingAPISupported', supported); + } + + get isSyncFontLoadingSupported() { + let supported = false; + if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('CHROME')) { + if (typeof navigator === 'undefined') { + // Node.js - we can pretend that sync font loading is supported. + supported = true; + } else { + // User agent string sniffing is bad, but there is no reliable way to + // tell if the font is fully loaded and ready to be used with canvas. + const m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(navigator.userAgent); + if (m && m[1] >= 14) { + supported = true; } - ctx.font = '30px ' + name; - ctx.fillText('.', 0, 20); - var imageData = ctx.getImageData(0, 0, 1, 1); - if (imageData.data[3] > 0) { - callback(); - return; - } - setTimeout(isFontReady.bind(null, name, callback)); - } - - var loadTestFontId = 'lt' + Date.now() + this.loadTestFontId++; - // Chromium seems to cache fonts based on a hash of the actual font data, - // so the font must be modified for each load test else it will appear to - // be loaded already. - // TODO: This could maybe be made faster by avoiding the btoa of the full - // font by splitting it in chunks before hand and padding the font id. - var data = this.loadTestFont; - var COMMENT_OFFSET = 976; // has to be on 4 byte boundary (for checksum) - data = spliceString(data, COMMENT_OFFSET, loadTestFontId.length, - loadTestFontId); - // CFF checksum is important for IE, adjusting it - var CFF_CHECKSUM_OFFSET = 16; - var XXXX_VALUE = 0x58585858; // the "comment" filled with 'X' - var checksum = int32(data, CFF_CHECKSUM_OFFSET); - for (i = 0, ii = loadTestFontId.length - 3; i < ii; i += 4) { - checksum = (checksum - XXXX_VALUE + int32(loadTestFontId, i)) | 0; - } - if (i < loadTestFontId.length) { // align to 4 bytes boundary - checksum = (checksum - XXXX_VALUE + - int32(loadTestFontId + 'XXX', i)) | 0; - } - data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, string32(checksum)); - - var url = 'url(data:font/opentype;base64,' + btoa(data) + ');'; - var rule = '@font-face { font-family:"' + loadTestFontId + '";src:' + - url + '}'; - this.insertRule(rule); - - var names = []; - for (i = 0, ii = fonts.length; i < ii; i++) { - names.push(fonts[i].loadedName); - } - names.push(loadTestFontId); - - var div = document.createElement('div'); - div.setAttribute('style', - 'visibility: hidden;' + - 'width: 10px; height: 10px;' + - 'position: absolute; top: 0px; left: 0px;'); - for (i = 0, ii = names.length; i < ii; ++i) { - var span = document.createElement('span'); - span.textContent = 'Hi'; - span.style.fontFamily = names[i]; - div.appendChild(span); - } - document.body.appendChild(div); - - isFontReady(loadTestFontId, function() { - document.body.removeChild(div); - request.complete(); - }); - /** Hack end */ - }; -} else { - FontLoader.prototype.bind = function fontLoaderBind(fonts, callback) { - for (var i = 0, ii = fonts.length; i < ii; i++) { - var font = fonts[i]; - if (font.attached) { - continue; - } - - font.attached = true; - var rule = font.createFontFaceRule(); - if (rule) { - this.insertRule(rule); + // TODO - other browsers... } } + return shadow(this, 'isSyncFontLoadingSupported', supported); + } - setTimeout(callback); - }; -} -if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { - FontLoader.isFontLoadingAPISupported = typeof document !== 'undefined' && - !!document.fonts; -} -if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL || CHROME')) { - var isSyncFontLoadingSupported = function isSyncFontLoadingSupported() { - if (typeof navigator === 'undefined') { - // node.js - we can pretend sync font loading is supported. - return true; + get _loadTestFont() { + const getLoadTestFont = function() { + // This is a CFF font with 1 glyph for '.' that fills its entire width and + // height. + return atob( + 'T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQA' + + 'FQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAA' + + 'ALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgA' + + 'AAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1' + + 'AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD' + + '6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACM' + + 'AooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4D' + + 'IP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAA' + + 'AAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUA' + + 'AQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgAB' + + 'AAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABY' + + 'AAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAA' + + 'AC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAA' + + 'AAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQAC' + + 'AQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3' + + 'Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTj' + + 'FQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=='); + }; + return shadow(this, '_loadTestFont', getLoadTestFont()); + } + + _prepareFontLoadEvent(rules, fonts, request) { + /** Hack begin */ + // There's currently no event when a font has finished downloading so the + // following code is a dirty hack to 'guess' when a font is ready. + // It's assumed fonts are loaded in order, so add a known test font after + // the desired fonts and then test for the loading of that test font. + + function int32(data, offset) { + return (data.charCodeAt(offset) << 24) | + (data.charCodeAt(offset + 1) << 16) | + (data.charCodeAt(offset + 2) << 8) | + (data.charCodeAt(offset + 3) & 0xff); + } + function spliceString(s, offset, remove, insert) { + let chunk1 = s.substring(0, offset); + let chunk2 = s.substring(offset + remove); + return chunk1 + insert + chunk2; + } + let i, ii; + + // The temporary canvas is used to determine if fonts are loaded. + let canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + let ctx = canvas.getContext('2d'); + + let called = 0; + function isFontReady(name, callback) { + called++; + // With setTimeout clamping this gives the font ~100ms to load. + if (called > 30) { + warn('Load test font never loaded.'); + callback(); + return; + } + ctx.font = '30px ' + name; + ctx.fillText('.', 0, 20); + let imageData = ctx.getImageData(0, 0, 1, 1); + if (imageData.data[3] > 0) { + callback(); + return; + } + setTimeout(isFontReady.bind(null, name, callback)); } - var supported = false; - - // User agent string sniffing is bad, but there is no reliable way to tell - // if font is fully loaded and ready to be used with canvas. - var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(navigator.userAgent); - if (m && m[1] >= 14) { - supported = true; + const loadTestFontId = `lt${Date.now()}${this.loadTestFontId++}`; + // Chromium seems to cache fonts based on a hash of the actual font data, + // so the font must be modified for each load test else it will appear to + // be loaded already. + // TODO: This could maybe be made faster by avoiding the btoa of the full + // font by splitting it in chunks before hand and padding the font id. + let data = this._loadTestFont; + let COMMENT_OFFSET = 976; // has to be on 4 byte boundary (for checksum) + data = spliceString(data, COMMENT_OFFSET, loadTestFontId.length, + loadTestFontId); + // CFF checksum is important for IE, adjusting it + let CFF_CHECKSUM_OFFSET = 16; + let XXXX_VALUE = 0x58585858; // the "comment" filled with 'X' + let checksum = int32(data, CFF_CHECKSUM_OFFSET); + for (i = 0, ii = loadTestFontId.length - 3; i < ii; i += 4) { + checksum = (checksum - XXXX_VALUE + int32(loadTestFontId, i)) | 0; } - // TODO other browsers - return supported; - }; - Object.defineProperty(FontLoader, 'isSyncFontLoadingSupported', { - get() { - return shadow(FontLoader, 'isSyncFontLoadingSupported', - isSyncFontLoadingSupported()); - }, - enumerable: true, - configurable: true, - }); -} + if (i < loadTestFontId.length) { // align to 4 bytes boundary + checksum = (checksum - XXXX_VALUE + int32(loadTestFontId + 'XXX', i)) | 0; + } + data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, string32(checksum)); + + const url = `url(data:font/opentype;base64,${btoa(data)});`; + const rule = `@font-face {font-family:"${loadTestFontId}";src:${url}}`; + this.insertRule(rule); + + let names = []; + for (i = 0, ii = fonts.length; i < ii; i++) { + names.push(fonts[i].loadedName); + } + names.push(loadTestFontId); + + let div = document.createElement('div'); + div.setAttribute('style', 'visibility: hidden;' + + 'width: 10px; height: 10px;' + + 'position: absolute; top: 0px; left: 0px;'); + for (i = 0, ii = names.length; i < ii; ++i) { + let span = document.createElement('span'); + span.textContent = 'Hi'; + span.style.fontFamily = names[i]; + div.appendChild(span); + } + document.body.appendChild(div); + + isFontReady(loadTestFontId, function() { + document.body.removeChild(div); + request.complete(); + }); + /** Hack end */ + } +}; + +} // End of PDFJSDev.test('CHROME || GENERIC') const IsEvalSupportedCached = { get value() { From 435ec6a0d5b5d725eadc47191648eff3c3897c20 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 16 Aug 2018 20:34:56 +0200 Subject: [PATCH 5/6] Use the Font Loading API in `MOZCENTRAL` builds, and `GENERIC` builds for Firefox version 63 and above (issue 9945) --- src/display/font_loader.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 6525e99d0..d35ba6adf 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -75,10 +75,7 @@ class BaseFontLoader { warn(`Failed to load font "${nativeFontFace.family}": ${reason}`); }); }; - // Firefox Font Loading API does not work with mozPrintCallback -- - // disabling it in this case. - const isFontLoadingAPISupported = this.isFontLoadingAPISupported && - !this.isSyncFontLoadingSupported; + for (const font of fonts) { // Add the font to the DOM only once; skip if the font is already loaded. if (font.attached || font.missingFile) { @@ -86,7 +83,7 @@ class BaseFontLoader { } font.attached = true; - if (isFontLoadingAPISupported) { + if (this.isFontLoadingAPISupported) { const nativeFontFace = font.createNativeFontFace(); if (nativeFontFace) { this.addNativeFontFace(nativeFontFace); @@ -103,7 +100,7 @@ class BaseFontLoader { } const request = this._queueLoadingCallback(callback); - if (isFontLoadingAPISupported) { + if (this.isFontLoadingAPISupported) { Promise.all(fontLoadPromises).then(request.complete); } else if (rules.length > 0 && !this.isSyncFontLoadingSupported) { this._prepareFontLoadEvent(rules, fontsToLoad, request); @@ -176,6 +173,16 @@ FontLoader = class GenericFontLoader extends BaseFontLoader { get isFontLoadingAPISupported() { let supported = (typeof document !== 'undefined' && !!document.fonts); + + if ((typeof PDFJSDev === 'undefined' || !PDFJSDev.test('CHROME')) && + (supported && typeof navigator !== 'undefined')) { + // The Firefox Font Loading API does not work with `mozPrintCallback` + // prior to version 63; see https://bugzilla.mozilla.org/show_bug.cgi?id=1473742 + const m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(navigator.userAgent); + if (m && m[1] < 63) { + supported = false; + } + } return shadow(this, 'isFontLoadingAPISupported', supported); } From d6f4d2ff33a81a1cb952aa9eadcfa654996f1519 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 20 Aug 2018 12:14:37 +0200 Subject: [PATCH 6/6] Add a `Symbol` polyfill, using core-js, to allow using `for...of` loops https://github.com/zloirock/core-js#ecmascript-symbol --- src/shared/compatibility.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/shared/compatibility.js b/src/shared/compatibility.js index 6ade96dc1..d1417421b 100644 --- a/src/shared/compatibility.js +++ b/src/shared/compatibility.js @@ -183,6 +183,14 @@ const hasDOM = typeof window === 'object' && typeof document === 'object'; String.fromCodePoint = require('core-js/fn/string/from-code-point'); })(); +// Support: IE +(function checkSymbol() { + if (globalScope.Symbol) { + return; + } + require('core-js/es6/symbol'); +})(); + } // End of !PDFJSDev.test('CHROME') // Provides support for Object.values in legacy browsers.