Fallback to the built-in font renderer when font loading fails

After PR 9340 all glyphs are now re-mapped to a Private Use Area (PUA) which means that if a font fails to load, for whatever reason[1], all glyphs in the font will now render as Unicode glyph outlines.
This obviously doesn't look good, to say the least, and might be seen as a "regression" since previously many glyphs were left in their original positions which provided a slightly better fallback[2].

Hence this patch, which implements a *general* fallback to the PDF.js built-in font renderer for fonts that fail to load (i.e. are rejected by the sanitizer). One caveat here is that this only works for the Font Loading API, since it's easy to handle errors in that case[3].

The solution implemented in this patch does *not* in any way delay the loading of valid fonts, which was the problem with my previous attempt at a solution, and will only require a bit of extra work/waiting for those fonts that actually fail to load.

*Please note:* This patch doesn't fix any of the underlying PDF.js font conversion bugs that's responsible for creating corrupt font files, however it does *improve* rendering in a number of cases; refer to this possibly incomplete list:

[Bug 1524888](https://bugzilla.mozilla.org/show_bug.cgi?id=1524888)
Issue 10175
Issue 10232

---
[1] Usually because the PDF.js font conversion code wasn't able to parse the font file correctly.

[2] Glyphs fell back to some default font, which while not accurate was more useful than the current state.

[3] Furthermore I'm not sure how to implement this generally, assuming that's even possible, and don't really have time/interest to look into it either.
This commit is contained in:
Jonas Jenwald 2019-02-11 00:47:56 +01:00
parent 13230a1123
commit b6d090cc14
9 changed files with 107 additions and 44 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,
@ -1947,6 +1950,12 @@ class WorkerTransport {
this.fontLoader.bind(font).then(() => {
this.commonObjs.resolve(id, font);
}, (reason) => {
messageHandler.sendWithPromise('FontFallback', {
id,
}).finally(() => {
this.commonObjs.resolve(id, font);
});
});
break;
case 'FontPath':

View File

@ -19,11 +19,12 @@ 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;
@ -74,9 +75,12 @@ class BaseFontLoader {
try {
await nativeFontFace.loaded;
} catch (ex) {
// Return a promise that is always fulfilled, even when the font
// failed to load.
this._onUnsupportedFeature({ featureId: UNSUPPORTED_FEATURES.font, });
warn(`Failed to load font '${nativeFontFace.family}': '${ex}'.`);
// When font loading failed, fall back to the built-in font renderer.
font.disableFontFace = true;
throw ex;
}
}
return; // The font was, asynchronously, loaded.

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() {