From 778981ec893baea12ac3182122817cfc1ce405a5 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald <jonas.jenwald@gmail.com> Date: Wed, 13 Jun 2018 11:01:58 +0200 Subject: [PATCH 1/4] Catch, and propagate, errors in the `requestAnimationFrame` branch of `InternalRenderTask._scheduleNext` To support these changes, `InternalRenderTask._next` now returns a Promise. --- src/display/api.js | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 6e71f8cc7..1ebdd22ed 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -2455,30 +2455,34 @@ var InternalRenderTask = (function InternalRenderTaskClosure() { _scheduleNext: function InternalRenderTask__scheduleNext() { if (this.useRequestAnimationFrame && typeof window !== 'undefined') { - window.requestAnimationFrame(this._nextBound); + window.requestAnimationFrame(() => { + this._nextBound().catch(this.callback); + }); } else { Promise.resolve().then(this._nextBound).catch(this.callback); } }, _next: function InternalRenderTask__next() { - if (this.cancelled) { - return; - } - this.operatorListIdx = this.gfx.executeOperatorList(this.operatorList, - this.operatorListIdx, - this._continueBound, - this.stepper); - if (this.operatorListIdx === this.operatorList.argsArray.length) { - this.running = false; - if (this.operatorList.lastChunk) { - this.gfx.endDrawing(); - if (this._canvas) { - canvasInRendering.delete(this._canvas); - } - this.callback(); + return new Promise(() => { + if (this.cancelled) { + return; } - } + this.operatorListIdx = this.gfx.executeOperatorList(this.operatorList, + this.operatorListIdx, + this._continueBound, + this.stepper); + if (this.operatorListIdx === this.operatorList.argsArray.length) { + this.running = false; + if (this.operatorList.lastChunk) { + this.gfx.endDrawing(); + if (this._canvas) { + canvasInRendering.delete(this._canvas); + } + this.callback(); + } + } + }); }, }; From fe288bb872aca1cc1df688426a2b4a73bc3910e3 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald <jonas.jenwald@gmail.com> Date: Wed, 13 Jun 2018 11:02:02 +0200 Subject: [PATCH 2/4] Refactor the `FontFaceObject.getPathGenerator` method - Reduce the overall indentation level, by making use of early returns. - Replace `var` with `let`. --- src/display/font_loader.js | 72 ++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 904712bf6..411872582 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -385,45 +385,41 @@ var FontFaceObject = (function FontFaceObjectClosure() { return rule; }, - getPathGenerator: - function FontFaceObject_getPathGenerator(objs, character) { - if (!(character in this.compiledGlyphs)) { - var cmds = objs.get(this.loadedName + '_path_' + character); - var current, i, len; - - // If we can, compile cmds into JS for MAXIMUM SPEED - if (this.isEvalSupported && IsEvalSupportedCached.value) { - var args, js = ''; - for (i = 0, len = cmds.length; i < len; 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 - this.compiledGlyphs[character] = new Function('c', 'size', js); - } else { - // But fall back on using Function.prototype.apply() if we're - // blocked from using eval() for whatever reason (like CSP policies) - this.compiledGlyphs[character] = function(c, size) { - for (i = 0, len = cmds.length; i < len; i++) { - current = cmds[i]; - - if (current.cmd === 'scale') { - current.args = [size, -size]; - } - - c[current.cmd].apply(c, current.args); - } - }; - } + getPathGenerator(objs, character) { + if (this.compiledGlyphs[character] !== undefined) { + return this.compiledGlyphs[character]; } - return this.compiledGlyphs[character]; + + let cmds = objs.get(this.loadedName + '_path_' + character), current; + + // 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); + } + }; }, }; From bf0db0fb728cb6aca338a3cd83b2ab12511a04c8 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald <jonas.jenwald@gmail.com> Date: Wed, 13 Jun 2018 11:02:06 +0200 Subject: [PATCH 3/4] Pass the `ignoreErrors` API option to the `FontFaceObject` constructor, and utilize it in `getPathGenerator` to ignore missing glyphs Obviously it's still not possible to render non-embedded fonts as paths, but in this way the rest of the page will at least be allowed to continue rendering. *Please note:* Including the 14 standard fonts in PDF.js probably wouldn't be *that* difficult to implement. (I'm not a lawyer, but the fonts from PDFium could probably be used given their BSD license.) However, the main blocker ought to be the total size of the necessary font data, since I cannot imagine people being OK with shipping ~5 MB of (additional) font data with Firefox. (Based on the reactions when the CMap files were added, and those are only ~1 MB in size.) --- src/display/api.js | 1 + src/display/font_loader.js | 16 +++++++++++++++- test/pdfs/issue4244.pdf.link | 1 + test/test_manifest.json | 7 +++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 test/pdfs/issue4244.pdf.link diff --git a/src/display/api.js b/src/display/api.js index 1ebdd22ed..5a37e574c 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1884,6 +1884,7 @@ var WorkerTransport = (function WorkerTransportClosure() { var font = new FontFaceObject(exportedData, { isEvalSupported: params.isEvalSupported, disableFontFace: params.disableFontFace, + ignoreErrors: params.ignoreErrors, fontRegistry, }); var fontReady = (fontObjs) => { diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 411872582..be662bcae 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -338,6 +338,7 @@ var IsEvalSupportedCached = { var FontFaceObject = (function FontFaceObjectClosure() { function FontFaceObject(translatedData, { isEvalSupported = true, disableFontFace = false, + ignoreErrors = false, fontRegistry = null, }) { this.compiledGlyphs = Object.create(null); // importing translated data @@ -346,6 +347,7 @@ var FontFaceObject = (function FontFaceObjectClosure() { } this.isEvalSupported = isEvalSupported !== false; this.disableFontFace = disableFontFace === true; + this.ignoreErrors = ignoreErrors === true; this.fontRegistry = fontRegistry; } FontFaceObject.prototype = { @@ -390,7 +392,19 @@ var FontFaceObject = (function FontFaceObjectClosure() { return this.compiledGlyphs[character]; } - let cmds = objs.get(this.loadedName + '_path_' + character), current; + let cmds, current; + try { + cmds = objs.get(this.loadedName + '_path_' + character); + } catch (ex) { + if (!this.ignoreErrors) { + throw ex; + } + 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) { diff --git a/test/pdfs/issue4244.pdf.link b/test/pdfs/issue4244.pdf.link new file mode 100644 index 000000000..05767bbfe --- /dev/null +++ b/test/pdfs/issue4244.pdf.link @@ -0,0 +1 @@ +https://web.archive.org/web/20180613082417/https://tcpdf.org/files/examples/example_026.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index 6b8142274..99213a1a7 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2267,6 +2267,13 @@ "rounds": 1, "type": "eq" }, + { "id": "issue4244", + "file": "pdfs/issue4244.pdf", + "md5": "26845274a32a537182ced1fd693a38b2", + "rounds": 1, + "link": true, + "type": "eq" + }, { "id": "preistabelle", "file": "pdfs/preistabelle.pdf", "md5": "d2f0b2086160d4f3d325c79a5dc1fb4d", From 09580067138d302bd2010ab4413fe5ee1d254270 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald <jonas.jenwald@gmail.com> Date: Wed, 13 Jun 2018 11:02:10 +0200 Subject: [PATCH 4/4] Send `UnsupportedFeature` notification when errors are ignored in `FontFaceObject.getPathGenerator` --- src/display/api.js | 21 ++++++++++++--------- src/display/font_loader.js | 8 +++++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index 5a37e574c..029c3ac59 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1885,6 +1885,7 @@ var WorkerTransport = (function WorkerTransportClosure() { isEvalSupported: params.isEvalSupported, disableFontFace: params.disableFontFace, ignoreErrors: params.ignoreErrors, + onUnsupportedFeature: this._onUnsupportedFeature.bind(this), fontRegistry, }); var fontReady = (fontObjs) => { @@ -1987,15 +1988,7 @@ var WorkerTransport = (function WorkerTransportClosure() { } }, this); - messageHandler.on('UnsupportedFeature', function(data) { - if (this.destroyed) { - return; // Ignore any pending requests if the worker was terminated. - } - let loadingTask = this.loadingTask; - if (loadingTask.onUnsupportedFeature) { - loadingTask.onUnsupportedFeature(data.featureId); - } - }, this); + messageHandler.on('UnsupportedFeature', this._onUnsupportedFeature, this); messageHandler.on('JpegDecode', function(data) { if (this.destroyed) { @@ -2061,6 +2054,16 @@ var WorkerTransport = (function WorkerTransportClosure() { }, this); }, + _onUnsupportedFeature({ featureId, }) { + if (this.destroyed) { + return; // Ignore any pending requests if the worker was terminated. + } + let loadingTask = this.loadingTask; + if (loadingTask.onUnsupportedFeature) { + loadingTask.onUnsupportedFeature(featureId); + } + }, + getData: function WorkerTransport_getData() { return this.messageHandler.sendWithPromise('GetData', null); }, diff --git a/src/display/font_loader.js b/src/display/font_loader.js index be662bcae..cb7b26f49 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -14,7 +14,8 @@ */ import { - assert, bytesToString, isEvalSupported, shadow, string32, warn + assert, bytesToString, isEvalSupported, shadow, string32, + UNSUPPORTED_FEATURES, warn } from '../shared/util'; function FontLoader(docId) { @@ -339,6 +340,7 @@ var FontFaceObject = (function FontFaceObjectClosure() { function FontFaceObject(translatedData, { isEvalSupported = true, disableFontFace = false, ignoreErrors = false, + onUnsupportedFeature = null, fontRegistry = null, }) { this.compiledGlyphs = Object.create(null); // importing translated data @@ -348,6 +350,7 @@ var FontFaceObject = (function FontFaceObjectClosure() { this.isEvalSupported = isEvalSupported !== false; this.disableFontFace = disableFontFace === true; this.ignoreErrors = ignoreErrors === true; + this._onUnsupportedFeature = onUnsupportedFeature; this.fontRegistry = fontRegistry; } FontFaceObject.prototype = { @@ -399,6 +402,9 @@ var FontFaceObject = (function FontFaceObjectClosure() { 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) {