From ad83fbe2a18128095713e3621f6dc4ca268b65b1 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 25 Sep 2022 11:54:43 +0200 Subject: [PATCH] Replace the `BaseFontLoader` classes with one `FontLoader` class By having just *one* class, and using pre-processor blocks directly in the relevant methods, we reduce the size of this code in the *built* `pdf.js` file. Originally, when the `BaseFontLoader` abstraction was added in PR 9982, the idea was probably that additional build-targets would get their own implementations. Given that this hasn't happened in the four years since that landed, it doesn't seem meaningful to keep it around. --- src/display/font_loader.js | 384 +++++++++++++++++-------------------- 1 file changed, 179 insertions(+), 205 deletions(-) diff --git a/src/display/font_loader.js b/src/display/font_loader.js index c8998218b..ad8c5f23d 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -19,22 +19,17 @@ import { FeatureTest, shadow, string32, - unreachable, UNSUPPORTED_FEATURES, warn, } from "../shared/util.js"; -class BaseFontLoader { +class FontLoader { constructor({ docId, onUnsupportedFeature, ownerDocument = globalThis.document, - // For testing only. - styleElement = null, + styleElement = null, // For testing only. }) { - if (this.constructor === BaseFontLoader) { - unreachable("Cannot initialize BaseFontLoader."); - } this.docId = docId; this._onUnsupportedFeature = onUnsupportedFeature; this._document = ownerDocument; @@ -44,6 +39,11 @@ class BaseFontLoader { typeof PDFJSDev === "undefined" || PDFJSDev.test("!PRODUCTION || TESTING") ? styleElement : null; + + if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { + this.loadingRequests = []; + this.loadTestFontId = 0; + } } addNativeFontFace(nativeFontFace) { @@ -120,10 +120,6 @@ class BaseFontLoader { } } - _queueLoadingCallback(callback) { - unreachable("Abstract method `_queueLoadingCallback`."); - } - get isFontLoadingAPISupported() { const hasFonts = !!this._document?.fonts; if ( @@ -139,220 +135,198 @@ class BaseFontLoader { return shadow(this, "isFontLoadingAPISupported", hasFonts); } - // eslint-disable-next-line getter-return get isSyncFontLoadingSupported() { - unreachable("Abstract method `isSyncFontLoadingSupported`."); - } - - // eslint-disable-next-line getter-return - get _loadTestFont() { - unreachable("Abstract method `_loadTestFont`."); - } - - _prepareFontLoadEvent(rules, fontsToLoad, request) { - unreachable("Abstract method `_prepareFontLoadEvent`."); - } -} - -let FontLoader; -if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - FontLoader = class MozcentralFontLoader extends BaseFontLoader { - get isSyncFontLoadingSupported() { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { return shadow(this, "isSyncFontLoadingSupported", true); } - }; -} else { - // PDFJSDev.test('CHROME || GENERIC') - FontLoader = class GenericFontLoader extends BaseFontLoader { - constructor(params) { - super(params); - this.loadingRequests = []; - this.loadTestFontId = 0; - } - - 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. + 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?.[1] >= 14) { 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?.[1] >= 14) { - supported = true; - } - // TODO - other browsers... } + // TODO - other browsers... } - return shadow(this, "isSyncFontLoadingSupported", supported); + } + return shadow(this, "isSyncFontLoadingSupported", supported); + } + + _queueLoadingCallback(callback) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + throw new Error("Not implemented: _queueLoadingCallback"); } - _queueLoadingCallback(callback) { - function completeRequest() { - assert(!request.done, "completeRequest() cannot be called twice."); - request.done = true; + 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 (loadingRequests.length > 0 && loadingRequests[0].done) { - const otherRequest = loadingRequests.shift(); - setTimeout(otherRequest.callback, 0); - } + // Sending all completed requests in order of how they were queued. + while (loadingRequests.length > 0 && loadingRequests[0].done) { + const otherRequest = loadingRequests.shift(); + setTimeout(otherRequest.callback, 0); } - - const { loadingRequests } = this; - const request = { - done: false, - complete: completeRequest, - callback, - }; - loadingRequests.push(request); - return request; } - 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()); + const { loadingRequests } = this; + const request = { + done: false, + complete: completeRequest, + callback, + }; + loadingRequests.push(request); + return request; + } + + get _loadTestFont() { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + throw new Error("Not implemented: _loadTestFont"); } - _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. + // This is a CFF font with 1 glyph for '.' that fills its entire width + // and height. + const testFont = 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", testFont); + } - 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) { - const chunk1 = s.substring(0, offset); - const chunk2 = s.substring(offset + remove); - return chunk1 + insert + chunk2; - } - let i, ii; + _prepareFontLoadEvent(rules, fonts, request) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + throw new Error("Not implemented: _prepareFontLoadEvent"); + } - // The temporary canvas is used to determine if fonts are loaded. - const canvas = this._document.createElement("canvas"); - canvas.width = 1; - canvas.height = 1; - const ctx = canvas.getContext("2d"); + /** 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. - 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); - const imageData = ctx.getImageData(0, 0, 1, 1); - if (imageData.data[3] > 0) { - callback(); - return; - } - setTimeout(isFontReady.bind(null, name, callback)); - } - - 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; - const COMMENT_OFFSET = 976; // has to be on 4 byte boundary (for checksum) - data = spliceString( - data, - COMMENT_OFFSET, - loadTestFontId.length, - loadTestFontId + function int32(data, offset) { + return ( + (data.charCodeAt(offset) << 24) | + (data.charCodeAt(offset + 1) << 16) | + (data.charCodeAt(offset + 2) << 8) | + (data.charCodeAt(offset + 3) & 0xff) ); - // CFF checksum is important for IE, adjusting it - const CFF_CHECKSUM_OFFSET = 16; - const 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; - } - 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); - - const names = []; - for (const font of fonts) { - names.push(font.loadedName); - } - names.push(loadTestFontId); - - const div = this._document.createElement("div"); - div.style.visibility = "hidden"; - div.style.width = div.style.height = "10px"; - div.style.position = "absolute"; - div.style.top = div.style.left = "0px"; - - for (const name of names) { - const span = this._document.createElement("span"); - span.textContent = "Hi"; - span.style.fontFamily = name; - div.append(span); - } - this._document.body.append(div); - - isFontReady(loadTestFontId, () => { - div.remove(); - request.complete(); - }); - /** Hack end */ } - }; -} // End of PDFJSDev.test('CHROME || GENERIC') + function spliceString(s, offset, remove, insert) { + const chunk1 = s.substring(0, offset); + const chunk2 = s.substring(offset + remove); + return chunk1 + insert + chunk2; + } + let i, ii; + + // The temporary canvas is used to determine if fonts are loaded. + const canvas = this._document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + const 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); + const imageData = ctx.getImageData(0, 0, 1, 1); + if (imageData.data[3] > 0) { + callback(); + return; + } + setTimeout(isFontReady.bind(null, name, callback)); + } + + 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; + const 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 + const CFF_CHECKSUM_OFFSET = 16; + const 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; + } + 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); + + const names = []; + for (const font of fonts) { + names.push(font.loadedName); + } + names.push(loadTestFontId); + + const div = this._document.createElement("div"); + div.style.visibility = "hidden"; + div.style.width = div.style.height = "10px"; + div.style.position = "absolute"; + div.style.top = div.style.left = "0px"; + + for (const name of names) { + const span = this._document.createElement("span"); + span.textContent = "Hi"; + span.style.fontFamily = name; + div.append(span); + } + this._document.body.append(div); + + isFontReady(loadTestFontId, () => { + div.remove(); + request.complete(); + }); + /** Hack end */ + } +} class FontFaceObject { constructor(