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() {
return this.catalog.cleanup();
}

View File

@ -610,37 +610,18 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
});
},
handleText: function PartialEvaluator_handleText(chars, state) {
var font = state.font;
var 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
]);
}
};
handleText(chars, state) {
const font = state.font;
const glyphs = font.charsToGlyphs(chars);
for (var i = 0, ii = glyphs.length; i < ii; i++) {
var glyph = glyphs[i];
buildPath(glyph.fontChar);
// If the glyph has an accent we need to build a path for its
// fontChar too, otherwise CanvasGraphics_paintChar will fail.
var accent = glyph.accent;
if (accent && accent.fontChar) {
buildPath(accent.fontChar);
}
if (font.data) {
const isAddToPathSet = !!(state.textRenderingMode &
TextRenderingMode.ADD_TO_PATH_FLAG);
if (isAddToPathSet || state.fillColorSpace.name === 'Pattern' ||
font.disableFontFace || this.options.disableFontFace) {
PartialEvaluator.buildFontPaths(font, glyphs, this.handler);
}
}
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;
})();
@ -2639,14 +2644,31 @@ var TranslatedFont = (function TranslatedFontClosure() {
if (this.sent) {
return;
}
var fontData = this.font.exportData();
this.sent = true;
handler.send('commonobj', [
this.loadedName,
'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) {
if (!this.font.isType3Font) {
throw new Error('Must be a Type3 font.');

View File

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

View File

@ -490,6 +490,22 @@ class Catalog {
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() {
this.pageKidsCountCache.clear();

View File

@ -68,6 +68,10 @@ class BasePdfManager {
return this.pdfDocument.getPage(pageIndex);
}
fontFallback(id, handler) {
return this.pdfDocument.fontFallback(id, handler);
}
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) {
return pdfManager.cleanup();
});

View File

@ -1676,7 +1676,10 @@ class WorkerTransport {
this.messageHandler = messageHandler;
this.loadingTask = loadingTask;
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.CMapReaderFactory = new params.CMapReaderFactory({
baseUrl: params.cMapUrl,
@ -1944,11 +1947,16 @@ class WorkerTransport {
onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
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;
case 'FontPath':
this.commonObjs.resolve(id, exportedData);

View File

@ -19,18 +19,15 @@ import {
} from '../shared/util';
class BaseFontLoader {
constructor(docId) {
constructor({ docId, onUnsupportedFeature, }) {
if (this.constructor === BaseFontLoader) {
unreachable('Cannot initialize BaseFontLoader.');
}
this.docId = docId;
this._onUnsupportedFeature = onUnsupportedFeature;
this.nativeFontFaces = [];
this.styleElement = null;
this.loadingContext = {
requests: [],
nextRequestId: 0,
};
}
addNativeFontFace(nativeFontFace) {
@ -64,72 +61,48 @@ class BaseFontLoader {
}
}
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(reason) {
warn(`Failed to load font "${nativeFontFace.family}": ${reason}`);
});
};
async bind(font) {
// Add the font to the DOM only once; skip if the font is already loaded.
if (font.attached || font.missingFile) {
return;
}
font.attached = true;
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 (this.isFontLoadingAPISupported) {
const nativeFontFace = font.createNativeFontFace();
if (nativeFontFace) {
this.addNativeFontFace(nativeFontFace);
try {
await nativeFontFace.loaded;
} catch (ex) {
this._onUnsupportedFeature({ featureId: UNSUPPORTED_FEATURES.font, });
warn(`Failed to load font '${nativeFontFace.family}': '${ex}'.`);
if (this.isFontLoadingAPISupported) {
const nativeFontFace = font.createNativeFontFace();
if (nativeFontFace) {
this.addNativeFontFace(nativeFontFace);
fontLoadPromises.push(getNativeFontPromise(nativeFontFace));
}
} else {
const rule = font.createFontFaceRule();
if (rule) {
this.insertRule(rule);
rules.push(rule);
fontsToLoad.push(font);
// When font loading failed, fall back to the built-in font renderer.
font.disableFontFace = true;
throw ex;
}
}
return; // The font was, asynchronously, loaded.
}
const request = this._queueLoadingCallback(callback);
if (this.isFontLoadingAPISupported) {
Promise.all(fontLoadPromises).then(request.complete);
} else if (rules.length > 0 && !this.isSyncFontLoadingSupported) {
this._prepareFontLoadEvent(rules, fontsToLoad, request);
} else {
request.complete();
// !this.isFontLoadingAPISupported
const rule = font.createFontFaceRule();
if (rule) {
this.insertRule(rule);
if (this.isSyncFontLoadingSupported) {
return; // The font was, synchronously, loaded.
}
return new Promise((resolve) => {
const request = this._queueLoadingCallback(resolve);
this._prepareFontLoadEvent([rule], [font], request);
});
}
}
_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;
unreachable('Abstract method `_queueLoadingCallback`.');
}
get isFontLoadingAPISupported() {
@ -168,6 +141,10 @@ FontLoader = class MozcentralFontLoader extends BaseFontLoader {
FontLoader = class GenericFontLoader extends BaseFontLoader {
constructor(docId) {
super(docId);
this.loadingContext = {
requests: [],
nextRequestId: 0,
};
this.loadTestFontId = 0;
}
@ -205,6 +182,29 @@ FontLoader = class GenericFontLoader extends BaseFontLoader {
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() {
const getLoadTestFont = function() {
// 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;
// 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 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');
})();
// Support: IE, Safari<8, Chrome<32
// Support: IE, Safari<11, Chrome<63
(function checkPromise() {
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('IMAGE_DECODERS')) {
// The current image decoders are synchronous, hence `Promise` shouldn't
// need to be polyfilled for the IMAGE_DECODERS build target.
return;
}
if (globalScope.Promise) {
if (globalScope.Promise && (globalScope.Promise.prototype &&
globalScope.Promise.prototype.finally)) {
return;
}
globalScope.Promise = require('core-js/fn/promise');
@ -254,8 +250,6 @@ const hasDOM = typeof window === 'object' && typeof document === 'object';
require('core-js/es6/symbol');
})();
} // End of !PDFJSDev.test('CHROME')
// Provides support for String.prototype.padStart in legacy browsers.
// Support: IE, Chrome<57
(function checkStringPadStart() {