Merge pull request #10539 from Snuffleupagus/fallback-disableFontFace-v2

[api-minor] Fallback to the built-in font renderer when font loading fails
This commit is contained in:
Brendan Dahl 2019-02-12 12:08:01 -08:00 committed by GitHub
commit 81f5835cd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 164 additions and 106 deletions

View File

@ -666,6 +666,10 @@ class PDFDocument {
}); });
} }
fontFallback(id, handler) {
return this.catalog.fontFallback(id, handler);
}
cleanup() { cleanup() {
return this.catalog.cleanup(); return this.catalog.cleanup();
} }

View File

@ -610,37 +610,18 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
}); });
}, },
handleText: function PartialEvaluator_handleText(chars, state) { handleText(chars, state) {
var font = state.font; const font = state.font;
var glyphs = font.charsToGlyphs(chars); const glyphs = font.charsToGlyphs(chars);
var isAddToPathSet = !!(state.textRenderingMode &
TextRenderingMode.ADD_TO_PATH_FLAG);
if (font.data && (isAddToPathSet || this.options.disableFontFace ||
state.fillColorSpace.name === 'Pattern')) {
var buildPath = (fontChar) => {
if (!font.renderer.hasBuiltPath(fontChar)) {
var path = font.renderer.getPathJs(fontChar);
this.handler.send('commonobj', [
font.loadedName + '_path_' + fontChar,
'FontPath',
path
]);
}
};
for (var i = 0, ii = glyphs.length; i < ii; i++) { if (font.data) {
var glyph = glyphs[i]; const isAddToPathSet = !!(state.textRenderingMode &
buildPath(glyph.fontChar); TextRenderingMode.ADD_TO_PATH_FLAG);
if (isAddToPathSet || state.fillColorSpace.name === 'Pattern' ||
// If the glyph has an accent we need to build a path for its font.disableFontFace || this.options.disableFontFace) {
// fontChar too, otherwise CanvasGraphics_paintChar will fail. PartialEvaluator.buildFontPaths(font, glyphs, this.handler);
var accent = glyph.accent;
if (accent && accent.fontChar) {
buildPath(accent.fontChar);
}
} }
} }
return glyphs; return glyphs;
}, },
@ -2623,6 +2604,30 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
}, },
}; };
PartialEvaluator.buildFontPaths = function(font, glyphs, handler) {
function buildPath(fontChar) {
if (font.renderer.hasBuiltPath(fontChar)) {
return;
}
handler.send('commonobj', [
`${font.loadedName}_path_${fontChar}`,
'FontPath',
font.renderer.getPathJs(fontChar),
]);
}
for (const glyph of glyphs) {
buildPath(glyph.fontChar);
// If the glyph has an accent we need to build a path for its
// fontChar too, otherwise CanvasGraphics_paintChar will fail.
const accent = glyph.accent;
if (accent && accent.fontChar) {
buildPath(accent.fontChar);
}
}
};
return PartialEvaluator; return PartialEvaluator;
})(); })();
@ -2639,14 +2644,31 @@ var TranslatedFont = (function TranslatedFontClosure() {
if (this.sent) { if (this.sent) {
return; return;
} }
var fontData = this.font.exportData(); this.sent = true;
handler.send('commonobj', [ handler.send('commonobj', [
this.loadedName, this.loadedName,
'Font', 'Font',
fontData this.font.exportData(),
]); ]);
this.sent = true;
}, },
fallback(handler) {
if (!this.font.data) {
return;
}
// When font loading failed, fall back to the built-in font renderer.
this.font.disableFontFace = true;
// An arbitrary number of text rendering operators could have been
// encountered between the point in time when the 'Font' message was sent
// to the main-thread, and the point in time when the 'FontFallback'
// message was received on the worker-thread.
// To ensure that all 'FontPath's are available on the main-thread, when
// font loading failed, attempt to resend *all* previously parsed glyphs.
const glyphs = this.font.glyphCacheValues;
PartialEvaluator.buildFontPaths(this.font, glyphs, handler);
},
loadType3Data(evaluator, resources, parentOperatorList, task) { loadType3Data(evaluator, resources, parentOperatorList, task) {
if (!this.font.isType3Font) { if (!this.font.isType3Font) {
throw new Error('Must be a Type3 font.'); throw new Error('Must be a Type3 font.');

View File

@ -1159,6 +1159,8 @@ var Font = (function FontClosure() {
font: null, font: null,
mimetype: null, mimetype: null,
encoding: null, encoding: null,
disableFontFace: false,
get renderer() { get renderer() {
var renderer = FontRendererFactory.create(this, SEAC_ANALYSIS_ENABLED); var renderer = FontRendererFactory.create(this, SEAC_ANALYSIS_ENABLED);
return shadow(this, 'renderer', renderer); return shadow(this, 'renderer', renderer);
@ -2944,6 +2946,10 @@ var Font = (function FontClosure() {
// Enter the translated string into the cache // Enter the translated string into the cache
return (charsCache[charsCacheKey] = glyphs); return (charsCache[charsCacheKey] = glyphs);
}, },
get glyphCacheValues() {
return Object.values(this.glyphCache);
},
}; };
return Font; return Font;

View File

@ -490,6 +490,22 @@ class Catalog {
return shadow(this, 'javaScript', javaScript); return shadow(this, 'javaScript', javaScript);
} }
fontFallback(id, handler) {
const promises = [];
this.fontCache.forEach(function(promise) {
promises.push(promise);
});
return Promise.all(promises).then((translatedFonts) => {
for (const translatedFont of translatedFonts) {
if (translatedFont.loadedName === id) {
translatedFont.fallback(handler);
return;
}
}
});
}
cleanup() { cleanup() {
this.pageKidsCountCache.clear(); this.pageKidsCountCache.clear();

View File

@ -68,6 +68,10 @@ class BasePdfManager {
return this.pdfDocument.getPage(pageIndex); return this.pdfDocument.getPage(pageIndex);
} }
fontFallback(id, handler) {
return this.pdfDocument.fontFallback(id, handler);
}
cleanup() { cleanup() {
return this.pdfDocument.cleanup(); return this.pdfDocument.cleanup();
} }

View File

@ -667,6 +667,10 @@ var WorkerMessageHandler = {
}); });
}); });
handler.on('FontFallback', function(data) {
return pdfManager.fontFallback(data.id, handler);
});
handler.on('Cleanup', function wphCleanup(data) { handler.on('Cleanup', function wphCleanup(data) {
return pdfManager.cleanup(); return pdfManager.cleanup();
}); });

View File

@ -1676,7 +1676,10 @@ class WorkerTransport {
this.messageHandler = messageHandler; this.messageHandler = messageHandler;
this.loadingTask = loadingTask; this.loadingTask = loadingTask;
this.commonObjs = new PDFObjects(); this.commonObjs = new PDFObjects();
this.fontLoader = new FontLoader(loadingTask.docId); this.fontLoader = new FontLoader({
docId: loadingTask.docId,
onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
});
this._params = params; this._params = params;
this.CMapReaderFactory = new params.CMapReaderFactory({ this.CMapReaderFactory = new params.CMapReaderFactory({
baseUrl: params.cMapUrl, baseUrl: params.cMapUrl,
@ -1944,11 +1947,16 @@ class WorkerTransport {
onUnsupportedFeature: this._onUnsupportedFeature.bind(this), onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
fontRegistry, fontRegistry,
}); });
const fontReady = (fontObjs) => {
this.commonObjs.resolve(id, font);
};
this.fontLoader.bind([font], fontReady); this.fontLoader.bind(font).then(() => {
this.commonObjs.resolve(id, font);
}, (reason) => {
messageHandler.sendWithPromise('FontFallback', {
id,
}).finally(() => {
this.commonObjs.resolve(id, font);
});
});
break; break;
case 'FontPath': case 'FontPath':
this.commonObjs.resolve(id, exportedData); this.commonObjs.resolve(id, exportedData);

View File

@ -19,18 +19,15 @@ import {
} from '../shared/util'; } from '../shared/util';
class BaseFontLoader { class BaseFontLoader {
constructor(docId) { constructor({ docId, onUnsupportedFeature, }) {
if (this.constructor === BaseFontLoader) { if (this.constructor === BaseFontLoader) {
unreachable('Cannot initialize BaseFontLoader.'); unreachable('Cannot initialize BaseFontLoader.');
} }
this.docId = docId; this.docId = docId;
this._onUnsupportedFeature = onUnsupportedFeature;
this.nativeFontFaces = []; this.nativeFontFaces = [];
this.styleElement = null; this.styleElement = null;
this.loadingContext = {
requests: [],
nextRequestId: 0,
};
} }
addNativeFontFace(nativeFontFace) { addNativeFontFace(nativeFontFace) {
@ -64,72 +61,48 @@ class BaseFontLoader {
} }
} }
bind(fonts, callback) { async bind(font) {
const rules = []; // Add the font to the DOM only once; skip if the font is already loaded.
const fontsToLoad = []; if (font.attached || font.missingFile) {
const fontLoadPromises = []; return;
const getNativeFontPromise = function(nativeFontFace) { }
// Return a promise that is always fulfilled, even when the font fails to font.attached = true;
// load.
return nativeFontFace.loaded.catch(function(reason) {
warn(`Failed to load font "${nativeFontFace.family}": ${reason}`);
});
};
for (const font of fonts) { if (this.isFontLoadingAPISupported) {
// Add the font to the DOM only once; skip if the font is already loaded. const nativeFontFace = font.createNativeFontFace();
if (font.attached || font.missingFile) { if (nativeFontFace) {
continue; this.addNativeFontFace(nativeFontFace);
} try {
font.attached = true; await nativeFontFace.loaded;
} catch (ex) {
this._onUnsupportedFeature({ featureId: UNSUPPORTED_FEATURES.font, });
warn(`Failed to load font '${nativeFontFace.family}': '${ex}'.`);
if (this.isFontLoadingAPISupported) { // When font loading failed, fall back to the built-in font renderer.
const nativeFontFace = font.createNativeFontFace(); font.disableFontFace = true;
if (nativeFontFace) { throw ex;
this.addNativeFontFace(nativeFontFace);
fontLoadPromises.push(getNativeFontPromise(nativeFontFace));
}
} else {
const rule = font.createFontFaceRule();
if (rule) {
this.insertRule(rule);
rules.push(rule);
fontsToLoad.push(font);
} }
} }
return; // The font was, asynchronously, loaded.
} }
const request = this._queueLoadingCallback(callback); // !this.isFontLoadingAPISupported
if (this.isFontLoadingAPISupported) { const rule = font.createFontFaceRule();
Promise.all(fontLoadPromises).then(request.complete); if (rule) {
} else if (rules.length > 0 && !this.isSyncFontLoadingSupported) { this.insertRule(rule);
this._prepareFontLoadEvent(rules, fontsToLoad, request);
} else { if (this.isSyncFontLoadingSupported) {
request.complete(); return; // The font was, synchronously, loaded.
}
return new Promise((resolve) => {
const request = this._queueLoadingCallback(resolve);
this._prepareFontLoadEvent([rule], [font], request);
});
} }
} }
_queueLoadingCallback(callback) { _queueLoadingCallback(callback) {
function completeRequest() { unreachable('Abstract method `_queueLoadingCallback`.');
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) {
const otherRequest = context.requests.shift();
setTimeout(otherRequest.callback, 0);
}
}
const context = this.loadingContext;
const request = {
id: `pdfjs-font-loading-${context.nextRequestId++}`,
done: false,
complete: completeRequest,
callback,
};
context.requests.push(request);
return request;
} }
get isFontLoadingAPISupported() { get isFontLoadingAPISupported() {
@ -168,6 +141,10 @@ FontLoader = class MozcentralFontLoader extends BaseFontLoader {
FontLoader = class GenericFontLoader extends BaseFontLoader { FontLoader = class GenericFontLoader extends BaseFontLoader {
constructor(docId) { constructor(docId) {
super(docId); super(docId);
this.loadingContext = {
requests: [],
nextRequestId: 0,
};
this.loadTestFontId = 0; this.loadTestFontId = 0;
} }
@ -205,6 +182,29 @@ FontLoader = class GenericFontLoader extends BaseFontLoader {
return shadow(this, 'isSyncFontLoadingSupported', supported); return shadow(this, 'isSyncFontLoadingSupported', supported);
} }
_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) {
const otherRequest = context.requests.shift();
setTimeout(otherRequest.callback, 0);
}
}
const context = this.loadingContext;
const request = {
id: `pdfjs-font-loading-${context.nextRequestId++}`,
done: false,
complete: completeRequest,
callback,
};
context.requests.push(request);
return request;
}
get _loadTestFont() { get _loadTestFont() {
const getLoadTestFont = function() { const getLoadTestFont = function() {
// This is a CFF font with 1 glyph for '.' that fills its entire width and // This is a CFF font with 1 glyph for '.' that fills its entire width and

View File

@ -23,11 +23,6 @@ if ((typeof PDFJSDev === 'undefined' ||
globalScope._pdfjsCompatibilityChecked = true; globalScope._pdfjsCompatibilityChecked = true;
// In the Chrome extension, most of the polyfills are unnecessary.
// We support down to Chrome 49, because it's still commonly used by Windows XP
// users - https://github.com/mozilla/pdf.js/issues/9397
if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('CHROME')) {
const isNodeJS = require('./is_node'); const isNodeJS = require('./is_node');
const hasDOM = typeof window === 'object' && typeof document === 'object'; const hasDOM = typeof window === 'object' && typeof document === 'object';
@ -199,14 +194,15 @@ const hasDOM = typeof window === 'object' && typeof document === 'object';
Number.isInteger = require('core-js/fn/number/is-integer'); Number.isInteger = require('core-js/fn/number/is-integer');
})(); })();
// Support: IE, Safari<8, Chrome<32 // Support: IE, Safari<11, Chrome<63
(function checkPromise() { (function checkPromise() {
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) { if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) {
// The current image decoders are synchronous, hence `Promise` shouldn't // The current image decoders are synchronous, hence `Promise` shouldn't
// need to be polyfilled for the IMAGE_DECODERS build target. // need to be polyfilled for the IMAGE_DECODERS build target.
return; return;
} }
if (globalScope.Promise) { if (globalScope.Promise && (globalScope.Promise.prototype &&
globalScope.Promise.prototype.finally)) {
return; return;
} }
globalScope.Promise = require('core-js/fn/promise'); globalScope.Promise = require('core-js/fn/promise');
@ -254,8 +250,6 @@ const hasDOM = typeof window === 'object' && typeof document === 'object';
require('core-js/es6/symbol'); require('core-js/es6/symbol');
})(); })();
} // End of !PDFJSDev.test('CHROME')
// Provides support for String.prototype.padStart in legacy browsers. // Provides support for String.prototype.padStart in legacy browsers.
// Support: IE, Chrome<57 // Support: IE, Chrome<57
(function checkStringPadStart() { (function checkStringPadStart() {