From 80441346a3fe91b52a08331ff278da6a43cf0971 Mon Sep 17 00:00:00 2001
From: Jonas Jenwald <jonas.jenwald@gmail.com>
Date: Thu, 1 Feb 2018 16:43:10 +0100
Subject: [PATCH] Fallback to the built-in JPEG decoder if 'JpegStream', in
 `src/display/api.js`, fails to load the image

This works by making `PartialEvaluator.buildPaintImageXObject` wait for the success/failure of `loadJpegStream` on the API side *before* parsing continues.

Please note that in practice, it should be quite rare for the browser to fail loading/decoding of a JPEG image. In the general case, it should thus not be completely surprising if even `src/core/jpg.js` will fail to decode the image.
---
 src/core/evaluator.js | 52 +++++++++++++++++++++++++++++++------------
 src/display/api.js    | 10 ++++-----
 2 files changed, 43 insertions(+), 19 deletions(-)

diff --git a/src/core/evaluator.js b/src/core/evaluator.js
index 2ad04dbca..302ed26a5 100644
--- a/src/core/evaluator.js
+++ b/src/core/evaluator.js
@@ -349,7 +349,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
     },
 
     buildPaintImageXObject({ resources, image, isInline = false, operatorList,
-                             cacheKey, imageCache, }) {
+                             cacheKey, imageCache,
+                             forceDisableNativeImageDecoder = false, }) {
       var dict = image.dict;
       var w = dict.get('Width', 'W');
       var h = dict.get('Height', 'H');
@@ -419,28 +420,47 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
         return Promise.resolve();
       }
 
-      var nativeImageDecoderSupport = this.options.nativeImageDecoderSupport;
+      const nativeImageDecoderSupport = forceDisableNativeImageDecoder ?
+        NativeImageDecoding.NONE : this.options.nativeImageDecoderSupport;
       // If there is no imageMask, create the PDFImage and a lot
       // of image processing can be done here.
       var objId = 'img_' + this.idFactory.createObjId();
-      operatorList.addDependency(objId);
-      args = [objId, w, h];
 
       if (nativeImageDecoderSupport !== NativeImageDecoding.NONE &&
           !softMask && !mask && image instanceof JpegStream &&
           NativeImageDecoder.isSupported(image, this.xref, resources,
                                          this.pdfFunctionFactory)) {
         // These JPEGs don't need any more processing so we can just send it.
-        operatorList.addOp(OPS.paintJpegXObject, args);
-        this.handler.send('obj', [objId, this.pageIndex, 'JpegStream',
-                                  image.getIR(this.options.forceDataSchema)]);
-        if (cacheKey) {
-          imageCache[cacheKey] = {
-            fn: OPS.paintJpegXObject,
-            args,
-          };
-        }
-        return Promise.resolve();
+        return this.handler.sendWithPromise('obj', [
+          objId, this.pageIndex, 'JpegStream',
+          image.getIR(this.options.forceDataSchema)
+        ]).then(function() {
+          // Only add the dependency once we know that the native JPEG decoding
+          // succeeded, to ensure that rendering will always complete.
+          operatorList.addDependency(objId);
+          args = [objId, w, h];
+
+          operatorList.addOp(OPS.paintJpegXObject, args);
+          if (cacheKey) {
+            imageCache[cacheKey] = {
+              fn: OPS.paintJpegXObject,
+              args,
+            };
+          }
+        }, (reason) => {
+          warn('Native JPEG decoding failed -- trying to recover: ' +
+               (reason && reason.message));
+          // Try to decode the JPEG image with the built-in decoder instead.
+          return this.buildPaintImageXObject({
+            resources,
+            image,
+            isInline,
+            operatorList,
+            cacheKey,
+            imageCache,
+            forceDisableNativeImageDecoder: true,
+          });
+        });
       }
 
       // Creates native image decoder only if a JPEG image or mask is present.
@@ -457,6 +477,10 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
         });
       }
 
+      // Ensure that the dependency is added before the image is decoded.
+      operatorList.addDependency(objId);
+      args = [objId, w, h];
+
       PDFImage.buildImage({
         handler: this.handler,
         xref: this.xref,
diff --git a/src/display/api.js b/src/display/api.js
index d50651c42..0819b9736 100644
--- a/src/display/api.js
+++ b/src/display/api.js
@@ -1817,22 +1817,22 @@ var WorkerTransport = (function WorkerTransportClosure() {
         switch (type) {
           case 'JpegStream':
             imageData = data[3];
-            new Promise((resolve, reject) => {
+            return new Promise((resolve, reject) => {
               const img = new Image();
               img.onload = function() {
                 resolve(img);
               };
               img.onerror = function() {
                 reject(new Error('Error during JPEG image loading'));
+                // Note that when the browser image loading/decoding fails,
+                // we'll fallback to the built-in PDF.js JPEG decoder; see
+                // `PartialEvaluator.buildPaintImageXObject` in the
+                // `src/core/evaluator.js` file.
               };
               img.src = imageData;
             }).then((img) => {
               pageProxy.objs.resolve(id, img);
-            }, (reason) => {
-              warn(reason);
-              pageProxy.objs.resolve(id, null);
             });
-            break;
           case 'Image':
             imageData = data[3];
             pageProxy.objs.resolve(id, imageData);