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