소스 검색

PDF.js version 2.6.347 - See https://github.com/mozilla/pdf.js/releases/tag/v2.6.347

pdfjsbot 4 년 전
부모
커밋
75ce02e272
100개의 변경된 파일18897개의 추가작업 그리고 5373개의 파일을 삭제
  1. 1 1
      bower.json
  2. 401 117
      build/pdf.js
  3. 0 0
      build/pdf.js.map
  4. 0 0
      build/pdf.min.js
  5. 363 218
      build/pdf.worker.js
  6. 0 0
      build/pdf.worker.js.map
  7. 0 0
      build/pdf.worker.min.js
  8. 449 229
      es5/build/pdf.js
  9. 0 0
      es5/build/pdf.js.map
  10. 21 0
      es5/build/pdf.min.js
  11. 558 435
      es5/build/pdf.worker.js
  12. 0 0
      es5/build/pdf.worker.js.map
  13. 21 0
      es5/build/pdf.worker.min.js
  14. 8192 0
      es5/image_decoders/pdf.image_decoders.js
  15. 0 0
      es5/image_decoders/pdf.image_decoders.js.map
  16. 21 0
      es5/image_decoders/pdf.image_decoders.min.js
  17. BIN
      es5/web/images/shadow.png
  18. BIN
      es5/web/images/texture.png
  19. 8 5
      es5/web/pdf_viewer.css
  20. 195 51
      es5/web/pdf_viewer.js
  21. 0 0
      es5/web/pdf_viewer.js.map
  22. 24 16
      image_decoders/pdf.image_decoders.js
  23. 0 0
      image_decoders/pdf.image_decoders.js.map
  24. 0 0
      image_decoders/pdf.image_decoders.min.js
  25. 479 43
      lib/core/annotation.js
  26. 643 615
      lib/core/cff_parser.js
  27. 64 50
      lib/core/chunked_stream.js
  28. 126 104
      lib/core/colorspace.js
  29. 37 0
      lib/core/crypto.js
  30. 128 75
      lib/core/document.js
  31. 2423 2164
      lib/core/evaluator.js
  32. 47 46
      lib/core/fonts.js
  33. 79 6
      lib/core/function.js
  34. 364 357
      lib/core/image.js
  35. 109 8
      lib/core/image_utils.js
  36. 8 3
      lib/core/jpg.js
  37. 255 34
      lib/core/obj.js
  38. 17 15
      lib/core/operator_list.js
  39. 40 3
      lib/core/parser.js
  40. 24 11
      lib/core/pattern.js
  41. 112 53
      lib/core/primitives.js
  42. 2 0
      lib/core/type1_parser.js
  43. 102 37
      lib/core/worker.js
  44. 242 0
      lib/core/writer.js
  45. 51 10
      lib/display/annotation_layer.js
  46. 89 0
      lib/display/annotation_storage.js
  47. 140 54
      lib/display/api.js
  48. 215 46
      lib/display/canvas.js
  49. 55 18
      lib/display/display_utils.js
  50. 6 0
      lib/display/fetch_stream.js
  51. 26 15
      lib/display/font_loader.js
  52. 1 7
      lib/display/metadata.js
  53. 0 14
      lib/display/network.js
  54. 87 0
      lib/display/node_utils.js
  55. 184 0
      lib/display/optional_content_config.js
  56. 1 1
      lib/display/svg.js
  57. 4 1
      lib/display/text_layer.js
  58. 2 2
      lib/pdf.js
  59. 2 2
      lib/pdf.worker.js
  60. 1 1
      lib/shared/is_node.js
  61. 13 10
      lib/shared/util.js
  62. 520 50
      lib/test/unit/annotation_spec.js
  63. 83 0
      lib/test/unit/annotation_storage_spec.js
  64. 16 6
      lib/test/unit/api_spec.js
  65. 4 4
      lib/test/unit/cmap_spec.js
  66. 216 24
      lib/test/unit/colorspace_spec.js
  67. 51 4
      lib/test/unit/crypto_spec.js
  68. 129 1
      lib/test/unit/custom_spec.js
  69. 112 1
      lib/test/unit/document_spec.js
  70. 29 10
      lib/test/unit/evaluator_spec.js
  71. 1 1
      lib/test/unit/jasmine-boot.js
  72. 3 3
      lib/test/unit/metadata_spec.js
  73. 242 63
      lib/test/unit/primitives_spec.js
  74. 42 89
      lib/test/unit/test_utils.js
  75. 17 6
      lib/test/unit/testreporter.js
  76. 13 12
      lib/test/unit/util_spec.js
  77. 84 0
      lib/test/unit/writer_spec.js
  78. 13 9
      lib/web/annotation_layer_builder.js
  79. 231 26
      lib/web/app.js
  80. 1 6
      lib/web/app_options.js
  81. 100 0
      lib/web/base_tree_viewer.js
  82. 53 4
      lib/web/base_viewer.js
  83. 3 10
      lib/web/download_manager.js
  84. 32 12
      lib/web/firefox_print_service.js
  85. 20 6
      lib/web/firefoxcom.js
  86. 1 1
      lib/web/interfaces.js
  87. 80 38
      lib/web/pdf_attachment_viewer.js
  88. 2 1
      lib/web/pdf_find_controller.js
  89. 208 0
      lib/web/pdf_layer_viewer.js
  90. 24 57
      lib/web/pdf_outline_viewer.js
  91. 29 6
      lib/web/pdf_page_view.js
  92. 13 11
      lib/web/pdf_print_service.js
  93. 46 25
      lib/web/pdf_sidebar.js
  94. 16 3
      lib/web/pdf_thumbnail_view.js
  95. 17 1
      lib/web/pdf_thumbnail_viewer.js
  96. 2 2
      lib/web/pdf_viewer.component.js
  97. 1 1
      lib/web/preferences.js
  98. 1 1
      lib/web/toolbar.js
  99. 7 1
      lib/web/ui_utils.js
  100. 3 1
      package.json

+ 1 - 1
bower.json

@@ -1,6 +1,6 @@
 {
   "name": "pdfjs-dist",
-  "version": "2.5.207",
+  "version": "2.6.347",
   "main": [
     "build/pdf.js",
     "build/pdf.worker.js"

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 401 - 117
build/pdf.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
build/pdf.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
build/pdf.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 363 - 218
build/pdf.worker.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
build/pdf.worker.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
build/pdf.worker.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 449 - 229
es5/build/pdf.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
es5/build/pdf.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 21 - 0
es5/build/pdf.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 558 - 435
es5/build/pdf.worker.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
es5/build/pdf.worker.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 21 - 0
es5/build/pdf.worker.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 8192 - 0
es5/image_decoders/pdf.image_decoders.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
es5/image_decoders/pdf.image_decoders.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 21 - 0
es5/image_decoders/pdf.image_decoders.min.js


BIN
es5/web/images/shadow.png


BIN
es5/web/images/texture.png


+ 8 - 5
es5/web/pdf_viewer.css

@@ -21,7 +21,7 @@
   bottom: 0;
   overflow: hidden;
   opacity: 0.2;
-  line-height: 1.0;
+  line-height: 1;
 }
 
 .textLayer > span {
@@ -168,7 +168,7 @@
 .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after,
 .annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before {
   background-color: rgba(0, 0, 0, 1);
-  content: '';
+  content: "";
   display: block;
   position: absolute;
 }
@@ -308,13 +308,16 @@
   border: none;
 }
 
-.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped, .spread {
+.pdfViewer.scrollHorizontal,
+.pdfViewer.scrollWrapped,
+.spread {
   margin-left: 3.5px;
   margin-right: 3.5px;
   text-align: center;
 }
 
-.pdfViewer.scrollHorizontal, .spread {
+.pdfViewer.scrollHorizontal,
+.spread {
   white-space: nowrap;
 }
 
@@ -364,7 +367,7 @@
   top: 0;
   right: 0;
   bottom: 0;
-  background: url('images/loading-icon.gif') center no-repeat;
+  background: url("images/loading-icon.gif") center no-repeat;
 }
 
 .pdfPresentationMode .pdfViewer {

+ 195 - 51
es5/web/pdf_viewer.js

@@ -245,8 +245,8 @@ var _pdf_single_page_viewer = __w_pdfjs_require__(18);
 
 var _pdf_viewer = __w_pdfjs_require__(20);
 
-var pdfjsVersion = '2.5.207';
-var pdfjsBuild = '0974d605';
+var pdfjsVersion = '2.6.347';
+var pdfjsBuild = '3be9c65f';
 
 /***/ }),
 /* 1 */
@@ -278,10 +278,12 @@ var AnnotationLayerBuilder = /*#__PURE__*/function () {
         pdfPage = _ref.pdfPage,
         linkService = _ref.linkService,
         downloadManager = _ref.downloadManager,
+        _ref$annotationStorag = _ref.annotationStorage,
+        annotationStorage = _ref$annotationStorag === void 0 ? null : _ref$annotationStorag,
         _ref$imageResourcesPa = _ref.imageResourcesPath,
         imageResourcesPath = _ref$imageResourcesPa === void 0 ? "" : _ref$imageResourcesPa,
         _ref$renderInteractiv = _ref.renderInteractiveForms,
-        renderInteractiveForms = _ref$renderInteractiv === void 0 ? false : _ref$renderInteractiv,
+        renderInteractiveForms = _ref$renderInteractiv === void 0 ? true : _ref$renderInteractiv,
         _ref$l10n = _ref.l10n,
         l10n = _ref$l10n === void 0 ? _ui_utils.NullL10n : _ref$l10n;
 
@@ -294,6 +296,7 @@ var AnnotationLayerBuilder = /*#__PURE__*/function () {
     this.imageResourcesPath = imageResourcesPath;
     this.renderInteractiveForms = renderInteractiveForms;
     this.l10n = l10n;
+    this.annotationStorage = annotationStorage;
     this.div = null;
     this._cancelled = false;
   }
@@ -304,13 +307,17 @@ var AnnotationLayerBuilder = /*#__PURE__*/function () {
       var _this = this;
 
       var intent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "display";
-      this.pdfPage.getAnnotations({
+      return this.pdfPage.getAnnotations({
         intent: intent
       }).then(function (annotations) {
         if (_this._cancelled) {
           return;
         }
 
+        if (annotations.length === 0) {
+          return;
+        }
+
         var parameters = {
           viewport: viewport.clone({
             dontFlip: true
@@ -321,16 +328,13 @@ var AnnotationLayerBuilder = /*#__PURE__*/function () {
           imageResourcesPath: _this.imageResourcesPath,
           renderInteractiveForms: _this.renderInteractiveForms,
           linkService: _this.linkService,
-          downloadManager: _this.downloadManager
+          downloadManager: _this.downloadManager,
+          annotationStorage: _this.annotationStorage
         };
 
         if (_this.div) {
           _pdfjsLib.AnnotationLayer.update(parameters);
         } else {
-          if (annotations.length === 0) {
-            return;
-          }
-
           _this.div = document.createElement("div");
           _this.div.className = "annotationLayer";
 
@@ -373,16 +377,18 @@ var DefaultAnnotationLayerFactory = /*#__PURE__*/function () {
   _createClass(DefaultAnnotationLayerFactory, [{
     key: "createAnnotationLayerBuilder",
     value: function createAnnotationLayerBuilder(pageDiv, pdfPage) {
-      var imageResourcesPath = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
-      var renderInteractiveForms = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
-      var l10n = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : _ui_utils.NullL10n;
+      var annotationStorage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+      var imageResourcesPath = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "";
+      var renderInteractiveForms = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
+      var l10n = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : _ui_utils.NullL10n;
       return new AnnotationLayerBuilder({
         pageDiv: pageDiv,
         pdfPage: pdfPage,
         imageResourcesPath: imageResourcesPath,
         renderInteractiveForms: renderInteractiveForms,
         linkService: new _pdf_link_service.SimpleLinkService(),
-        l10n: l10n
+        l10n: l10n,
+        annotationStorage: annotationStorage
       });
     }
   }]);
@@ -436,6 +442,7 @@ exports.getOutputScale = getOutputScale;
 exports.scrollIntoView = scrollIntoView;
 exports.watchScroll = watchScroll;
 exports.binarySearchFirstItem = binarySearchFirstItem;
+exports.normalizeWheelEventDirection = normalizeWheelEventDirection;
 exports.normalizeWheelEventDelta = normalizeWheelEventDelta;
 exports.waitOnEventOrTimeout = waitOnEventOrTimeout;
 exports.moveToEndOfArray = moveToEndOfArray;
@@ -959,7 +966,7 @@ function getPDFFileNameFromURL(url) {
   return suggestedFilename || defaultFilename;
 }
 
-function normalizeWheelEventDelta(evt) {
+function normalizeWheelEventDirection(evt) {
   var delta = Math.sqrt(evt.deltaX * evt.deltaX + evt.deltaY * evt.deltaY);
   var angle = Math.atan2(evt.deltaY, evt.deltaX);
 
@@ -967,6 +974,11 @@ function normalizeWheelEventDelta(evt) {
     delta = -delta;
   }
 
+  return delta;
+}
+
+function normalizeWheelEventDelta(evt) {
+  var delta = normalizeWheelEventDirection(evt);
   var MOUSE_DOM_DELTA_PIXEL_MODE = 0;
   var MOUSE_DOM_DELTA_LINE_MODE = 1;
   var MOUSE_PIXELS_PER_LINE = 30;
@@ -1282,6 +1294,24 @@ var runtime = function (exports) {
   var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator";
   var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag";
 
+  function define(obj, key, value) {
+    Object.defineProperty(obj, key, {
+      value: value,
+      enumerable: true,
+      configurable: true,
+      writable: true
+    });
+    return obj[key];
+  }
+
+  try {
+    define({}, "");
+  } catch (err) {
+    define = function define(obj, key, value) {
+      return obj[key] = value;
+    };
+  }
+
   function wrap(innerFn, outerFn, self, tryLocsList) {
     var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
     var generator = Object.create(protoGenerator.prototype);
@@ -1334,13 +1364,13 @@ var runtime = function (exports) {
   var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype);
   GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
   GeneratorFunctionPrototype.constructor = GeneratorFunction;
-  GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction";
+  GeneratorFunction.displayName = define(GeneratorFunctionPrototype, toStringTagSymbol, "GeneratorFunction");
 
   function defineIteratorMethods(prototype) {
     ["next", "throw", "return"].forEach(function (method) {
-      prototype[method] = function (arg) {
+      define(prototype, method, function (arg) {
         return this._invoke(method, arg);
-      };
+      });
     });
   }
 
@@ -1354,10 +1384,7 @@ var runtime = function (exports) {
       Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
     } else {
       genFun.__proto__ = GeneratorFunctionPrototype;
-
-      if (!(toStringTagSymbol in genFun)) {
-        genFun[toStringTagSymbol] = "GeneratorFunction";
-      }
+      define(genFun, toStringTagSymbol, "GeneratorFunction");
     }
 
     genFun.prototype = Object.create(Gp);
@@ -1553,7 +1580,7 @@ var runtime = function (exports) {
   }
 
   defineIteratorMethods(Gp);
-  Gp[toStringTagSymbol] = "Generator";
+  define(Gp, toStringTagSymbol, "Generator");
 
   Gp[iteratorSymbol] = function () {
     return this;
@@ -2802,7 +2829,6 @@ function _defineProperties(target, props) { for (var i = 0; i < props.length; i+
 function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
 
 ;
-var DISABLE_CREATE_OBJECT_URL = _viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL || false;
 
 function _download(blobUrl, filename) {
   var a = document.createElement("a");
@@ -2824,13 +2850,8 @@ function _download(blobUrl, filename) {
 }
 
 var DownloadManager = /*#__PURE__*/function () {
-  function DownloadManager(_ref) {
-    var _ref$disableCreateObj = _ref.disableCreateObjectURL,
-        disableCreateObjectURL = _ref$disableCreateObj === void 0 ? DISABLE_CREATE_OBJECT_URL : _ref$disableCreateObj;
-
+  function DownloadManager() {
     _classCallCheck(this, DownloadManager);
-
-    this.disableCreateObjectURL = disableCreateObjectURL;
   }
 
   _createClass(DownloadManager, [{
@@ -2852,13 +2873,15 @@ var DownloadManager = /*#__PURE__*/function () {
         return;
       }
 
-      var blobUrl = (0, _pdfjsLib.createObjectURL)(data, contentType, this.disableCreateObjectURL);
+      var blobUrl = (0, _pdfjsLib.createObjectURL)(data, contentType, _viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL);
 
       _download(blobUrl, filename);
     }
   }, {
     key: "download",
     value: function download(blob, url, filename) {
+      var sourceEventType = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "download";
+
       if (navigator.msSaveBlob) {
         if (!navigator.msSaveBlob(blob, filename)) {
           this.downloadUrl(url, filename);
@@ -2867,7 +2890,7 @@ var DownloadManager = /*#__PURE__*/function () {
         return;
       }
 
-      if (this.disableCreateObjectURL) {
+      if (_viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL) {
         this.downloadUrl(url, filename);
         return;
       }
@@ -4601,7 +4624,8 @@ var PDFFindController = /*#__PURE__*/function () {
         source: this,
         state: state,
         previous: previous,
-        matchesCount: this._requestMatchesCount()
+        matchesCount: this._requestMatchesCount(),
+        rawQuery: this._state ? this._state.query : null
       });
     }
   }, {
@@ -5441,10 +5465,12 @@ var PDFPageView = /*#__PURE__*/function () {
     this.scale = options.scale || _ui_utils.DEFAULT_SCALE;
     this.viewport = defaultViewport;
     this.pdfPageRotate = defaultViewport.rotation;
+    this._annotationStorage = options.annotationStorage || null;
+    this._optionalContentConfigPromise = options.optionalContentConfigPromise || null;
     this.hasRestrictedScaling = false;
     this.textLayerMode = Number.isInteger(options.textLayerMode) ? options.textLayerMode : _ui_utils.TextLayerMode.ENABLE;
     this.imageResourcesPath = options.imageResourcesPath || "";
-    this.renderInteractiveForms = options.renderInteractiveForms || false;
+    this.renderInteractiveForms = typeof options.renderInteractiveForms === "boolean" ? options.renderInteractiveForms : true;
     this.useOnlyCssZoom = options.useOnlyCssZoom || false;
     this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS;
     this.eventBus = options.eventBus;
@@ -5493,6 +5519,52 @@ var PDFPageView = /*#__PURE__*/function () {
         this.pdfPage.cleanup();
       }
     }
+  }, {
+    key: "_renderAnnotationLayer",
+    value: function () {
+      var _renderAnnotationLayer2 = _asyncToGenerator( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
+        var error;
+        return _regenerator["default"].wrap(function _callee$(_context) {
+          while (1) {
+            switch (_context.prev = _context.next) {
+              case 0:
+                error = null;
+                _context.prev = 1;
+                _context.next = 4;
+                return this.annotationLayer.render(this.viewport, "display");
+
+              case 4:
+                _context.next = 9;
+                break;
+
+              case 6:
+                _context.prev = 6;
+                _context.t0 = _context["catch"](1);
+                error = _context.t0;
+
+              case 9:
+                _context.prev = 9;
+                this.eventBus.dispatch("annotationlayerrendered", {
+                  source: this,
+                  pageNumber: this.id,
+                  error: error
+                });
+                return _context.finish(9);
+
+              case 12:
+              case "end":
+                return _context.stop();
+            }
+          }
+        }, _callee, this, [[1, 6, 9, 12]]);
+      }));
+
+      function _renderAnnotationLayer() {
+        return _renderAnnotationLayer2.apply(this, arguments);
+      }
+
+      return _renderAnnotationLayer;
+    }()
   }, {
     key: "_resetZoomLayer",
     value: function _resetZoomLayer() {
@@ -5569,12 +5641,17 @@ var PDFPageView = /*#__PURE__*/function () {
   }, {
     key: "update",
     value: function update(scale, rotation) {
+      var optionalContentConfigPromise = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
       this.scale = scale || this.scale;
 
       if (typeof rotation !== "undefined") {
         this.rotation = rotation;
       }
 
+      if (optionalContentConfigPromise instanceof Promise) {
+        this._optionalContentConfigPromise = optionalContentConfigPromise;
+      }
+
       var totalRotation = (this.rotation + this.pdfPageRotate) % 360;
       this.viewport = this.viewport.clone({
         scale: this.scale * _ui_utils.CSS_UNITS,
@@ -5713,7 +5790,7 @@ var PDFPageView = /*#__PURE__*/function () {
       }
 
       if (redrawAnnotations && this.annotationLayer) {
-        this.annotationLayer.render(this.viewport, "display");
+        this._renderAnnotationLayer();
       }
     }
   }, {
@@ -5795,22 +5872,22 @@ var PDFPageView = /*#__PURE__*/function () {
       }
 
       var finishPaintTask = /*#__PURE__*/function () {
-        var _ref = _asyncToGenerator( /*#__PURE__*/_regenerator["default"].mark(function _callee(error) {
-          return _regenerator["default"].wrap(function _callee$(_context) {
+        var _ref = _asyncToGenerator( /*#__PURE__*/_regenerator["default"].mark(function _callee2(error) {
+          return _regenerator["default"].wrap(function _callee2$(_context2) {
             while (1) {
-              switch (_context.prev = _context.next) {
+              switch (_context2.prev = _context2.next) {
                 case 0:
                   if (paintTask === _this.paintTask) {
                     _this.paintTask = null;
                   }
 
                   if (!(error instanceof _pdfjsLib.RenderingCancelledException)) {
-                    _context.next = 4;
+                    _context2.next = 4;
                     break;
                   }
 
                   _this.error = null;
-                  return _context.abrupt("return");
+                  return _context2.abrupt("return");
 
                 case 4:
                   _this.renderingState = _pdf_rendering_queue.RenderingStates.FINISHED;
@@ -5833,7 +5910,7 @@ var PDFPageView = /*#__PURE__*/function () {
                   });
 
                   if (!error) {
-                    _context.next = 12;
+                    _context2.next = 12;
                     break;
                   }
 
@@ -5841,10 +5918,10 @@ var PDFPageView = /*#__PURE__*/function () {
 
                 case 12:
                 case "end":
-                  return _context.stop();
+                  return _context2.stop();
               }
             }
-          }, _callee);
+          }, _callee2);
         }));
 
         return function finishPaintTask(_x) {
@@ -5871,10 +5948,10 @@ var PDFPageView = /*#__PURE__*/function () {
 
       if (this.annotationLayerFactory) {
         if (!this.annotationLayer) {
-          this.annotationLayer = this.annotationLayerFactory.createAnnotationLayerBuilder(div, pdfPage, this.imageResourcesPath, this.renderInteractiveForms, this.l10n);
+          this.annotationLayer = this.annotationLayerFactory.createAnnotationLayerBuilder(div, pdfPage, this._annotationStorage, this.imageResourcesPath, this.renderInteractiveForms, this.l10n);
         }
 
-        this.annotationLayer.render(this.viewport, "display");
+        this._renderAnnotationLayer();
       }
 
       div.setAttribute("data-loaded", true);
@@ -5959,7 +6036,8 @@ var PDFPageView = /*#__PURE__*/function () {
         transform: transform,
         viewport: this.viewport,
         enableWebGL: this.enableWebGL,
-        renderInteractiveForms: this.renderInteractiveForms
+        renderInteractiveForms: this.renderInteractiveForms,
+        optionalContentConfigPromise: this._optionalContentConfigPromise
       };
       var renderTask = this.pdfPage.render(renderContext);
 
@@ -6410,6 +6488,12 @@ var _pdf_link_service = __w_pdfjs_require__(7);
 
 var _text_layer_builder = __w_pdfjs_require__(8);
 
+function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
+
+function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
+
+function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
+
 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
 
 function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
@@ -6481,6 +6565,11 @@ var BaseViewer = /*#__PURE__*/function () {
     this._name = this.constructor.name;
     this.container = options.container;
     this.viewer = options.viewer || options.container.firstElementChild;
+
+    if (!(this.container instanceof HTMLDivElement && this.viewer instanceof HTMLDivElement)) {
+      throw new Error("Invalid `container` and/or `viewer` option.");
+    }
+
     this.eventBus = options.eventBus;
     this.linkService = options.linkService || new _pdf_link_service.SimpleLinkService();
     this.downloadManager = options.downloadManager || null;
@@ -6488,7 +6577,7 @@ var BaseViewer = /*#__PURE__*/function () {
     this.removePageBorders = options.removePageBorders || false;
     this.textLayerMode = Number.isInteger(options.textLayerMode) ? options.textLayerMode : _ui_utils.TextLayerMode.ENABLE;
     this.imageResourcesPath = options.imageResourcesPath || "";
-    this.renderInteractiveForms = options.renderInteractiveForms || false;
+    this.renderInteractiveForms = typeof options.renderInteractiveForms === "boolean" ? options.renderInteractiveForms : true;
     this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
     this.renderer = options.renderer || _ui_utils.RendererType.CANVAS;
     this.enableWebGL = options.enableWebGL || false;
@@ -6588,6 +6677,8 @@ var BaseViewer = /*#__PURE__*/function () {
 
       var pagesCount = pdfDocument.numPages;
       var firstPagePromise = pdfDocument.getPage(1);
+      var annotationStorage = pdfDocument.annotationStorage;
+      var optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
 
       this._pagesCapability.promise.then(function () {
         _this2.eventBus.dispatch("pagesloaded", {
@@ -6625,6 +6716,7 @@ var BaseViewer = /*#__PURE__*/function () {
       firstPagePromise.then(function (firstPdfPage) {
         _this2._firstPageCapability.resolve(firstPdfPage);
 
+        _this2._optionalContentConfigPromise = optionalContentConfigPromise;
         var scale = _this2.currentScale;
         var viewport = firstPdfPage.getViewport({
           scale: scale * _ui_utils.CSS_UNITS
@@ -6638,6 +6730,8 @@ var BaseViewer = /*#__PURE__*/function () {
             id: pageNum,
             scale: scale,
             defaultViewport: viewport.clone(),
+            annotationStorage: annotationStorage,
+            optionalContentConfigPromise: optionalContentConfigPromise,
             renderingQueue: _this2.renderingQueue,
             textLayerFactory: textLayerFactory,
             textLayerMode: _this2.textLayerMode,
@@ -6756,6 +6850,7 @@ var BaseViewer = /*#__PURE__*/function () {
       this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE);
       this._location = null;
       this._pagesRotation = 0;
+      this._optionalContentConfigPromise = null;
       this._pagesRequests = new WeakMap();
       this._firstPageCapability = (0, _pdfjsLib.createPromiseCapability)();
       this._onePageRenderedCapability = (0, _pdfjsLib.createPromiseCapability)();
@@ -7243,12 +7338,14 @@ var BaseViewer = /*#__PURE__*/function () {
   }, {
     key: "createAnnotationLayerBuilder",
     value: function createAnnotationLayerBuilder(pageDiv, pdfPage) {
-      var imageResourcesPath = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
-      var renderInteractiveForms = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
-      var l10n = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : _ui_utils.NullL10n;
+      var annotationStorage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
+      var imageResourcesPath = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : "";
+      var renderInteractiveForms = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
+      var l10n = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : _ui_utils.NullL10n;
       return new _annotation_layer_builder.AnnotationLayerBuilder({
         pageDiv: pageDiv,
         pdfPage: pdfPage,
+        annotationStorage: annotationStorage,
         imageResourcesPath: imageResourcesPath,
         renderInteractiveForms: renderInteractiveForms,
         linkService: this.linkService,
@@ -7274,9 +7371,8 @@ var BaseViewer = /*#__PURE__*/function () {
         return pagesOverview;
       }
 
-      var isFirstPagePortrait = (0, _ui_utils.isPortraitOrientation)(pagesOverview[0]);
       return pagesOverview.map(function (size) {
-        if (isFirstPagePortrait === (0, _ui_utils.isPortraitOrientation)(size)) {
+        if ((0, _ui_utils.isPortraitOrientation)(size)) {
           return size;
         }
 
@@ -7537,6 +7633,54 @@ var BaseViewer = /*#__PURE__*/function () {
 
       return true;
     }
+  }, {
+    key: "optionalContentConfigPromise",
+    get: function get() {
+      if (!this.pdfDocument) {
+        return Promise.resolve(null);
+      }
+
+      if (!this._optionalContentConfigPromise) {
+        return this.pdfDocument.getOptionalContentConfig();
+      }
+
+      return this._optionalContentConfigPromise;
+    },
+    set: function set(promise) {
+      if (!(promise instanceof Promise)) {
+        throw new Error("Invalid optionalContentConfigPromise: ".concat(promise));
+      }
+
+      if (!this.pdfDocument) {
+        return;
+      }
+
+      if (!this._optionalContentConfigPromise) {
+        return;
+      }
+
+      this._optionalContentConfigPromise = promise;
+
+      var _iterator = _createForOfIteratorHelper(this._pages),
+          _step;
+
+      try {
+        for (_iterator.s(); !(_step = _iterator.n()).done;) {
+          var pageView = _step.value;
+          pageView.update(pageView.scale, pageView.rotation, promise);
+        }
+      } catch (err) {
+        _iterator.e(err);
+      } finally {
+        _iterator.f();
+      }
+
+      this.update();
+      this.eventBus.dispatch("optionalcontentconfigchanged", {
+        source: this,
+        promise: promise
+      });
+    }
   }, {
     key: "scrollMode",
     get: function get() {

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
es5/web/pdf_viewer.js.map


+ 24 - 16
image_decoders/pdf.image_decoders.js

@@ -165,8 +165,8 @@ var _jpg = __w_pdfjs_require__(8);
 
 var _jpx = __w_pdfjs_require__(9);
 
-const pdfjsVersion = '2.5.207';
-const pdfjsBuild = '0974d605';
+const pdfjsVersion = '2.6.347';
+const pdfjsBuild = '3be9c65f';
 
 /***/ }),
 /* 1 */
@@ -183,12 +183,13 @@ exports.arraysToBytes = arraysToBytes;
 exports.assert = assert;
 exports.bytesToString = bytesToString;
 exports.createPromiseCapability = createPromiseCapability;
+exports.escapeString = escapeString;
+exports.getModificationDate = getModificationDate;
 exports.getVerbosityLevel = getVerbosityLevel;
 exports.info = info;
 exports.isArrayBuffer = isArrayBuffer;
 exports.isArrayEqual = isArrayEqual;
 exports.isBool = isBool;
-exports.isEmptyObj = isEmptyObj;
 exports.isNum = isNum;
 exports.isString = isString;
 exports.isSameOrigin = isSameOrigin;
@@ -487,7 +488,8 @@ const UNSUPPORTED_FEATURES = {
   errorOperatorList: "errorOperatorList",
   errorFontToUnicode: "errorFontToUnicode",
   errorFontLoadNative: "errorFontLoadNative",
-  errorFontGetPath: "errorFontGetPath"
+  errorFontGetPath: "errorFontGetPath",
+  errorMarkedContent: "errorMarkedContent"
 };
 exports.UNSUPPORTED_FEATURES = UNSUPPORTED_FEATURES;
 const PasswordResponses = {
@@ -902,6 +904,10 @@ function stringToPDFString(str) {
   return strBuf.join("");
 }
 
+function escapeString(str) {
+  return str.replace(/([\(\)\\])/g, "\\$1");
+}
+
 function stringToUTF8String(str) {
   return decodeURIComponent(escape(str));
 }
@@ -910,14 +916,6 @@ function utf8StringToString(str) {
   return unescape(encodeURIComponent(str));
 }
 
-function isEmptyObj(obj) {
-  for (const key in obj) {
-    return false;
-  }
-
-  return true;
-}
-
 function isBool(v) {
   return typeof v === "boolean";
 }
@@ -944,6 +942,11 @@ function isArrayEqual(arr1, arr2) {
   });
 }
 
+function getModificationDate(date = new Date(Date.now())) {
+  const buffer = [date.getUTCFullYear().toString(), (date.getUTCMonth() + 1).toString().padStart(2, "0"), (date.getUTCDate() + 1).toString().padStart(2, "0"), date.getUTCHours().toString().padStart(2, "0"), date.getUTCMinutes().toString().padStart(2, "0"), date.getUTCSeconds().toString().padStart(2, "0")];
+  return buffer.join("");
+}
+
 function createPromiseCapability() {
   const capability = Object.create(null);
   let isSettled = false;
@@ -1018,7 +1021,7 @@ Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.isNodeJS = void 0;
-const isNodeJS = typeof process === "object" && process + "" === "[object process]" && !process.versions.nw && !process.versions.electron;
+const isNodeJS = typeof process === "object" && process + "" === "[object process]" && !process.versions.nw && !(process.versions.electron && process.type && process.type !== "browser");
 exports.isNodeJS = isNodeJS;
 
 /***/ }),
@@ -4565,9 +4568,9 @@ var JpegImage = function JpegImageClosure() {
             }
           } else if (nextByte === 0xd9) {
             if (parseDNLMarker) {
-              const maybeScanLines = blockRow * 8;
+              const maybeScanLines = blockRow * (frame.precision === 8 ? 8 : 0);
 
-              if (maybeScanLines > 0 && maybeScanLines < frame.scanLines / 10) {
+              if (maybeScanLines > 0 && Math.round(frame.scanLines / maybeScanLines) >= 10) {
                 throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, " + "possibly caused by incorrect `scanLines` parameter", maybeScanLines);
               }
             }
@@ -5387,8 +5390,10 @@ var JpegImage = function JpegImageClosure() {
                 component;
 
             for (i = 0; i < selectorsCount; i++) {
-              var componentIndex = frame.componentIds[data[offset++]];
+              const index = data[offset++];
+              var componentIndex = frame.componentIds[index];
               component = frame.components[componentIndex];
+              component.index = index;
               var tableSpec = data[offset++];
               component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4];
               component.huffmanTableAC = huffmanTablesAC[tableSpec & 15];
@@ -5465,6 +5470,7 @@ var JpegImage = function JpegImageClosure() {
         }
 
         this.components.push({
+          index: component.index,
           output: buildComponentData(frame, component),
           scaleX: component.h / frame.maxH,
           scaleY: component.v / frame.maxV,
@@ -5545,6 +5551,8 @@ var JpegImage = function JpegImageClosure() {
       if (this.numComponents === 3) {
         if (this._colorTransform === 0) {
           return false;
+        } else if (this.components[0].index === 0x52 && this.components[1].index === 0x47 && this.components[2].index === 0x42) {
+          return false;
         }
 
         return true;

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
image_decoders/pdf.image_decoders.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
image_decoders/pdf.image_decoders.min.js


+ 479 - 43
lib/core/annotation.js

@@ -41,12 +41,16 @@ var _operator_list = require("./operator_list.js");
 
 var _stream = require("./stream.js");
 
+var _writer = require("./writer.js");
+
 class AnnotationFactory {
   static create(xref, ref, pdfManager, idFactory) {
-    return pdfManager.ensure(this, "_create", [xref, ref, pdfManager, idFactory]);
+    return pdfManager.ensureCatalog("acroForm").then(acroForm => {
+      return pdfManager.ensure(this, "_create", [xref, ref, pdfManager, idFactory, acroForm]);
+    });
   }
 
-  static _create(xref, ref, pdfManager, idFactory) {
+  static _create(xref, ref, pdfManager, idFactory, acroForm) {
     const dict = xref.fetchIfRef(ref);
 
     if (!(0, _primitives.isDict)(dict)) {
@@ -58,10 +62,12 @@ class AnnotationFactory {
     subtype = (0, _primitives.isName)(subtype) ? subtype.name : null;
     const parameters = {
       xref,
+      ref,
       dict,
       subtype,
       id,
-      pdfManager
+      pdfManager,
+      acroForm: acroForm instanceof _primitives.Dict ? acroForm : _primitives.Dict.empty
     };
 
     switch (subtype) {
@@ -384,13 +390,14 @@ class Annotation {
     });
   }
 
-  getOperatorList(evaluator, task, renderForms) {
+  getOperatorList(evaluator, task, renderForms, annotationStorage) {
     if (!this.appearance) {
       return Promise.resolve(new _operator_list.OperatorList());
     }
 
+    const appearance = this.appearance;
     const data = this.data;
-    const appearanceDict = this.appearance.dict;
+    const appearanceDict = appearance.dict;
     const resourcesPromise = this.loadResources(["ExtGState", "ColorSpace", "Pattern", "Shading", "XObject", "Font"]);
     const bbox = appearanceDict.getArray("BBox") || [0, 0, 1, 1];
     const matrix = appearanceDict.getArray("Matrix") || [1, 0, 0, 1, 0, 0];
@@ -399,18 +406,22 @@ class Annotation {
       const opList = new _operator_list.OperatorList();
       opList.addOp(_util.OPS.beginAnnotation, [data.rect, transform, matrix]);
       return evaluator.getOperatorList({
-        stream: this.appearance,
+        stream: appearance,
         task,
         resources,
         operatorList: opList
       }).then(() => {
         opList.addOp(_util.OPS.endAnnotation, []);
-        this.appearance.reset();
+        appearance.reset();
         return opList;
       });
     });
   }
 
+  async save(evaluator, task, annotationStorage) {
+    return null;
+  }
+
 }
 
 exports.Annotation = Annotation;
@@ -583,18 +594,20 @@ class WidgetAnnotation extends Annotation {
     super(params);
     const dict = params.dict;
     const data = this.data;
+    this.ref = params.ref;
     data.annotationType = _util.AnnotationType.WIDGET;
     data.fieldName = this._constructFieldName(dict);
-    data.fieldValue = (0, _core_utils.getInheritableProperty)({
+    const fieldValue = (0, _core_utils.getInheritableProperty)({
       dict,
       key: "V",
       getArray: true
     });
+    data.fieldValue = this._decodeFormValue(fieldValue);
     data.alternativeText = (0, _util.stringToPDFString)(dict.get("TU") || "");
     data.defaultAppearance = (0, _core_utils.getInheritableProperty)({
       dict,
       key: "DA"
-    }) || "";
+    }) || params.acroForm.get("DA") || "";
     const fieldType = (0, _core_utils.getInheritableProperty)({
       dict,
       key: "FT"
@@ -603,7 +616,7 @@ class WidgetAnnotation extends Annotation {
     this.fieldResources = (0, _core_utils.getInheritableProperty)({
       dict,
       key: "DR"
-    }) || _primitives.Dict.empty;
+    }) || params.acroForm.get("DR") || _primitives.Dict.empty;
     data.fieldFlags = (0, _core_utils.getInheritableProperty)({
       dict,
       key: "Ff"
@@ -654,16 +667,225 @@ class WidgetAnnotation extends Annotation {
     return fieldName.join(".");
   }
 
+  _decodeFormValue(formValue) {
+    if (Array.isArray(formValue)) {
+      return formValue.filter(item => (0, _util.isString)(item)).map(item => (0, _util.stringToPDFString)(item));
+    } else if ((0, _primitives.isName)(formValue)) {
+      return (0, _util.stringToPDFString)(formValue.name);
+    } else if ((0, _util.isString)(formValue)) {
+      return (0, _util.stringToPDFString)(formValue);
+    }
+
+    return null;
+  }
+
   hasFieldFlag(flag) {
     return !!(this.data.fieldFlags & flag);
   }
 
-  getOperatorList(evaluator, task, renderForms) {
+  getOperatorList(evaluator, task, renderForms, annotationStorage) {
     if (renderForms) {
       return Promise.resolve(new _operator_list.OperatorList());
     }
 
-    return super.getOperatorList(evaluator, task, renderForms);
+    if (!this._hasText) {
+      return super.getOperatorList(evaluator, task, renderForms, annotationStorage);
+    }
+
+    return this._getAppearance(evaluator, task, annotationStorage).then(content => {
+      if (this.appearance && content === null) {
+        return super.getOperatorList(evaluator, task, renderForms, annotationStorage);
+      }
+
+      const operatorList = new _operator_list.OperatorList();
+
+      if (!this.data.defaultAppearance || content === null) {
+        return operatorList;
+      }
+
+      const matrix = [1, 0, 0, 1, 0, 0];
+      const bbox = [0, 0, this.data.rect[2] - this.data.rect[0], this.data.rect[3] - this.data.rect[1]];
+      const transform = getTransformMatrix(this.data.rect, bbox, matrix);
+      operatorList.addOp(_util.OPS.beginAnnotation, [this.data.rect, transform, matrix]);
+      const stream = new _stream.StringStream(content);
+      return evaluator.getOperatorList({
+        stream,
+        task,
+        resources: this.fieldResources,
+        operatorList
+      }).then(function () {
+        operatorList.addOp(_util.OPS.endAnnotation, []);
+        return operatorList;
+      });
+    });
+  }
+
+  async save(evaluator, task, annotationStorage) {
+    if (this.data.fieldValue === annotationStorage[this.data.id]) {
+      return null;
+    }
+
+    let appearance = await this._getAppearance(evaluator, task, annotationStorage);
+
+    if (appearance === null) {
+      return null;
+    }
+
+    const dict = evaluator.xref.fetchIfRef(this.ref);
+
+    if (!(0, _primitives.isDict)(dict)) {
+      return null;
+    }
+
+    const bbox = [0, 0, this.data.rect[2] - this.data.rect[0], this.data.rect[3] - this.data.rect[1]];
+    const newRef = evaluator.xref.getNewRef();
+    const AP = new _primitives.Dict(evaluator.xref);
+    AP.set("N", newRef);
+    const value = annotationStorage[this.data.id];
+    const encrypt = evaluator.xref.encrypt;
+    let originalTransform = null;
+    let newTransform = null;
+
+    if (encrypt) {
+      originalTransform = encrypt.createCipherTransform(this.ref.num, this.ref.gen);
+      newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
+      appearance = newTransform.encryptString(appearance);
+    }
+
+    dict.set("V", value);
+    dict.set("AP", AP);
+    dict.set("M", `D:${(0, _util.getModificationDate)()}`);
+    const appearanceDict = new _primitives.Dict(evaluator.xref);
+    appearanceDict.set("Length", appearance.length);
+    appearanceDict.set("Subtype", _primitives.Name.get("Form"));
+    appearanceDict.set("Resources", this.fieldResources);
+    appearanceDict.set("BBox", bbox);
+    const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`];
+    (0, _writer.writeDict)(dict, bufferOriginal, originalTransform);
+    bufferOriginal.push("\nendobj\n");
+    const bufferNew = [`${newRef.num} ${newRef.gen} obj\n`];
+    (0, _writer.writeDict)(appearanceDict, bufferNew, newTransform);
+    bufferNew.push(" stream\n");
+    bufferNew.push(appearance);
+    bufferNew.push("\nendstream\nendobj\n");
+    return [{
+      ref: this.ref,
+      data: bufferOriginal.join("")
+    }, {
+      ref: newRef,
+      data: bufferNew.join("")
+    }];
+  }
+
+  async _getAppearance(evaluator, task, annotationStorage) {
+    const isPassword = this.hasFieldFlag(_util.AnnotationFieldFlag.PASSWORD);
+
+    if (!annotationStorage || isPassword) {
+      return null;
+    }
+
+    const value = annotationStorage[this.data.id];
+
+    if (value === "") {
+      return "";
+    }
+
+    const defaultPadding = 2;
+    const hPadding = defaultPadding;
+    const totalHeight = this.data.rect[3] - this.data.rect[1];
+    const totalWidth = this.data.rect[2] - this.data.rect[0];
+    const fontInfo = await this._getFontData(evaluator, task);
+    const [font, fontName] = fontInfo;
+    let fontSize = fontInfo[2];
+    fontSize = this._computeFontSize(font, fontName, fontSize, totalHeight);
+    let descent = font.descent;
+
+    if (isNaN(descent)) {
+      descent = 0;
+    }
+
+    const vPadding = defaultPadding + Math.abs(descent) * fontSize;
+    const defaultAppearance = this.data.defaultAppearance;
+    const alignment = this.data.textAlignment;
+
+    if (this.data.comb) {
+      return this._getCombAppearance(defaultAppearance, value, totalWidth, hPadding, vPadding);
+    }
+
+    if (this.data.multiLine) {
+      return this._getMultilineAppearance(defaultAppearance, value, font, fontSize, totalWidth, totalHeight, alignment, hPadding, vPadding);
+    }
+
+    if (alignment === 0 || alignment > 2) {
+      return "/Tx BMC q BT " + defaultAppearance + ` 1 0 0 1 ${hPadding} ${vPadding} Tm (${(0, _util.escapeString)(value)}) Tj` + " ET Q EMC";
+    }
+
+    const renderedText = this._renderText(value, font, fontSize, totalWidth, alignment, hPadding, vPadding);
+
+    return "/Tx BMC q BT " + defaultAppearance + ` 1 0 0 1 0 0 Tm ${renderedText}` + " ET Q EMC";
+  }
+
+  async _getFontData(evaluator, task) {
+    const operatorList = new _operator_list.OperatorList();
+    const initialState = {
+      fontSize: 0,
+      font: null,
+      fontName: null,
+
+      clone() {
+        return this;
+      }
+
+    };
+    await evaluator.getOperatorList({
+      stream: new _stream.StringStream(this.data.defaultAppearance),
+      task,
+      resources: this.fieldResources,
+      operatorList,
+      initialState
+    });
+    return [initialState.font, initialState.fontName, initialState.fontSize];
+  }
+
+  _computeFontSize(font, fontName, fontSize, height) {
+    if (fontSize === null || fontSize === 0) {
+      const em = font.charsToGlyphs("M", true)[0].width / 1000;
+      const capHeight = 0.7 * em;
+      fontSize = Math.max(1, Math.floor(height / (1.5 * capHeight)));
+      let fontRegex = new RegExp(`/${fontName}\\s+[0-9\.]+\\s+Tf`);
+
+      if (this.data.defaultAppearance.search(fontRegex) === -1) {
+        fontRegex = new RegExp(`/${fontName}\\s+Tf`);
+      }
+
+      this.data.defaultAppearance = this.data.defaultAppearance.replace(fontRegex, `/${fontName} ${fontSize} Tf`);
+    }
+
+    return fontSize;
+  }
+
+  _renderText(text, font, fontSize, totalWidth, alignment, hPadding, vPadding) {
+    const glyphs = font.charsToGlyphs(text);
+    const scale = fontSize / 1000;
+    let width = 0;
+
+    for (const glyph of glyphs) {
+      width += glyph.width * scale;
+    }
+
+    let shift;
+
+    if (alignment === 1) {
+      shift = (totalWidth - width) / 2;
+    } else if (alignment === 2) {
+      shift = totalWidth - width - hPadding;
+    } else {
+      shift = hPadding;
+    }
+
+    shift = shift.toFixed(2);
+    vPadding = vPadding.toFixed(2);
+    return `${shift} ${vPadding} Td (${(0, _util.escapeString)(text)}) Tj`;
   }
 
 }
@@ -671,8 +893,13 @@ class WidgetAnnotation extends Annotation {
 class TextWidgetAnnotation extends WidgetAnnotation {
   constructor(params) {
     super(params);
+    this._hasText = true;
     const dict = params.dict;
-    this.data.fieldValue = (0, _util.stringToPDFString)(this.data.fieldValue || "");
+
+    if (!(0, _util.isString)(this.data.fieldValue)) {
+      this.data.fieldValue = "";
+    }
+
     let alignment = (0, _core_utils.getInheritableProperty)({
       dict,
       key: "Q"
@@ -697,26 +924,86 @@ class TextWidgetAnnotation extends WidgetAnnotation {
     this.data.comb = this.hasFieldFlag(_util.AnnotationFieldFlag.COMB) && !this.hasFieldFlag(_util.AnnotationFieldFlag.MULTILINE) && !this.hasFieldFlag(_util.AnnotationFieldFlag.PASSWORD) && !this.hasFieldFlag(_util.AnnotationFieldFlag.FILESELECT) && this.data.maxLen !== null;
   }
 
-  getOperatorList(evaluator, task, renderForms) {
-    if (renderForms || this.appearance) {
-      return super.getOperatorList(evaluator, task, renderForms);
+  _getCombAppearance(defaultAppearance, text, width, hPadding, vPadding) {
+    const combWidth = (width / this.data.maxLen).toFixed(2);
+    const buf = [];
+
+    for (const character of text) {
+      buf.push(`(${(0, _util.escapeString)(character)}) Tj`);
+    }
+
+    const renderedComb = buf.join(` ${combWidth} 0 Td `);
+    return "/Tx BMC q BT " + defaultAppearance + ` 1 0 0 1 ${hPadding} ${vPadding} Tm ${renderedComb}` + " ET Q EMC";
+  }
+
+  _getMultilineAppearance(defaultAppearance, text, font, fontSize, width, height, alignment, hPadding, vPadding) {
+    const lines = text.split(/\r\n|\r|\n/);
+    const buf = [];
+    const totalWidth = width - 2 * hPadding;
+
+    for (const line of lines) {
+      const chunks = this._splitLine(line, font, fontSize, totalWidth);
+
+      for (const chunk of chunks) {
+        const padding = buf.length === 0 ? hPadding : 0;
+        buf.push(this._renderText(chunk, font, fontSize, width, alignment, padding, -fontSize));
+      }
     }
 
-    const operatorList = new _operator_list.OperatorList();
+    const renderedText = buf.join("\n");
+    return "/Tx BMC q BT " + defaultAppearance + ` 1 0 0 1 0 ${height} Tm ${renderedText}` + " ET Q EMC";
+  }
 
-    if (!this.data.defaultAppearance) {
-      return Promise.resolve(operatorList);
+  _splitLine(line, font, fontSize, width) {
+    if (line.length <= 1) {
+      return [line];
     }
 
-    const stream = new _stream.Stream((0, _util.stringToBytes)(this.data.defaultAppearance));
-    return evaluator.getOperatorList({
-      stream,
-      task,
-      resources: this.fieldResources,
-      operatorList
-    }).then(function () {
-      return operatorList;
-    });
+    const scale = fontSize / 1000;
+    const whitespace = font.charsToGlyphs(" ", true)[0].width * scale;
+    const chunks = [];
+    let lastSpacePos = -1,
+        startChunk = 0,
+        currentWidth = 0;
+
+    for (let i = 0, ii = line.length; i < ii; i++) {
+      const character = line.charAt(i);
+
+      if (character === " ") {
+        if (currentWidth + whitespace > width) {
+          chunks.push(line.substring(startChunk, i));
+          startChunk = i;
+          currentWidth = whitespace;
+          lastSpacePos = -1;
+        } else {
+          currentWidth += whitespace;
+          lastSpacePos = i;
+        }
+      } else {
+        const charWidth = font.charsToGlyphs(character, false)[0].width * scale;
+
+        if (currentWidth + charWidth > width) {
+          if (lastSpacePos !== -1) {
+            chunks.push(line.substring(startChunk, lastSpacePos + 1));
+            startChunk = i = lastSpacePos + 1;
+            lastSpacePos = -1;
+            currentWidth = 0;
+          } else {
+            chunks.push(line.substring(startChunk, i));
+            startChunk = i;
+            currentWidth = charWidth;
+          }
+        } else {
+          currentWidth += charWidth;
+        }
+      }
+    }
+
+    if (startChunk < line.length) {
+      chunks.push(line.substring(startChunk, line.length));
+    }
+
+    return chunks;
   }
 
 }
@@ -724,6 +1011,8 @@ class TextWidgetAnnotation extends WidgetAnnotation {
 class ButtonWidgetAnnotation extends WidgetAnnotation {
   constructor(params) {
     super(params);
+    this.checkedAppearance = null;
+    this.uncheckedAppearance = null;
     this.data.checkBox = !this.hasFieldFlag(_util.AnnotationFieldFlag.RADIO) && !this.hasFieldFlag(_util.AnnotationFieldFlag.PUSHBUTTON);
     this.data.radioButton = this.hasFieldFlag(_util.AnnotationFieldFlag.RADIO) && !this.hasFieldFlag(_util.AnnotationFieldFlag.PUSHBUTTON);
     this.data.pushButton = this.hasFieldFlag(_util.AnnotationFieldFlag.PUSHBUTTON);
@@ -739,31 +1028,171 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
     }
   }
 
-  _processCheckBox(params) {
-    if ((0, _primitives.isName)(this.data.fieldValue)) {
-      this.data.fieldValue = this.data.fieldValue.name;
+  getOperatorList(evaluator, task, renderForms, annotationStorage) {
+    if (this.data.pushButton) {
+      return super.getOperatorList(evaluator, task, false, annotationStorage);
+    }
+
+    if (annotationStorage) {
+      const value = annotationStorage[this.data.id] || false;
+      let appearance;
+
+      if (value) {
+        appearance = this.checkedAppearance;
+      } else {
+        appearance = this.uncheckedAppearance;
+      }
+
+      if (appearance) {
+        const savedAppearance = this.appearance;
+        this.appearance = appearance;
+        const operatorList = super.getOperatorList(evaluator, task, renderForms, annotationStorage);
+        this.appearance = savedAppearance;
+        return operatorList;
+      }
+
+      return Promise.resolve(new _operator_list.OperatorList());
     }
 
+    return super.getOperatorList(evaluator, task, renderForms, annotationStorage);
+  }
+
+  async save(evaluator, task, annotationStorage) {
+    if (this.data.checkBox) {
+      return this._saveCheckbox(evaluator, task, annotationStorage);
+    }
+
+    if (this.data.radioButton) {
+      return this._saveRadioButton(evaluator, task, annotationStorage);
+    }
+
+    return super.save(evaluator, task, annotationStorage);
+  }
+
+  async _saveCheckbox(evaluator, task, annotationStorage) {
+    const defaultValue = this.data.fieldValue && this.data.fieldValue !== "Off";
+    const value = annotationStorage[this.data.id];
+
+    if (defaultValue === value) {
+      return null;
+    }
+
+    const dict = evaluator.xref.fetchIfRef(this.ref);
+
+    if (!(0, _primitives.isDict)(dict)) {
+      return null;
+    }
+
+    const name = _primitives.Name.get(value ? this.data.exportValue : "Off");
+
+    dict.set("V", name);
+    dict.set("AS", name);
+    dict.set("M", `D:${(0, _util.getModificationDate)()}`);
+    const encrypt = evaluator.xref.encrypt;
+    let originalTransform = null;
+
+    if (encrypt) {
+      originalTransform = encrypt.createCipherTransform(this.ref.num, this.ref.gen);
+    }
+
+    const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
+    (0, _writer.writeDict)(dict, buffer, originalTransform);
+    buffer.push("\nendobj\n");
+    return [{
+      ref: this.ref,
+      data: buffer.join("")
+    }];
+  }
+
+  async _saveRadioButton(evaluator, task, annotationStorage) {
+    const defaultValue = this.data.fieldValue === this.data.buttonValue;
+    const value = annotationStorage[this.data.id];
+
+    if (defaultValue === value) {
+      return null;
+    }
+
+    const dict = evaluator.xref.fetchIfRef(this.ref);
+
+    if (!(0, _primitives.isDict)(dict)) {
+      return null;
+    }
+
+    const name = _primitives.Name.get(value ? this.data.buttonValue : "Off");
+
+    let parentBuffer = null;
+    const encrypt = evaluator.xref.encrypt;
+
+    if (value) {
+      if ((0, _primitives.isRef)(this.parent)) {
+        const parent = evaluator.xref.fetch(this.parent);
+        let parentTransform = null;
+
+        if (encrypt) {
+          parentTransform = encrypt.createCipherTransform(this.parent.num, this.parent.gen);
+        }
+
+        parent.set("V", name);
+        parentBuffer = [`${this.parent.num} ${this.parent.gen} obj\n`];
+        (0, _writer.writeDict)(parent, parentBuffer, parentTransform);
+        parentBuffer.push("\nendobj\n");
+      } else if ((0, _primitives.isDict)(this.parent)) {
+        this.parent.set("V", name);
+      }
+    }
+
+    dict.set("AS", name);
+    dict.set("M", `D:${(0, _util.getModificationDate)()}`);
+    let originalTransform = null;
+
+    if (encrypt) {
+      originalTransform = encrypt.createCipherTransform(this.ref.num, this.ref.gen);
+    }
+
+    const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
+    (0, _writer.writeDict)(dict, buffer, originalTransform);
+    buffer.push("\nendobj\n");
+    const newRefs = [{
+      ref: this.ref,
+      data: buffer.join("")
+    }];
+
+    if (parentBuffer !== null) {
+      newRefs.push({
+        ref: this.parent,
+        data: parentBuffer.join("")
+      });
+    }
+
+    return newRefs;
+  }
+
+  _processCheckBox(params) {
     const customAppearance = params.dict.get("AP");
 
     if (!(0, _primitives.isDict)(customAppearance)) {
       return;
     }
 
-    const exportValueOptionsDict = customAppearance.get("D");
+    const normalAppearance = customAppearance.get("N");
 
-    if (!(0, _primitives.isDict)(exportValueOptionsDict)) {
+    if (!(0, _primitives.isDict)(normalAppearance)) {
       return;
     }
 
-    const exportValues = exportValueOptionsDict.getKeys();
-    const hasCorrectOptionCount = exportValues.length === 2;
+    const exportValues = normalAppearance.getKeys();
 
-    if (!hasCorrectOptionCount) {
+    if (!exportValues.includes("Off")) {
+      exportValues.push("Off");
+    }
+
+    if (exportValues.length !== 2) {
       return;
     }
 
     this.data.exportValue = exportValues[0] === "Off" ? exportValues[1] : exportValues[0];
+    this.checkedAppearance = normalAppearance.get(this.data.exportValue);
+    this.uncheckedAppearance = normalAppearance.get("Off") || null;
   }
 
   _processRadioButton(params) {
@@ -774,7 +1203,8 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
       const fieldParentValue = fieldParent.get("V");
 
       if ((0, _primitives.isName)(fieldParentValue)) {
-        this.data.fieldValue = fieldParentValue.name;
+        this.parent = params.dict.getRaw("Parent");
+        this.data.fieldValue = this._decodeFormValue(fieldParentValue);
       }
     }
 
@@ -784,18 +1214,21 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
       return;
     }
 
-    const normalAppearanceState = appearanceStates.get("N");
+    const normalAppearance = appearanceStates.get("N");
 
-    if (!(0, _primitives.isDict)(normalAppearanceState)) {
+    if (!(0, _primitives.isDict)(normalAppearance)) {
       return;
     }
 
-    for (const key of normalAppearanceState.getKeys()) {
+    for (const key of normalAppearance.getKeys()) {
       if (key !== "Off") {
         this.data.buttonValue = key;
         break;
       }
     }
+
+    this.checkedAppearance = normalAppearance.get(this.data.buttonValue);
+    this.uncheckedAppearance = normalAppearance.get("Off") || null;
   }
 
   _processPushButton(params) {
@@ -829,18 +1262,21 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
         const option = xref.fetchIfRef(options[i]);
         const isOptionArray = Array.isArray(option);
         this.data.options[i] = {
-          exportValue: isOptionArray ? xref.fetchIfRef(option[0]) : option,
-          displayValue: (0, _util.stringToPDFString)(isOptionArray ? xref.fetchIfRef(option[1]) : option)
+          exportValue: this._decodeFormValue(isOptionArray ? xref.fetchIfRef(option[0]) : option),
+          displayValue: this._decodeFormValue(isOptionArray ? xref.fetchIfRef(option[1]) : option)
         };
       }
     }
 
-    if (!Array.isArray(this.data.fieldValue)) {
+    if ((0, _util.isString)(this.data.fieldValue)) {
       this.data.fieldValue = [this.data.fieldValue];
+    } else if (!this.data.fieldValue) {
+      this.data.fieldValue = [];
     }
 
     this.data.combo = this.hasFieldFlag(_util.AnnotationFieldFlag.COMBO);
     this.data.multiSelect = this.hasFieldFlag(_util.AnnotationFieldFlag.MULTISELECT);
+    this._hasText = true;
   }
 
 }

+ 643 - 615
lib/core/cff_parser.js

@@ -248,14 +248,14 @@ var CFFParser = function CFFParserClosure() {
     resetStack: true
   }];
 
-  function CFFParser(file, properties, seacAnalysisEnabled) {
-    this.bytes = file.getBytes();
-    this.properties = properties;
-    this.seacAnalysisEnabled = !!seacAnalysisEnabled;
-  }
+  class CFFParser {
+    constructor(file, properties, seacAnalysisEnabled) {
+      this.bytes = file.getBytes();
+      this.properties = properties;
+      this.seacAnalysisEnabled = !!seacAnalysisEnabled;
+    }
 
-  CFFParser.prototype = {
-    parse: function CFFParser_parse() {
+    parse() {
       var properties = this.properties;
       var cff = new CFF();
       this.cff = cff;
@@ -323,8 +323,9 @@ var CFFParser = function CFFParserClosure() {
       cff.seacs = charStringsAndSeacs.seacs;
       cff.widths = charStringsAndSeacs.widths;
       return cff;
-    },
-    parseHeader: function CFFParser_parseHeader() {
+    }
+
+    parseHeader() {
       var bytes = this.bytes;
       var bytesLength = bytes.length;
       var offset = 0;
@@ -352,8 +353,9 @@ var CFFParser = function CFFParserClosure() {
         obj: header,
         endPos: hdrSize
       };
-    },
-    parseDict: function CFFParser_parseDict(dict) {
+    }
+
+    parseDict(dict) {
       var pos = 0;
 
       function parseOperand() {
@@ -432,8 +434,9 @@ var CFFParser = function CFFParserClosure() {
       }
 
       return entries;
-    },
-    parseIndex: function CFFParser_parseIndex(pos) {
+    }
+
+    parseIndex(pos) {
       var cffIndex = new CFFIndex();
       var bytes = this.bytes;
       var count = bytes[pos++] << 8 | bytes[pos++];
@@ -469,8 +472,9 @@ var CFFParser = function CFFParserClosure() {
         obj: cffIndex,
         endPos: end
       };
-    },
-    parseNameIndex: function CFFParser_parseNameIndex(index) {
+    }
+
+    parseNameIndex(index) {
       var names = [];
 
       for (var i = 0, ii = index.count; i < ii; ++i) {
@@ -479,8 +483,9 @@ var CFFParser = function CFFParserClosure() {
       }
 
       return names;
-    },
-    parseStringIndex: function CFFParser_parseStringIndex(index) {
+    }
+
+    parseStringIndex(index) {
       var strings = new CFFStrings();
 
       for (var i = 0, ii = index.count; i < ii; ++i) {
@@ -489,8 +494,9 @@ var CFFParser = function CFFParserClosure() {
       }
 
       return strings;
-    },
-    createDict: function CFFParser_createDict(Type, dict, strings) {
+    }
+
+    createDict(Type, dict, strings) {
       var cffDict = new Type(strings);
 
       for (var i = 0, ii = dict.length; i < ii; ++i) {
@@ -501,8 +507,9 @@ var CFFParser = function CFFParserClosure() {
       }
 
       return cffDict;
-    },
-    parseCharString: function CFFParser_parseCharString(state, data, localSubrIndex, globalSubrIndex) {
+    }
+
+    parseCharString(state, data, localSubrIndex, globalSubrIndex) {
       if (!data || state.callDepth > MAX_SUBR_NESTING) {
         return false;
       }
@@ -660,7 +667,7 @@ var CFFParser = function CFFParserClosure() {
 
       state.stackSize = stackSize;
       return true;
-    },
+    }
 
     parseCharStrings({
       charStrings,
@@ -738,14 +745,15 @@ var CFFParser = function CFFParserClosure() {
         seacs,
         widths
       };
-    },
+    }
 
-    emptyPrivateDictionary: function CFFParser_emptyPrivateDictionary(parentDict) {
+    emptyPrivateDictionary(parentDict) {
       var privateDict = this.createDict(CFFPrivateDict, [], parentDict.strings);
       parentDict.setByKey(18, [0, 0]);
       parentDict.privateDict = privateDict;
-    },
-    parsePrivateDict: function CFFParser_parsePrivateDict(parentDict) {
+    }
+
+    parsePrivateDict(parentDict) {
       if (!parentDict.hasName("Private")) {
         this.emptyPrivateDictionary(parentDict);
         return;
@@ -786,8 +794,9 @@ var CFFParser = function CFFParserClosure() {
 
       var subrsIndex = this.parseIndex(relativeOffset);
       privateDict.subrsIndex = subrsIndex.obj;
-    },
-    parseCharsets: function CFFParser_parseCharsets(pos, length, strings, cid) {
+    }
+
+    parseCharsets(pos, length, strings, cid) {
       if (pos === 0) {
         return new CFFCharset(true, CFFCharsetPredefinedTypes.ISO_ADOBE, _charsets.ISOAdobeCharset);
       } else if (pos === 1) {
@@ -843,8 +852,9 @@ var CFFParser = function CFFParserClosure() {
       var end = pos;
       var raw = bytes.subarray(start, end);
       return new CFFCharset(false, format, charset, raw);
-    },
-    parseEncoding: function CFFParser_parseEncoding(pos, properties, strings, charset) {
+    }
+
+    parseEncoding(pos, properties, strings, charset) {
       var encoding = Object.create(null);
       var bytes = this.bytes;
       var predefined = false;
@@ -918,8 +928,9 @@ var CFFParser = function CFFParserClosure() {
 
       format = format & 0x7f;
       return new CFFEncoding(predefined, format, encoding, raw);
-    },
-    parseFDSelect: function CFFParser_parseFDSelect(pos, length) {
+    }
+
+    parseFDSelect(pos, length) {
       var bytes = this.bytes;
       var format = bytes[pos++];
       var fdSelect = [];
@@ -966,14 +977,16 @@ var CFFParser = function CFFParserClosure() {
 
       return new CFFFDSelect(format, fdSelect);
     }
-  };
+
+  }
+
   return CFFParser;
 }();
 
 exports.CFFParser = CFFParser;
 
-var CFF = function CFFClosure() {
-  function CFF() {
+class CFF {
+  constructor() {
     this.header = null;
     this.names = [];
     this.topDict = null;
@@ -987,124 +1000,120 @@ var CFF = function CFFClosure() {
     this.isCIDFont = false;
   }
 
-  CFF.prototype = {
-    duplicateFirstGlyph: function CFF_duplicateFirstGlyph() {
-      if (this.charStrings.count >= 65535) {
-        (0, _util.warn)("Not enough space in charstrings to duplicate first glyph.");
-        return;
-      }
+  duplicateFirstGlyph() {
+    if (this.charStrings.count >= 65535) {
+      (0, _util.warn)("Not enough space in charstrings to duplicate first glyph.");
+      return;
+    }
 
-      var glyphZero = this.charStrings.get(0);
-      this.charStrings.add(glyphZero);
+    var glyphZero = this.charStrings.get(0);
+    this.charStrings.add(glyphZero);
 
-      if (this.isCIDFont) {
-        this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0]);
-      }
-    },
-    hasGlyphId: function CFF_hasGlyphID(id) {
-      if (id < 0 || id >= this.charStrings.count) {
-        return false;
-      }
+    if (this.isCIDFont) {
+      this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0]);
+    }
+  }
 
-      var glyph = this.charStrings.get(id);
-      return glyph.length > 0;
+  hasGlyphId(id) {
+    if (id < 0 || id >= this.charStrings.count) {
+      return false;
     }
-  };
-  return CFF;
-}();
+
+    var glyph = this.charStrings.get(id);
+    return glyph.length > 0;
+  }
+
+}
 
 exports.CFF = CFF;
 
-var CFFHeader = function CFFHeaderClosure() {
-  function CFFHeader(major, minor, hdrSize, offSize) {
+class CFFHeader {
+  constructor(major, minor, hdrSize, offSize) {
     this.major = major;
     this.minor = minor;
     this.hdrSize = hdrSize;
     this.offSize = offSize;
   }
 
-  return CFFHeader;
-}();
+}
 
 exports.CFFHeader = CFFHeader;
 
-var CFFStrings = function CFFStringsClosure() {
-  function CFFStrings() {
+class CFFStrings {
+  constructor() {
     this.strings = [];
   }
 
-  CFFStrings.prototype = {
-    get: function CFFStrings_get(index) {
-      if (index >= 0 && index <= NUM_STANDARD_CFF_STRINGS - 1) {
-        return CFFStandardStrings[index];
-      }
-
-      if (index - NUM_STANDARD_CFF_STRINGS <= this.strings.length) {
-        return this.strings[index - NUM_STANDARD_CFF_STRINGS];
-      }
+  get(index) {
+    if (index >= 0 && index <= NUM_STANDARD_CFF_STRINGS - 1) {
+      return CFFStandardStrings[index];
+    }
 
-      return CFFStandardStrings[0];
-    },
-    getSID: function CFFStrings_getSID(str) {
-      let index = CFFStandardStrings.indexOf(str);
+    if (index - NUM_STANDARD_CFF_STRINGS <= this.strings.length) {
+      return this.strings[index - NUM_STANDARD_CFF_STRINGS];
+    }
 
-      if (index !== -1) {
-        return index;
-      }
+    return CFFStandardStrings[0];
+  }
 
-      index = this.strings.indexOf(str);
+  getSID(str) {
+    let index = CFFStandardStrings.indexOf(str);
 
-      if (index !== -1) {
-        return index + NUM_STANDARD_CFF_STRINGS;
-      }
+    if (index !== -1) {
+      return index;
+    }
 
-      return -1;
-    },
-    add: function CFFStrings_add(value) {
-      this.strings.push(value);
-    },
+    index = this.strings.indexOf(str);
 
-    get count() {
-      return this.strings.length;
+    if (index !== -1) {
+      return index + NUM_STANDARD_CFF_STRINGS;
     }
 
-  };
-  return CFFStrings;
-}();
+    return -1;
+  }
+
+  add(value) {
+    this.strings.push(value);
+  }
+
+  get count() {
+    return this.strings.length;
+  }
+
+}
 
 exports.CFFStrings = CFFStrings;
 
-var CFFIndex = function CFFIndexClosure() {
-  function CFFIndex() {
+class CFFIndex {
+  constructor() {
     this.objects = [];
     this.length = 0;
   }
 
-  CFFIndex.prototype = {
-    add: function CFFIndex_add(data) {
-      this.length += data.length;
-      this.objects.push(data);
-    },
-    set: function CFFIndex_set(index, data) {
-      this.length += data.length - this.objects[index].length;
-      this.objects[index] = data;
-    },
-    get: function CFFIndex_get(index) {
-      return this.objects[index];
-    },
-
-    get count() {
-      return this.objects.length;
-    }
-
-  };
-  return CFFIndex;
-}();
+  add(data) {
+    this.length += data.length;
+    this.objects.push(data);
+  }
+
+  set(index, data) {
+    this.length += data.length - this.objects[index].length;
+    this.objects[index] = data;
+  }
+
+  get(index) {
+    return this.objects[index];
+  }
+
+  get count() {
+    return this.objects.length;
+  }
+
+}
 
 exports.CFFIndex = CFFIndex;
 
-var CFFDict = function CFFDictClosure() {
-  function CFFDict(tables, strings) {
+class CFFDict {
+  constructor(tables, strings) {
     this.keyToNameMap = tables.keyToNameMap;
     this.nameToKeyMap = tables.nameToKeyMap;
     this.defaults = tables.defaults;
@@ -1115,63 +1124,65 @@ var CFFDict = function CFFDictClosure() {
     this.values = Object.create(null);
   }
 
-  CFFDict.prototype = {
-    setByKey: function CFFDict_setByKey(key, value) {
-      if (!(key in this.keyToNameMap)) {
-        return false;
-      }
+  setByKey(key, value) {
+    if (!(key in this.keyToNameMap)) {
+      return false;
+    }
 
-      var valueLength = value.length;
+    var valueLength = value.length;
 
-      if (valueLength === 0) {
+    if (valueLength === 0) {
+      return true;
+    }
+
+    for (var i = 0; i < valueLength; i++) {
+      if (isNaN(value[i])) {
+        (0, _util.warn)('Invalid CFFDict value: "' + value + '" for key "' + key + '".');
         return true;
       }
+    }
 
-      for (var i = 0; i < valueLength; i++) {
-        if (isNaN(value[i])) {
-          (0, _util.warn)('Invalid CFFDict value: "' + value + '" for key "' + key + '".');
-          return true;
-        }
-      }
+    var type = this.types[key];
 
-      var type = this.types[key];
+    if (type === "num" || type === "sid" || type === "offset") {
+      value = value[0];
+    }
 
-      if (type === "num" || type === "sid" || type === "offset") {
-        value = value[0];
-      }
+    this.values[key] = value;
+    return true;
+  }
 
-      this.values[key] = value;
-      return true;
-    },
-    setByName: function CFFDict_setByName(name, value) {
-      if (!(name in this.nameToKeyMap)) {
-        throw new _util.FormatError(`Invalid dictionary name "${name}"`);
-      }
+  setByName(name, value) {
+    if (!(name in this.nameToKeyMap)) {
+      throw new _util.FormatError(`Invalid dictionary name "${name}"`);
+    }
 
-      this.values[this.nameToKeyMap[name]] = value;
-    },
-    hasName: function CFFDict_hasName(name) {
-      return this.nameToKeyMap[name] in this.values;
-    },
-    getByName: function CFFDict_getByName(name) {
-      if (!(name in this.nameToKeyMap)) {
-        throw new _util.FormatError(`Invalid dictionary name ${name}"`);
-      }
+    this.values[this.nameToKeyMap[name]] = value;
+  }
 
-      var key = this.nameToKeyMap[name];
+  hasName(name) {
+    return this.nameToKeyMap[name] in this.values;
+  }
 
-      if (!(key in this.values)) {
-        return this.defaults[key];
-      }
+  getByName(name) {
+    if (!(name in this.nameToKeyMap)) {
+      throw new _util.FormatError(`Invalid dictionary name ${name}"`);
+    }
+
+    var key = this.nameToKeyMap[name];
 
-      return this.values[key];
-    },
-    removeByName: function CFFDict_removeByName(name) {
-      delete this.values[this.nameToKeyMap[name]];
+    if (!(key in this.values)) {
+      return this.defaults[key];
     }
-  };
 
-  CFFDict.createTables = function CFFDict_createTables(layout) {
+    return this.values[key];
+  }
+
+  removeByName(name) {
+    delete this.values[this.nameToKeyMap[name]];
+  }
+
+  static createTables(layout) {
     var tables = {
       keyToNameMap: {},
       nameToKeyMap: {},
@@ -1193,25 +1204,26 @@ var CFFDict = function CFFDictClosure() {
     }
 
     return tables;
-  };
+  }
 
-  return CFFDict;
-}();
+}
 
 var CFFTopDict = function CFFTopDictClosure() {
   var layout = [[[12, 30], "ROS", ["sid", "sid", "num"], null], [[12, 20], "SyntheticBase", "num", null], [0, "version", "sid", null], [1, "Notice", "sid", null], [[12, 0], "Copyright", "sid", null], [2, "FullName", "sid", null], [3, "FamilyName", "sid", null], [4, "Weight", "sid", null], [[12, 1], "isFixedPitch", "num", 0], [[12, 2], "ItalicAngle", "num", 0], [[12, 3], "UnderlinePosition", "num", -100], [[12, 4], "UnderlineThickness", "num", 50], [[12, 5], "PaintType", "num", 0], [[12, 6], "CharstringType", "num", 2], [[12, 7], "FontMatrix", ["num", "num", "num", "num", "num", "num"], [0.001, 0, 0, 0.001, 0, 0]], [13, "UniqueID", "num", null], [5, "FontBBox", ["num", "num", "num", "num"], [0, 0, 0, 0]], [[12, 8], "StrokeWidth", "num", 0], [14, "XUID", "array", null], [15, "charset", "offset", 0], [16, "Encoding", "offset", 0], [17, "CharStrings", "offset", 0], [18, "Private", ["offset", "offset"], null], [[12, 21], "PostScript", "sid", null], [[12, 22], "BaseFontName", "sid", null], [[12, 23], "BaseFontBlend", "delta", null], [[12, 31], "CIDFontVersion", "num", 0], [[12, 32], "CIDFontRevision", "num", 0], [[12, 33], "CIDFontType", "num", 0], [[12, 34], "CIDCount", "num", 8720], [[12, 35], "UIDBase", "num", null], [[12, 37], "FDSelect", "offset", null], [[12, 36], "FDArray", "offset", null], [[12, 38], "FontName", "sid", null]];
   var tables = null;
 
-  function CFFTopDict(strings) {
-    if (tables === null) {
-      tables = CFFDict.createTables(layout);
+  class CFFTopDict extends CFFDict {
+    constructor(strings) {
+      if (tables === null) {
+        tables = CFFDict.createTables(layout);
+      }
+
+      super(tables, strings);
+      this.privateDict = null;
     }
 
-    CFFDict.call(this, tables, strings);
-    this.privateDict = null;
   }
 
-  CFFTopDict.prototype = Object.create(CFFDict.prototype);
   return CFFTopDict;
 }();
 
@@ -1221,16 +1233,18 @@ var CFFPrivateDict = function CFFPrivateDictClosure() {
   var layout = [[6, "BlueValues", "delta", null], [7, "OtherBlues", "delta", null], [8, "FamilyBlues", "delta", null], [9, "FamilyOtherBlues", "delta", null], [[12, 9], "BlueScale", "num", 0.039625], [[12, 10], "BlueShift", "num", 7], [[12, 11], "BlueFuzz", "num", 1], [10, "StdHW", "num", null], [11, "StdVW", "num", null], [[12, 12], "StemSnapH", "delta", null], [[12, 13], "StemSnapV", "delta", null], [[12, 14], "ForceBold", "num", 0], [[12, 17], "LanguageGroup", "num", 0], [[12, 18], "ExpansionFactor", "num", 0.06], [[12, 19], "initialRandomSeed", "num", 0], [20, "defaultWidthX", "num", 0], [21, "nominalWidthX", "num", 0], [19, "Subrs", "offset", null]];
   var tables = null;
 
-  function CFFPrivateDict(strings) {
-    if (tables === null) {
-      tables = CFFDict.createTables(layout);
+  class CFFPrivateDict extends CFFDict {
+    constructor(strings) {
+      if (tables === null) {
+        tables = CFFDict.createTables(layout);
+      }
+
+      super(tables, strings);
+      this.subrsIndex = null;
     }
 
-    CFFDict.call(this, tables, strings);
-    this.subrsIndex = null;
   }
 
-  CFFPrivateDict.prototype = Object.create(CFFDict.prototype);
   return CFFPrivateDict;
 }();
 
@@ -1241,578 +1255,592 @@ var CFFCharsetPredefinedTypes = {
   EXPERT_SUBSET: 2
 };
 
-var CFFCharset = function CFFCharsetClosure() {
-  function CFFCharset(predefined, format, charset, raw) {
+class CFFCharset {
+  constructor(predefined, format, charset, raw) {
     this.predefined = predefined;
     this.format = format;
     this.charset = charset;
     this.raw = raw;
   }
 
-  return CFFCharset;
-}();
+}
 
 exports.CFFCharset = CFFCharset;
 
-var CFFEncoding = function CFFEncodingClosure() {
-  function CFFEncoding(predefined, format, encoding, raw) {
+class CFFEncoding {
+  constructor(predefined, format, encoding, raw) {
     this.predefined = predefined;
     this.format = format;
     this.encoding = encoding;
     this.raw = raw;
   }
 
-  return CFFEncoding;
-}();
+}
 
-var CFFFDSelect = function CFFFDSelectClosure() {
-  function CFFFDSelect(format, fdSelect) {
+class CFFFDSelect {
+  constructor(format, fdSelect) {
     this.format = format;
     this.fdSelect = fdSelect;
   }
 
-  CFFFDSelect.prototype = {
-    getFDIndex: function CFFFDSelect_get(glyphIndex) {
-      if (glyphIndex < 0 || glyphIndex >= this.fdSelect.length) {
-        return -1;
-      }
-
-      return this.fdSelect[glyphIndex];
+  getFDIndex(glyphIndex) {
+    if (glyphIndex < 0 || glyphIndex >= this.fdSelect.length) {
+      return -1;
     }
-  };
-  return CFFFDSelect;
-}();
+
+    return this.fdSelect[glyphIndex];
+  }
+
+}
 
 exports.CFFFDSelect = CFFFDSelect;
 
-var CFFOffsetTracker = function CFFOffsetTrackerClosure() {
-  function CFFOffsetTracker() {
+class CFFOffsetTracker {
+  constructor() {
     this.offsets = Object.create(null);
   }
 
-  CFFOffsetTracker.prototype = {
-    isTracking: function CFFOffsetTracker_isTracking(key) {
-      return key in this.offsets;
-    },
-    track: function CFFOffsetTracker_track(key, location) {
-      if (key in this.offsets) {
-        throw new _util.FormatError(`Already tracking location of ${key}`);
-      }
+  isTracking(key) {
+    return key in this.offsets;
+  }
 
-      this.offsets[key] = location;
-    },
-    offset: function CFFOffsetTracker_offset(value) {
-      for (var key in this.offsets) {
-        this.offsets[key] += value;
-      }
-    },
-    setEntryLocation: function CFFOffsetTracker_setEntryLocation(key, values, output) {
-      if (!(key in this.offsets)) {
-        throw new _util.FormatError(`Not tracking location of ${key}`);
-      }
+  track(key, location) {
+    if (key in this.offsets) {
+      throw new _util.FormatError(`Already tracking location of ${key}`);
+    }
 
-      var data = output.data;
-      var dataOffset = this.offsets[key];
-      var size = 5;
+    this.offsets[key] = location;
+  }
 
-      for (var i = 0, ii = values.length; i < ii; ++i) {
-        var offset0 = i * size + dataOffset;
-        var offset1 = offset0 + 1;
-        var offset2 = offset0 + 2;
-        var offset3 = offset0 + 3;
-        var offset4 = offset0 + 4;
+  offset(value) {
+    for (var key in this.offsets) {
+      this.offsets[key] += value;
+    }
+  }
 
-        if (data[offset0] !== 0x1d || data[offset1] !== 0 || data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) {
-          throw new _util.FormatError("writing to an offset that is not empty");
-        }
+  setEntryLocation(key, values, output) {
+    if (!(key in this.offsets)) {
+      throw new _util.FormatError(`Not tracking location of ${key}`);
+    }
 
-        var value = values[i];
-        data[offset0] = 0x1d;
-        data[offset1] = value >> 24 & 0xff;
-        data[offset2] = value >> 16 & 0xff;
-        data[offset3] = value >> 8 & 0xff;
-        data[offset4] = value & 0xff;
+    var data = output.data;
+    var dataOffset = this.offsets[key];
+    var size = 5;
+
+    for (var i = 0, ii = values.length; i < ii; ++i) {
+      var offset0 = i * size + dataOffset;
+      var offset1 = offset0 + 1;
+      var offset2 = offset0 + 2;
+      var offset3 = offset0 + 3;
+      var offset4 = offset0 + 4;
+
+      if (data[offset0] !== 0x1d || data[offset1] !== 0 || data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) {
+        throw new _util.FormatError("writing to an offset that is not empty");
       }
+
+      var value = values[i];
+      data[offset0] = 0x1d;
+      data[offset1] = value >> 24 & 0xff;
+      data[offset2] = value >> 16 & 0xff;
+      data[offset3] = value >> 8 & 0xff;
+      data[offset4] = value & 0xff;
     }
-  };
-  return CFFOffsetTracker;
-}();
+  }
+
+}
 
-var CFFCompiler = function CFFCompilerClosure() {
-  function CFFCompiler(cff) {
+class CFFCompiler {
+  constructor(cff) {
     this.cff = cff;
   }
 
-  CFFCompiler.prototype = {
-    compile: function CFFCompiler_compile() {
-      var cff = this.cff;
-      var output = {
-        data: [],
-        length: 0,
-        add: function CFFCompiler_add(data) {
-          this.data = this.data.concat(data);
-          this.length = this.data.length;
+  compile() {
+    var cff = this.cff;
+    var output = {
+      data: [],
+      length: 0,
+      add: function CFFCompiler_add(data) {
+        this.data = this.data.concat(data);
+        this.length = this.data.length;
+      }
+    };
+    var header = this.compileHeader(cff.header);
+    output.add(header);
+    var nameIndex = this.compileNameIndex(cff.names);
+    output.add(nameIndex);
+
+    if (cff.isCIDFont) {
+      if (cff.topDict.hasName("FontMatrix")) {
+        var base = cff.topDict.getByName("FontMatrix");
+        cff.topDict.removeByName("FontMatrix");
+
+        for (var i = 0, ii = cff.fdArray.length; i < ii; i++) {
+          var subDict = cff.fdArray[i];
+          var matrix = base.slice(0);
+
+          if (subDict.hasName("FontMatrix")) {
+            matrix = _util.Util.transform(matrix, subDict.getByName("FontMatrix"));
+          }
+
+          subDict.setByName("FontMatrix", matrix);
         }
-      };
-      var header = this.compileHeader(cff.header);
-      output.add(header);
-      var nameIndex = this.compileNameIndex(cff.names);
-      output.add(nameIndex);
+      }
+    }
 
-      if (cff.isCIDFont) {
-        if (cff.topDict.hasName("FontMatrix")) {
-          var base = cff.topDict.getByName("FontMatrix");
-          cff.topDict.removeByName("FontMatrix");
+    cff.topDict.setByName("charset", 0);
+    var compiled = this.compileTopDicts([cff.topDict], output.length, cff.isCIDFont);
+    output.add(compiled.output);
+    var topDictTracker = compiled.trackers[0];
+    var stringIndex = this.compileStringIndex(cff.strings.strings);
+    output.add(stringIndex);
+    var globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
+    output.add(globalSubrIndex);
+
+    if (cff.encoding && cff.topDict.hasName("Encoding")) {
+      if (cff.encoding.predefined) {
+        topDictTracker.setEntryLocation("Encoding", [cff.encoding.format], output);
+      } else {
+        var encoding = this.compileEncoding(cff.encoding);
+        topDictTracker.setEntryLocation("Encoding", [output.length], output);
+        output.add(encoding);
+      }
+    }
 
-          for (var i = 0, ii = cff.fdArray.length; i < ii; i++) {
-            var subDict = cff.fdArray[i];
-            var matrix = base.slice(0);
+    var charset = this.compileCharset(cff.charset, cff.charStrings.count, cff.strings, cff.isCIDFont);
+    topDictTracker.setEntryLocation("charset", [output.length], output);
+    output.add(charset);
+    var charStrings = this.compileCharStrings(cff.charStrings);
+    topDictTracker.setEntryLocation("CharStrings", [output.length], output);
+    output.add(charStrings);
+
+    if (cff.isCIDFont) {
+      topDictTracker.setEntryLocation("FDSelect", [output.length], output);
+      var fdSelect = this.compileFDSelect(cff.fdSelect);
+      output.add(fdSelect);
+      compiled = this.compileTopDicts(cff.fdArray, output.length, true);
+      topDictTracker.setEntryLocation("FDArray", [output.length], output);
+      output.add(compiled.output);
+      var fontDictTrackers = compiled.trackers;
+      this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output);
+    }
 
-            if (subDict.hasName("FontMatrix")) {
-              matrix = _util.Util.transform(matrix, subDict.getByName("FontMatrix"));
-            }
+    this.compilePrivateDicts([cff.topDict], [topDictTracker], output);
+    output.add([0]);
+    return output.data;
+  }
 
-            subDict.setByName("FontMatrix", matrix);
-          }
-        }
-      }
+  encodeNumber(value) {
+    if (Number.isInteger(value)) {
+      return this.encodeInteger(value);
+    }
 
-      cff.topDict.setByName("charset", 0);
-      var compiled = this.compileTopDicts([cff.topDict], output.length, cff.isCIDFont);
-      output.add(compiled.output);
-      var topDictTracker = compiled.trackers[0];
-      var stringIndex = this.compileStringIndex(cff.strings.strings);
-      output.add(stringIndex);
-      var globalSubrIndex = this.compileIndex(cff.globalSubrIndex);
-      output.add(globalSubrIndex);
-
-      if (cff.encoding && cff.topDict.hasName("Encoding")) {
-        if (cff.encoding.predefined) {
-          topDictTracker.setEntryLocation("Encoding", [cff.encoding.format], output);
-        } else {
-          var encoding = this.compileEncoding(cff.encoding);
-          topDictTracker.setEntryLocation("Encoding", [output.length], output);
-          output.add(encoding);
-        }
-      }
+    return this.encodeFloat(value);
+  }
 
-      var charset = this.compileCharset(cff.charset, cff.charStrings.count, cff.strings, cff.isCIDFont);
-      topDictTracker.setEntryLocation("charset", [output.length], output);
-      output.add(charset);
-      var charStrings = this.compileCharStrings(cff.charStrings);
-      topDictTracker.setEntryLocation("CharStrings", [output.length], output);
-      output.add(charStrings);
+  static get EncodeFloatRegExp() {
+    return (0, _util.shadow)(this, "EncodeFloatRegExp", /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/);
+  }
 
-      if (cff.isCIDFont) {
-        topDictTracker.setEntryLocation("FDSelect", [output.length], output);
-        var fdSelect = this.compileFDSelect(cff.fdSelect);
-        output.add(fdSelect);
-        compiled = this.compileTopDicts(cff.fdArray, output.length, true);
-        topDictTracker.setEntryLocation("FDArray", [output.length], output);
-        output.add(compiled.output);
-        var fontDictTrackers = compiled.trackers;
-        this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output);
-      }
+  encodeFloat(num) {
+    var value = num.toString();
+    var m = CFFCompiler.EncodeFloatRegExp.exec(value);
 
-      this.compilePrivateDicts([cff.topDict], [topDictTracker], output);
-      output.add([0]);
-      return output.data;
-    },
-    encodeNumber: function CFFCompiler_encodeNumber(value) {
-      if (parseFloat(value) === parseInt(value, 10) && !isNaN(value)) {
-        return this.encodeInteger(value);
-      }
+    if (m) {
+      var epsilon = parseFloat("1e" + ((m[2] ? +m[2] : 0) + m[1].length));
+      value = (Math.round(num * epsilon) / epsilon).toString();
+    }
+
+    var nibbles = "";
+    var i, ii;
 
-      return this.encodeFloat(value);
-    },
-    encodeFloat: function CFFCompiler_encodeFloat(num) {
-      var value = num.toString();
-      var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value);
+    for (i = 0, ii = value.length; i < ii; ++i) {
+      var a = value[i];
 
-      if (m) {
-        var epsilon = parseFloat("1e" + ((m[2] ? +m[2] : 0) + m[1].length));
-        value = (Math.round(num * epsilon) / epsilon).toString();
+      if (a === "e") {
+        nibbles += value[++i] === "-" ? "c" : "b";
+      } else if (a === ".") {
+        nibbles += "a";
+      } else if (a === "-") {
+        nibbles += "e";
+      } else {
+        nibbles += a;
       }
+    }
 
-      var nibbles = "";
-      var i, ii;
+    nibbles += nibbles.length & 1 ? "f" : "ff";
+    var out = [30];
 
-      for (i = 0, ii = value.length; i < ii; ++i) {
-        var a = value[i];
+    for (i = 0, ii = nibbles.length; i < ii; i += 2) {
+      out.push(parseInt(nibbles.substring(i, i + 2), 16));
+    }
 
-        if (a === "e") {
-          nibbles += value[++i] === "-" ? "c" : "b";
-        } else if (a === ".") {
-          nibbles += "a";
-        } else if (a === "-") {
-          nibbles += "e";
-        } else {
-          nibbles += a;
+    return out;
+  }
+
+  encodeInteger(value) {
+    var code;
+
+    if (value >= -107 && value <= 107) {
+      code = [value + 139];
+    } else if (value >= 108 && value <= 1131) {
+      value = value - 108;
+      code = [(value >> 8) + 247, value & 0xff];
+    } else if (value >= -1131 && value <= -108) {
+      value = -value - 108;
+      code = [(value >> 8) + 251, value & 0xff];
+    } else if (value >= -32768 && value <= 32767) {
+      code = [0x1c, value >> 8 & 0xff, value & 0xff];
+    } else {
+      code = [0x1d, value >> 24 & 0xff, value >> 16 & 0xff, value >> 8 & 0xff, value & 0xff];
+    }
+
+    return code;
+  }
+
+  compileHeader(header) {
+    return [header.major, header.minor, header.hdrSize, header.offSize];
+  }
+
+  compileNameIndex(names) {
+    var nameIndex = new CFFIndex();
+
+    for (var i = 0, ii = names.length; i < ii; ++i) {
+      var name = names[i];
+      var length = Math.min(name.length, 127);
+      var sanitizedName = new Array(length);
+
+      for (var j = 0; j < length; j++) {
+        var char = name[j];
+
+        if (char < "!" || char > "~" || char === "[" || char === "]" || char === "(" || char === ")" || char === "{" || char === "}" || char === "<" || char === ">" || char === "/" || char === "%") {
+          char = "_";
         }
+
+        sanitizedName[j] = char;
       }
 
-      nibbles += nibbles.length & 1 ? "f" : "ff";
-      var out = [30];
+      sanitizedName = sanitizedName.join("");
 
-      for (i = 0, ii = nibbles.length; i < ii; i += 2) {
-        out.push(parseInt(nibbles.substring(i, i + 2), 16));
+      if (sanitizedName === "") {
+        sanitizedName = "Bad_Font_Name";
       }
 
-      return out;
-    },
-    encodeInteger: function CFFCompiler_encodeInteger(value) {
-      var code;
-
-      if (value >= -107 && value <= 107) {
-        code = [value + 139];
-      } else if (value >= 108 && value <= 1131) {
-        value = value - 108;
-        code = [(value >> 8) + 247, value & 0xff];
-      } else if (value >= -1131 && value <= -108) {
-        value = -value - 108;
-        code = [(value >> 8) + 251, value & 0xff];
-      } else if (value >= -32768 && value <= 32767) {
-        code = [0x1c, value >> 8 & 0xff, value & 0xff];
-      } else {
-        code = [0x1d, value >> 24 & 0xff, value >> 16 & 0xff, value >> 8 & 0xff, value & 0xff];
-      }
+      nameIndex.add((0, _util.stringToBytes)(sanitizedName));
+    }
 
-      return code;
-    },
-    compileHeader: function CFFCompiler_compileHeader(header) {
-      return [header.major, header.minor, header.hdrSize, header.offSize];
-    },
-    compileNameIndex: function CFFCompiler_compileNameIndex(names) {
-      var nameIndex = new CFFIndex();
+    return this.compileIndex(nameIndex);
+  }
 
-      for (var i = 0, ii = names.length; i < ii; ++i) {
-        var name = names[i];
-        var length = Math.min(name.length, 127);
-        var sanitizedName = new Array(length);
+  compileTopDicts(dicts, length, removeCidKeys) {
+    var fontDictTrackers = [];
+    var fdArrayIndex = new CFFIndex();
 
-        for (var j = 0; j < length; j++) {
-          var char = name[j];
+    for (var i = 0, ii = dicts.length; i < ii; ++i) {
+      var fontDict = dicts[i];
 
-          if (char < "!" || char > "~" || char === "[" || char === "]" || char === "(" || char === ")" || char === "{" || char === "}" || char === "<" || char === ">" || char === "/" || char === "%") {
-            char = "_";
-          }
+      if (removeCidKeys) {
+        fontDict.removeByName("CIDFontVersion");
+        fontDict.removeByName("CIDFontRevision");
+        fontDict.removeByName("CIDFontType");
+        fontDict.removeByName("CIDCount");
+        fontDict.removeByName("UIDBase");
+      }
 
-          sanitizedName[j] = char;
-        }
+      var fontDictTracker = new CFFOffsetTracker();
+      var fontDictData = this.compileDict(fontDict, fontDictTracker);
+      fontDictTrackers.push(fontDictTracker);
+      fdArrayIndex.add(fontDictData);
+      fontDictTracker.offset(length);
+    }
 
-        sanitizedName = sanitizedName.join("");
+    fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers);
+    return {
+      trackers: fontDictTrackers,
+      output: fdArrayIndex
+    };
+  }
 
-        if (sanitizedName === "") {
-          sanitizedName = "Bad_Font_Name";
-        }
+  compilePrivateDicts(dicts, trackers, output) {
+    for (var i = 0, ii = dicts.length; i < ii; ++i) {
+      var fontDict = dicts[i];
+      var privateDict = fontDict.privateDict;
 
-        nameIndex.add((0, _util.stringToBytes)(sanitizedName));
+      if (!privateDict || !fontDict.hasName("Private")) {
+        throw new _util.FormatError("There must be a private dictionary.");
       }
 
-      return this.compileIndex(nameIndex);
-    },
-    compileTopDicts: function CFFCompiler_compileTopDicts(dicts, length, removeCidKeys) {
-      var fontDictTrackers = [];
-      var fdArrayIndex = new CFFIndex();
-
-      for (var i = 0, ii = dicts.length; i < ii; ++i) {
-        var fontDict = dicts[i];
-
-        if (removeCidKeys) {
-          fontDict.removeByName("CIDFontVersion");
-          fontDict.removeByName("CIDFontRevision");
-          fontDict.removeByName("CIDFontType");
-          fontDict.removeByName("CIDCount");
-          fontDict.removeByName("UIDBase");
-        }
+      var privateDictTracker = new CFFOffsetTracker();
+      var privateDictData = this.compileDict(privateDict, privateDictTracker);
+      var outputLength = output.length;
+      privateDictTracker.offset(outputLength);
 
-        var fontDictTracker = new CFFOffsetTracker();
-        var fontDictData = this.compileDict(fontDict, fontDictTracker);
-        fontDictTrackers.push(fontDictTracker);
-        fdArrayIndex.add(fontDictData);
-        fontDictTracker.offset(length);
+      if (!privateDictData.length) {
+        outputLength = 0;
       }
 
-      fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers);
-      return {
-        trackers: fontDictTrackers,
-        output: fdArrayIndex
-      };
-    },
-    compilePrivateDicts: function CFFCompiler_compilePrivateDicts(dicts, trackers, output) {
-      for (var i = 0, ii = dicts.length; i < ii; ++i) {
-        var fontDict = dicts[i];
-        var privateDict = fontDict.privateDict;
-
-        if (!privateDict || !fontDict.hasName("Private")) {
-          throw new _util.FormatError("There must be a private dictionary.");
-        }
+      trackers[i].setEntryLocation("Private", [privateDictData.length, outputLength], output);
+      output.add(privateDictData);
 
-        var privateDictTracker = new CFFOffsetTracker();
-        var privateDictData = this.compileDict(privateDict, privateDictTracker);
-        var outputLength = output.length;
-        privateDictTracker.offset(outputLength);
+      if (privateDict.subrsIndex && privateDict.hasName("Subrs")) {
+        var subrs = this.compileIndex(privateDict.subrsIndex);
+        privateDictTracker.setEntryLocation("Subrs", [privateDictData.length], output);
+        output.add(subrs);
+      }
+    }
+  }
 
-        if (!privateDictData.length) {
-          outputLength = 0;
-        }
+  compileDict(dict, offsetTracker) {
+    var out = [];
+    var order = dict.order;
 
-        trackers[i].setEntryLocation("Private", [privateDictData.length, outputLength], output);
-        output.add(privateDictData);
+    for (var i = 0; i < order.length; ++i) {
+      var key = order[i];
 
-        if (privateDict.subrsIndex && privateDict.hasName("Subrs")) {
-          var subrs = this.compileIndex(privateDict.subrsIndex);
-          privateDictTracker.setEntryLocation("Subrs", [privateDictData.length], output);
-          output.add(subrs);
-        }
+      if (!(key in dict.values)) {
+        continue;
       }
-    },
-    compileDict: function CFFCompiler_compileDict(dict, offsetTracker) {
-      var out = [];
-      var order = dict.order;
 
-      for (var i = 0; i < order.length; ++i) {
-        var key = order[i];
+      var values = dict.values[key];
+      var types = dict.types[key];
 
-        if (!(key in dict.values)) {
-          continue;
-        }
+      if (!Array.isArray(types)) {
+        types = [types];
+      }
 
-        var values = dict.values[key];
-        var types = dict.types[key];
+      if (!Array.isArray(values)) {
+        values = [values];
+      }
 
-        if (!Array.isArray(types)) {
-          types = [types];
-        }
+      if (values.length === 0) {
+        continue;
+      }
 
-        if (!Array.isArray(values)) {
-          values = [values];
-        }
+      for (var j = 0, jj = types.length; j < jj; ++j) {
+        var type = types[j];
+        var value = values[j];
 
-        if (values.length === 0) {
-          continue;
-        }
+        switch (type) {
+          case "num":
+          case "sid":
+            out = out.concat(this.encodeNumber(value));
+            break;
 
-        for (var j = 0, jj = types.length; j < jj; ++j) {
-          var type = types[j];
-          var value = values[j];
+          case "offset":
+            var name = dict.keyToNameMap[key];
 
-          switch (type) {
-            case "num":
-            case "sid":
-              out = out.concat(this.encodeNumber(value));
-              break;
+            if (!offsetTracker.isTracking(name)) {
+              offsetTracker.track(name, out.length);
+            }
 
-            case "offset":
-              var name = dict.keyToNameMap[key];
+            out = out.concat([0x1d, 0, 0, 0, 0]);
+            break;
 
-              if (!offsetTracker.isTracking(name)) {
-                offsetTracker.track(name, out.length);
-              }
+          case "array":
+          case "delta":
+            out = out.concat(this.encodeNumber(value));
 
-              out = out.concat([0x1d, 0, 0, 0, 0]);
-              break;
+            for (var k = 1, kk = values.length; k < kk; ++k) {
+              out = out.concat(this.encodeNumber(values[k]));
+            }
 
-            case "array":
-            case "delta":
-              out = out.concat(this.encodeNumber(value));
+            break;
 
-              for (var k = 1, kk = values.length; k < kk; ++k) {
-                out = out.concat(this.encodeNumber(values[k]));
-              }
+          default:
+            throw new _util.FormatError(`Unknown data type of ${type}`);
+        }
+      }
 
-              break;
+      out = out.concat(dict.opcodes[key]);
+    }
 
-            default:
-              throw new _util.FormatError(`Unknown data type of ${type}`);
-          }
-        }
+    return out;
+  }
 
-        out = out.concat(dict.opcodes[key]);
-      }
+  compileStringIndex(strings) {
+    var stringIndex = new CFFIndex();
 
-      return out;
-    },
-    compileStringIndex: function CFFCompiler_compileStringIndex(strings) {
-      var stringIndex = new CFFIndex();
+    for (var i = 0, ii = strings.length; i < ii; ++i) {
+      stringIndex.add((0, _util.stringToBytes)(strings[i]));
+    }
 
-      for (var i = 0, ii = strings.length; i < ii; ++i) {
-        stringIndex.add((0, _util.stringToBytes)(strings[i]));
-      }
+    return this.compileIndex(stringIndex);
+  }
 
-      return this.compileIndex(stringIndex);
-    },
-    compileGlobalSubrIndex: function CFFCompiler_compileGlobalSubrIndex() {
-      var globalSubrIndex = this.cff.globalSubrIndex;
-      this.out.writeByteArray(this.compileIndex(globalSubrIndex));
-    },
-    compileCharStrings: function CFFCompiler_compileCharStrings(charStrings) {
-      var charStringsIndex = new CFFIndex();
+  compileGlobalSubrIndex() {
+    var globalSubrIndex = this.cff.globalSubrIndex;
+    this.out.writeByteArray(this.compileIndex(globalSubrIndex));
+  }
 
-      for (var i = 0; i < charStrings.count; i++) {
-        var glyph = charStrings.get(i);
+  compileCharStrings(charStrings) {
+    var charStringsIndex = new CFFIndex();
 
-        if (glyph.length === 0) {
-          charStringsIndex.add(new Uint8Array([0x8b, 0x0e]));
-          continue;
-        }
+    for (var i = 0; i < charStrings.count; i++) {
+      var glyph = charStrings.get(i);
 
-        charStringsIndex.add(glyph);
+      if (glyph.length === 0) {
+        charStringsIndex.add(new Uint8Array([0x8b, 0x0e]));
+        continue;
       }
 
-      return this.compileIndex(charStringsIndex);
-    },
-    compileCharset: function CFFCompiler_compileCharset(charset, numGlyphs, strings, isCIDFont) {
-      let out;
-      const numGlyphsLessNotDef = numGlyphs - 1;
+      charStringsIndex.add(glyph);
+    }
 
-      if (isCIDFont) {
-        out = new Uint8Array([2, 0, 0, numGlyphsLessNotDef >> 8 & 0xff, numGlyphsLessNotDef & 0xff]);
-      } else {
-        const length = 1 + numGlyphsLessNotDef * 2;
-        out = new Uint8Array(length);
-        out[0] = 0;
-        let charsetIndex = 0;
-        const numCharsets = charset.charset.length;
-        let warned = false;
-
-        for (let i = 1; i < out.length; i += 2) {
-          let sid = 0;
-
-          if (charsetIndex < numCharsets) {
-            const name = charset.charset[charsetIndex++];
-            sid = strings.getSID(name);
-
-            if (sid === -1) {
-              sid = 0;
-
-              if (!warned) {
-                warned = true;
-                (0, _util.warn)(`Couldn't find ${name} in CFF strings`);
-              }
+    return this.compileIndex(charStringsIndex);
+  }
+
+  compileCharset(charset, numGlyphs, strings, isCIDFont) {
+    let out;
+    const numGlyphsLessNotDef = numGlyphs - 1;
+
+    if (isCIDFont) {
+      out = new Uint8Array([2, 0, 0, numGlyphsLessNotDef >> 8 & 0xff, numGlyphsLessNotDef & 0xff]);
+    } else {
+      const length = 1 + numGlyphsLessNotDef * 2;
+      out = new Uint8Array(length);
+      out[0] = 0;
+      let charsetIndex = 0;
+      const numCharsets = charset.charset.length;
+      let warned = false;
+
+      for (let i = 1; i < out.length; i += 2) {
+        let sid = 0;
+
+        if (charsetIndex < numCharsets) {
+          const name = charset.charset[charsetIndex++];
+          sid = strings.getSID(name);
+
+          if (sid === -1) {
+            sid = 0;
+
+            if (!warned) {
+              warned = true;
+              (0, _util.warn)(`Couldn't find ${name} in CFF strings`);
             }
           }
-
-          out[i] = sid >> 8 & 0xff;
-          out[i + 1] = sid & 0xff;
         }
+
+        out[i] = sid >> 8 & 0xff;
+        out[i + 1] = sid & 0xff;
       }
+    }
 
-      return this.compileTypedArray(out);
-    },
-    compileEncoding: function CFFCompiler_compileEncoding(encoding) {
-      return this.compileTypedArray(encoding.raw);
-    },
-    compileFDSelect: function CFFCompiler_compileFDSelect(fdSelect) {
-      const format = fdSelect.format;
-      let out, i;
+    return this.compileTypedArray(out);
+  }
 
-      switch (format) {
-        case 0:
-          out = new Uint8Array(1 + fdSelect.fdSelect.length);
-          out[0] = format;
+  compileEncoding(encoding) {
+    return this.compileTypedArray(encoding.raw);
+  }
 
-          for (i = 0; i < fdSelect.fdSelect.length; i++) {
-            out[i + 1] = fdSelect.fdSelect[i];
-          }
+  compileFDSelect(fdSelect) {
+    const format = fdSelect.format;
+    let out, i;
 
-          break;
+    switch (format) {
+      case 0:
+        out = new Uint8Array(1 + fdSelect.fdSelect.length);
+        out[0] = format;
 
-        case 3:
-          const start = 0;
-          let lastFD = fdSelect.fdSelect[0];
-          const ranges = [format, 0, 0, start >> 8 & 0xff, start & 0xff, lastFD];
+        for (i = 0; i < fdSelect.fdSelect.length; i++) {
+          out[i + 1] = fdSelect.fdSelect[i];
+        }
 
-          for (i = 1; i < fdSelect.fdSelect.length; i++) {
-            const currentFD = fdSelect.fdSelect[i];
+        break;
 
-            if (currentFD !== lastFD) {
-              ranges.push(i >> 8 & 0xff, i & 0xff, currentFD);
-              lastFD = currentFD;
-            }
+      case 3:
+        const start = 0;
+        let lastFD = fdSelect.fdSelect[0];
+        const ranges = [format, 0, 0, start >> 8 & 0xff, start & 0xff, lastFD];
+
+        for (i = 1; i < fdSelect.fdSelect.length; i++) {
+          const currentFD = fdSelect.fdSelect[i];
+
+          if (currentFD !== lastFD) {
+            ranges.push(i >> 8 & 0xff, i & 0xff, currentFD);
+            lastFD = currentFD;
           }
+        }
 
-          const numRanges = (ranges.length - 3) / 3;
-          ranges[1] = numRanges >> 8 & 0xff;
-          ranges[2] = numRanges & 0xff;
-          ranges.push(i >> 8 & 0xff, i & 0xff);
-          out = new Uint8Array(ranges);
-          break;
-      }
+        const numRanges = (ranges.length - 3) / 3;
+        ranges[1] = numRanges >> 8 & 0xff;
+        ranges[2] = numRanges & 0xff;
+        ranges.push(i >> 8 & 0xff, i & 0xff);
+        out = new Uint8Array(ranges);
+        break;
+    }
 
-      return this.compileTypedArray(out);
-    },
-    compileTypedArray: function CFFCompiler_compileTypedArray(data) {
-      var out = [];
+    return this.compileTypedArray(out);
+  }
 
-      for (var i = 0, ii = data.length; i < ii; ++i) {
-        out[i] = data[i];
-      }
+  compileTypedArray(data) {
+    var out = [];
 
-      return out;
-    },
-    compileIndex: function CFFCompiler_compileIndex(index, trackers) {
-      trackers = trackers || [];
-      var objects = index.objects;
-      var count = objects.length;
+    for (var i = 0, ii = data.length; i < ii; ++i) {
+      out[i] = data[i];
+    }
 
-      if (count === 0) {
-        return [0, 0, 0];
-      }
+    return out;
+  }
 
-      var data = [count >> 8 & 0xff, count & 0xff];
-      var lastOffset = 1,
-          i;
+  compileIndex(index, trackers = []) {
+    var objects = index.objects;
+    var count = objects.length;
 
-      for (i = 0; i < count; ++i) {
-        lastOffset += objects[i].length;
-      }
+    if (count === 0) {
+      return [0, 0, 0];
+    }
 
-      var offsetSize;
+    var data = [count >> 8 & 0xff, count & 0xff];
+    var lastOffset = 1,
+        i;
 
-      if (lastOffset < 0x100) {
-        offsetSize = 1;
-      } else if (lastOffset < 0x10000) {
-        offsetSize = 2;
-      } else if (lastOffset < 0x1000000) {
-        offsetSize = 3;
-      } else {
-        offsetSize = 4;
-      }
+    for (i = 0; i < count; ++i) {
+      lastOffset += objects[i].length;
+    }
 
-      data.push(offsetSize);
-      var relativeOffset = 1;
+    var offsetSize;
 
-      for (i = 0; i < count + 1; i++) {
-        if (offsetSize === 1) {
-          data.push(relativeOffset & 0xff);
-        } else if (offsetSize === 2) {
-          data.push(relativeOffset >> 8 & 0xff, relativeOffset & 0xff);
-        } else if (offsetSize === 3) {
-          data.push(relativeOffset >> 16 & 0xff, relativeOffset >> 8 & 0xff, relativeOffset & 0xff);
-        } else {
-          data.push(relativeOffset >>> 24 & 0xff, relativeOffset >> 16 & 0xff, relativeOffset >> 8 & 0xff, relativeOffset & 0xff);
-        }
+    if (lastOffset < 0x100) {
+      offsetSize = 1;
+    } else if (lastOffset < 0x10000) {
+      offsetSize = 2;
+    } else if (lastOffset < 0x1000000) {
+      offsetSize = 3;
+    } else {
+      offsetSize = 4;
+    }
 
-        if (objects[i]) {
-          relativeOffset += objects[i].length;
-        }
+    data.push(offsetSize);
+    var relativeOffset = 1;
+
+    for (i = 0; i < count + 1; i++) {
+      if (offsetSize === 1) {
+        data.push(relativeOffset & 0xff);
+      } else if (offsetSize === 2) {
+        data.push(relativeOffset >> 8 & 0xff, relativeOffset & 0xff);
+      } else if (offsetSize === 3) {
+        data.push(relativeOffset >> 16 & 0xff, relativeOffset >> 8 & 0xff, relativeOffset & 0xff);
+      } else {
+        data.push(relativeOffset >>> 24 & 0xff, relativeOffset >> 16 & 0xff, relativeOffset >> 8 & 0xff, relativeOffset & 0xff);
       }
 
-      for (i = 0; i < count; i++) {
-        if (trackers[i]) {
-          trackers[i].offset(data.length);
-        }
+      if (objects[i]) {
+        relativeOffset += objects[i].length;
+      }
+    }
 
-        for (var j = 0, jj = objects[i].length; j < jj; j++) {
-          data.push(objects[i][j]);
-        }
+    for (i = 0; i < count; i++) {
+      if (trackers[i]) {
+        trackers[i].offset(data.length);
       }
 
-      return data;
+      for (var j = 0, jj = objects[i].length; j < jj; j++) {
+        data.push(objects[i][j]);
+      }
     }
-  };
-  return CFFCompiler;
-}();
+
+    return data;
+  }
+
+}
 
 exports.CFFCompiler = CFFCompiler;

+ 64 - 50
lib/core/chunked_stream.js

@@ -37,8 +37,7 @@ class ChunkedStream {
     this.pos = 0;
     this.end = length;
     this.chunkSize = chunkSize;
-    this.loadedChunks = [];
-    this.numChunksLoaded = 0;
+    this._loadedChunks = new Set();
     this.numChunks = Math.ceil(length / chunkSize);
     this.manager = manager;
     this.progressiveDataLength = 0;
@@ -49,7 +48,7 @@ class ChunkedStream {
     const chunks = [];
 
     for (let chunk = 0, n = this.numChunks; chunk < n; ++chunk) {
-      if (!this.loadedChunks[chunk]) {
+      if (!this._loadedChunks.has(chunk)) {
         chunks.push(chunk);
       }
     }
@@ -61,6 +60,10 @@ class ChunkedStream {
     return [this];
   }
 
+  get numChunksLoaded() {
+    return this._loadedChunks.size;
+  }
+
   allChunksLoaded() {
     return this.numChunksLoaded === this.numChunks;
   }
@@ -83,10 +86,7 @@ class ChunkedStream {
     const endChunk = Math.floor((end - 1) / chunkSize) + 1;
 
     for (let curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
-      if (!this.loadedChunks[curChunk]) {
-        this.loadedChunks[curChunk] = true;
-        ++this.numChunksLoaded;
-      }
+      this._loadedChunks.add(curChunk);
     }
   }
 
@@ -99,10 +99,7 @@ class ChunkedStream {
     const endChunk = position >= this.end ? this.numChunks : Math.floor(position / this.chunkSize);
 
     for (let curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
-      if (!this.loadedChunks[curChunk]) {
-        this.loadedChunks[curChunk] = true;
-        ++this.numChunksLoaded;
-      }
+      this._loadedChunks.add(curChunk);
     }
   }
 
@@ -117,7 +114,7 @@ class ChunkedStream {
       return;
     }
 
-    if (!this.loadedChunks[chunk]) {
+    if (!this._loadedChunks.has(chunk)) {
       throw new _core_utils.MissingDataException(pos, pos + 1);
     }
 
@@ -138,7 +135,7 @@ class ChunkedStream {
     const endChunk = Math.floor((end - 1) / chunkSize) + 1;
 
     for (let chunk = beginChunk; chunk < endChunk; ++chunk) {
-      if (!this.loadedChunks[chunk]) {
+      if (!this._loadedChunks.has(chunk)) {
         throw new _core_utils.MissingDataException(begin, end);
       }
     }
@@ -150,7 +147,7 @@ class ChunkedStream {
     for (let i = 0; i < numChunks; ++i) {
       const chunk = (beginChunk + i) % numChunks;
 
-      if (!this.loadedChunks[chunk]) {
+      if (!this._loadedChunks.has(chunk)) {
         return chunk;
       }
     }
@@ -159,7 +156,7 @@ class ChunkedStream {
   }
 
   hasChunk(chunk) {
-    return !!this.loadedChunks[chunk];
+    return this._loadedChunks.has(chunk);
   }
 
   get length() {
@@ -302,7 +299,7 @@ class ChunkedStream {
       const missingChunks = [];
 
       for (let chunk = beginChunk; chunk < endChunk; ++chunk) {
-        if (!this.loadedChunks[chunk]) {
+        if (!this._loadedChunks.has(chunk)) {
           missingChunks.push(chunk);
         }
       }
@@ -338,9 +335,9 @@ class ChunkedStreamManager {
     this.disableAutoFetch = args.disableAutoFetch;
     this.msgHandler = args.msgHandler;
     this.currRequestId = 0;
-    this.chunksNeededByRequest = Object.create(null);
-    this.requestsByChunk = Object.create(null);
-    this.promisesByRequest = Object.create(null);
+    this._chunksNeededByRequest = new Map();
+    this._requestsByChunk = new Map();
+    this._promisesByRequest = new Map();
     this.progressiveDataLength = 0;
     this.aborted = false;
     this._loadedStreamCapability = (0, _util.createPromiseCapability)();
@@ -409,47 +406,57 @@ class ChunkedStreamManager {
 
   _requestChunks(chunks) {
     const requestId = this.currRequestId++;
-    const chunksNeeded = Object.create(null);
-    this.chunksNeededByRequest[requestId] = chunksNeeded;
+    const chunksNeeded = new Set();
+
+    this._chunksNeededByRequest.set(requestId, chunksNeeded);
 
     for (const chunk of chunks) {
       if (!this.stream.hasChunk(chunk)) {
-        chunksNeeded[chunk] = true;
+        chunksNeeded.add(chunk);
       }
     }
 
-    if ((0, _util.isEmptyObj)(chunksNeeded)) {
+    if (chunksNeeded.size === 0) {
       return Promise.resolve();
     }
 
     const capability = (0, _util.createPromiseCapability)();
-    this.promisesByRequest[requestId] = capability;
+
+    this._promisesByRequest.set(requestId, capability);
+
     const chunksToRequest = [];
 
-    for (let chunk in chunksNeeded) {
-      chunk = chunk | 0;
+    for (const chunk of chunksNeeded) {
+      let requestIds = this._requestsByChunk.get(chunk);
+
+      if (!requestIds) {
+        requestIds = [];
+
+        this._requestsByChunk.set(chunk, requestIds);
 
-      if (!(chunk in this.requestsByChunk)) {
-        this.requestsByChunk[chunk] = [];
         chunksToRequest.push(chunk);
       }
 
-      this.requestsByChunk[chunk].push(requestId);
-    }
-
-    if (!chunksToRequest.length) {
-      return capability.promise;
+      requestIds.push(requestId);
     }
 
-    const groupedChunksToRequest = this.groupChunks(chunksToRequest);
+    if (chunksToRequest.length > 0) {
+      const groupedChunksToRequest = this.groupChunks(chunksToRequest);
 
-    for (const groupedChunk of groupedChunksToRequest) {
-      const begin = groupedChunk.beginChunk * this.chunkSize;
-      const end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length);
-      this.sendRequest(begin, end);
+      for (const groupedChunk of groupedChunksToRequest) {
+        const begin = groupedChunk.beginChunk * this.chunkSize;
+        const end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length);
+        this.sendRequest(begin, end);
+      }
     }
 
-    return capability.promise;
+    return capability.promise.catch(reason => {
+      if (this.aborted) {
+        return;
+      }
+
+      throw reason;
+    });
   }
 
   getStream() {
@@ -551,17 +558,22 @@ class ChunkedStreamManager {
     const loadedRequests = [];
 
     for (let curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
-      const requestIds = this.requestsByChunk[curChunk] || [];
-      delete this.requestsByChunk[curChunk];
+      const requestIds = this._requestsByChunk.get(curChunk);
+
+      if (!requestIds) {
+        continue;
+      }
+
+      this._requestsByChunk.delete(curChunk);
 
       for (const requestId of requestIds) {
-        const chunksNeeded = this.chunksNeededByRequest[requestId];
+        const chunksNeeded = this._chunksNeededByRequest.get(requestId);
 
-        if (curChunk in chunksNeeded) {
-          delete chunksNeeded[curChunk];
+        if (chunksNeeded.has(curChunk)) {
+          chunksNeeded.delete(curChunk);
         }
 
-        if (!(0, _util.isEmptyObj)(chunksNeeded)) {
+        if (chunksNeeded.size > 0) {
           continue;
         }
 
@@ -569,7 +581,7 @@ class ChunkedStreamManager {
       }
     }
 
-    if (!this.disableAutoFetch && (0, _util.isEmptyObj)(this.requestsByChunk)) {
+    if (!this.disableAutoFetch && this._requestsByChunk.size === 0) {
       let nextEmptyChunk;
 
       if (this.stream.numChunksLoaded === 1) {
@@ -588,8 +600,10 @@ class ChunkedStreamManager {
     }
 
     for (const requestId of loadedRequests) {
-      const capability = this.promisesByRequest[requestId];
-      delete this.promisesByRequest[requestId];
+      const capability = this._promisesByRequest.get(requestId);
+
+      this._promisesByRequest.delete(requestId);
+
       capability.resolve();
     }
 
@@ -618,8 +632,8 @@ class ChunkedStreamManager {
       this.pdfNetworkStream.cancelAllRequests(reason);
     }
 
-    for (const requestId in this.promisesByRequest) {
-      this.promisesByRequest[requestId].reject(reason);
+    for (const capability of this._promisesByRequest.values()) {
+      capability.reject(reason);
     }
   }
 

+ 126 - 104
lib/core/colorspace.js

@@ -30,6 +30,8 @@ var _util = require("../shared/util.js");
 
 var _primitives = require("./primitives.js");
 
+var _core_utils = require("./core_utils.js");
+
 function resizeRgbImage(src, dest, w1, h1, w2, h2, alpha01) {
   const COMPONENTS = 3;
   alpha01 = alpha01 !== 1 ? 0 : alpha01;
@@ -162,135 +164,162 @@ class ColorSpace {
     return (0, _util.shadow)(this, "usesZeroToOneRange", true);
   }
 
-  static parse(cs, xref, res, pdfFunctionFactory) {
-    const IR = this.parseToIR(cs, xref, res, pdfFunctionFactory);
-    return this.fromIR(IR);
-  }
+  static _cache(cacheKey, xref, localColorSpaceCache, parsedColorSpace) {
+    if (!localColorSpaceCache) {
+      throw new Error('ColorSpace._cache - expected "localColorSpaceCache" argument.');
+    }
 
-  static fromIR(IR) {
-    const name = Array.isArray(IR) ? IR[0] : IR;
-    let whitePoint, blackPoint, gamma;
+    if (!parsedColorSpace) {
+      throw new Error('ColorSpace._cache - expected "parsedColorSpace" argument.');
+    }
 
-    switch (name) {
-      case "DeviceGrayCS":
-        return this.singletons.gray;
+    let csName, csRef;
 
-      case "DeviceRgbCS":
-        return this.singletons.rgb;
+    if (cacheKey instanceof _primitives.Ref) {
+      csRef = cacheKey;
+      cacheKey = xref.fetch(cacheKey);
+    }
 
-      case "DeviceCmykCS":
-        return this.singletons.cmyk;
+    if (cacheKey instanceof _primitives.Name) {
+      csName = cacheKey.name;
+    }
 
-      case "CalGrayCS":
-        whitePoint = IR[1];
-        blackPoint = IR[2];
-        gamma = IR[3];
-        return new CalGrayCS(whitePoint, blackPoint, gamma);
+    if (csName || csRef) {
+      localColorSpaceCache.set(csName, csRef, parsedColorSpace);
+    }
+  }
 
-      case "CalRGBCS":
-        whitePoint = IR[1];
-        blackPoint = IR[2];
-        gamma = IR[3];
-        const matrix = IR[4];
-        return new CalRGBCS(whitePoint, blackPoint, gamma, matrix);
+  static getCached(cacheKey, xref, localColorSpaceCache) {
+    if (!localColorSpaceCache) {
+      throw new Error('ColorSpace.getCached - expected "localColorSpaceCache" argument.');
+    }
+
+    if (cacheKey instanceof _primitives.Ref) {
+      const localColorSpace = localColorSpaceCache.getByRef(cacheKey);
 
-      case "PatternCS":
-        let basePatternCS = IR[1];
+      if (localColorSpace) {
+        return localColorSpace;
+      }
 
-        if (basePatternCS) {
-          basePatternCS = this.fromIR(basePatternCS);
+      try {
+        cacheKey = xref.fetch(cacheKey);
+      } catch (ex) {
+        if (ex instanceof _core_utils.MissingDataException) {
+          throw ex;
         }
+      }
+    }
 
-        return new PatternCS(basePatternCS);
+    if (cacheKey instanceof _primitives.Name) {
+      const localColorSpace = localColorSpaceCache.getByName(cacheKey.name);
 
-      case "IndexedCS":
-        const baseIndexedCS = IR[1];
-        const hiVal = IR[2];
-        const lookup = IR[3];
-        return new IndexedCS(this.fromIR(baseIndexedCS), hiVal, lookup);
+      if (localColorSpace) {
+        return localColorSpace;
+      }
+    }
 
-      case "AlternateCS":
-        const numComps = IR[1];
-        const alt = IR[2];
-        const tintFn = IR[3];
-        return new AlternateCS(numComps, this.fromIR(alt), tintFn);
+    return null;
+  }
+
+  static async parseAsync({
+    cs,
+    xref,
+    resources = null,
+    pdfFunctionFactory,
+    localColorSpaceCache
+  }) {
+    const parsedColorSpace = this._parse(cs, xref, resources, pdfFunctionFactory);
 
-      case "LabCS":
-        whitePoint = IR[1];
-        blackPoint = IR[2];
-        const range = IR[3];
-        return new LabCS(whitePoint, blackPoint, range);
+    this._cache(cs, xref, localColorSpaceCache, parsedColorSpace);
+
+    return parsedColorSpace;
+  }
 
-      default:
-        throw new _util.FormatError(`Unknown colorspace name: ${name}`);
+  static parse({
+    cs,
+    xref,
+    resources = null,
+    pdfFunctionFactory,
+    localColorSpaceCache
+  }) {
+    const cachedColorSpace = this.getCached(cs, xref, localColorSpaceCache);
+
+    if (cachedColorSpace) {
+      return cachedColorSpace;
     }
+
+    const parsedColorSpace = this._parse(cs, xref, resources, pdfFunctionFactory);
+
+    this._cache(cs, xref, localColorSpaceCache, parsedColorSpace);
+
+    return parsedColorSpace;
   }
 
-  static parseToIR(cs, xref, res = null, pdfFunctionFactory) {
+  static _parse(cs, xref, resources = null, pdfFunctionFactory) {
     cs = xref.fetchIfRef(cs);
 
     if ((0, _primitives.isName)(cs)) {
       switch (cs.name) {
         case "DeviceGray":
         case "G":
-          return "DeviceGrayCS";
+          return this.singletons.gray;
 
         case "DeviceRGB":
         case "RGB":
-          return "DeviceRgbCS";
+          return this.singletons.rgb;
 
         case "DeviceCMYK":
         case "CMYK":
-          return "DeviceCmykCS";
+          return this.singletons.cmyk;
 
         case "Pattern":
-          return ["PatternCS", null];
+          return new PatternCS(null);
 
         default:
-          if ((0, _primitives.isDict)(res)) {
-            const colorSpaces = res.get("ColorSpace");
+          if ((0, _primitives.isDict)(resources)) {
+            const colorSpaces = resources.get("ColorSpace");
 
             if ((0, _primitives.isDict)(colorSpaces)) {
-              const resCS = colorSpaces.get(cs.name);
+              const resourcesCS = colorSpaces.get(cs.name);
 
-              if (resCS) {
-                if ((0, _primitives.isName)(resCS)) {
-                  return this.parseToIR(resCS, xref, res, pdfFunctionFactory);
+              if (resourcesCS) {
+                if ((0, _primitives.isName)(resourcesCS)) {
+                  return this._parse(resourcesCS, xref, resources, pdfFunctionFactory);
                 }
 
-                cs = resCS;
+                cs = resourcesCS;
                 break;
               }
             }
           }
 
-          throw new _util.FormatError(`unrecognized colorspace ${cs.name}`);
+          throw new _util.FormatError(`Unrecognized ColorSpace: ${cs.name}`);
       }
     }
 
     if (Array.isArray(cs)) {
       const mode = xref.fetchIfRef(cs[0]).name;
-      let numComps, params, alt, whitePoint, blackPoint, gamma;
+      let params, numComps, baseCS, whitePoint, blackPoint, gamma;
 
       switch (mode) {
         case "DeviceGray":
         case "G":
-          return "DeviceGrayCS";
+          return this.singletons.gray;
 
         case "DeviceRGB":
         case "RGB":
-          return "DeviceRgbCS";
+          return this.singletons.rgb;
 
         case "DeviceCMYK":
         case "CMYK":
-          return "DeviceCmykCS";
+          return this.singletons.cmyk;
 
         case "CalGray":
           params = xref.fetchIfRef(cs[1]);
           whitePoint = params.getArray("WhitePoint");
           blackPoint = params.getArray("BlackPoint");
           gamma = params.get("Gamma");
-          return ["CalGrayCS", whitePoint, blackPoint, gamma];
+          return new CalGrayCS(whitePoint, blackPoint, gamma);
 
         case "CalRGB":
           params = xref.fetchIfRef(cs[1]);
@@ -298,77 +327,71 @@ class ColorSpace {
           blackPoint = params.getArray("BlackPoint");
           gamma = params.getArray("Gamma");
           const matrix = params.getArray("Matrix");
-          return ["CalRGBCS", whitePoint, blackPoint, gamma, matrix];
+          return new CalRGBCS(whitePoint, blackPoint, gamma, matrix);
 
         case "ICCBased":
           const stream = xref.fetchIfRef(cs[1]);
           const dict = stream.dict;
           numComps = dict.get("N");
-          alt = dict.get("Alternate");
+          const alt = dict.get("Alternate");
 
           if (alt) {
-            const altIR = this.parseToIR(alt, xref, res, pdfFunctionFactory);
-            const altCS = this.fromIR(altIR, pdfFunctionFactory);
+            const altCS = this._parse(alt, xref, resources, pdfFunctionFactory);
 
             if (altCS.numComps === numComps) {
-              return altIR;
+              return altCS;
             }
 
             (0, _util.warn)("ICCBased color space: Ignoring incorrect /Alternate entry.");
           }
 
           if (numComps === 1) {
-            return "DeviceGrayCS";
+            return this.singletons.gray;
           } else if (numComps === 3) {
-            return "DeviceRgbCS";
+            return this.singletons.rgb;
           } else if (numComps === 4) {
-            return "DeviceCmykCS";
+            return this.singletons.cmyk;
           }
 
           break;
 
         case "Pattern":
-          let basePatternCS = cs[1] || null;
+          baseCS = cs[1] || null;
 
-          if (basePatternCS) {
-            basePatternCS = this.parseToIR(basePatternCS, xref, res, pdfFunctionFactory);
+          if (baseCS) {
+            baseCS = this._parse(baseCS, xref, resources, pdfFunctionFactory);
           }
 
-          return ["PatternCS", basePatternCS];
+          return new PatternCS(baseCS);
 
         case "Indexed":
         case "I":
-          const baseIndexedCS = this.parseToIR(cs[1], xref, res, pdfFunctionFactory);
+          baseCS = this._parse(cs[1], xref, resources, pdfFunctionFactory);
           const hiVal = xref.fetchIfRef(cs[2]) + 1;
-          let lookup = xref.fetchIfRef(cs[3]);
-
-          if ((0, _primitives.isStream)(lookup)) {
-            lookup = lookup.getBytes();
-          }
-
-          return ["IndexedCS", baseIndexedCS, hiVal, lookup];
+          const lookup = xref.fetchIfRef(cs[3]);
+          return new IndexedCS(baseCS, hiVal, lookup);
 
         case "Separation":
         case "DeviceN":
           const name = xref.fetchIfRef(cs[1]);
           numComps = Array.isArray(name) ? name.length : 1;
-          alt = this.parseToIR(cs[2], xref, res, pdfFunctionFactory);
-          const tintFn = pdfFunctionFactory.create(xref.fetchIfRef(cs[3]));
-          return ["AlternateCS", numComps, alt, tintFn];
+          baseCS = this._parse(cs[2], xref, resources, pdfFunctionFactory);
+          const tintFn = pdfFunctionFactory.create(cs[3]);
+          return new AlternateCS(numComps, baseCS, tintFn);
 
         case "Lab":
           params = xref.fetchIfRef(cs[1]);
           whitePoint = params.getArray("WhitePoint");
           blackPoint = params.getArray("BlackPoint");
           const range = params.getArray("Range");
-          return ["LabCS", whitePoint, blackPoint, range];
+          return new LabCS(whitePoint, blackPoint, range);
 
         default:
-          throw new _util.FormatError(`unimplemented color space object "${mode}"`);
+          throw new _util.FormatError(`Unimplemented ColorSpace object: ${mode}`);
       }
     }
 
-    throw new _util.FormatError(`unrecognized color space object: "${cs}"`);
+    throw new _util.FormatError(`Unrecognized ColorSpace object: ${cs}`);
   }
 
   static isDefaultDecode(decode, numComps) {
@@ -484,23 +507,18 @@ class IndexedCS extends ColorSpace {
     super("Indexed", 1);
     this.base = base;
     this.highVal = highVal;
-    const baseNumComps = base.numComps;
-    const length = baseNumComps * highVal;
+    const length = base.numComps * highVal;
+    this.lookup = new Uint8Array(length);
 
     if ((0, _primitives.isStream)(lookup)) {
-      this.lookup = new Uint8Array(length);
       const bytes = lookup.getBytes(length);
       this.lookup.set(bytes);
-    } else if ((0, _util.isString)(lookup)) {
-      this.lookup = new Uint8Array(length);
-
+    } else if (typeof lookup === "string") {
       for (let i = 0; i < length; ++i) {
-        this.lookup[i] = lookup.charCodeAt(i);
+        this.lookup[i] = lookup.charCodeAt(i) & 0xff;
       }
-    } else if (lookup instanceof Uint8Array) {
-      this.lookup = lookup;
     } else {
-      throw new _util.FormatError(`Unrecognized lookup table: ${lookup}`);
+      throw new _util.FormatError(`IndexedCS - unrecognized lookup table: ${lookup}`);
     }
   }
 
@@ -762,6 +780,10 @@ const CalRGBCS = function CalRGBCSClosure() {
       return adjustToRange(0, 1, 12.92 * color);
     }
 
+    if (color >= 0.99554525) {
+      return 1;
+    }
+
     return adjustToRange(0, 1, (1 + 0.055) * color ** (1 / 2.4) - 0.055);
   }
 
@@ -834,9 +856,9 @@ const CalRGBCS = function CalRGBCSClosure() {
     const A = adjustToRange(0, 1, src[srcOffset] * scale);
     const B = adjustToRange(0, 1, src[srcOffset + 1] * scale);
     const C = adjustToRange(0, 1, src[srcOffset + 2] * scale);
-    const AGR = A ** cs.GR;
-    const BGG = B ** cs.GG;
-    const CGB = C ** cs.GB;
+    const AGR = A === 1 ? 1 : A ** cs.GR;
+    const BGG = B === 1 ? 1 : B ** cs.GG;
+    const CGB = C === 1 ? 1 : C ** cs.GB;
     const X = cs.MXA * AGR + cs.MXB * BGG + cs.MXC * CGB;
     const Y = cs.MYA * AGR + cs.MYB * BGG + cs.MYC * CGB;
     const Z = cs.MZA * AGR + cs.MZB * BGG + cs.MZC * CGB;

+ 37 - 0
lib/core/crypto.js

@@ -83,6 +83,7 @@ var ARCFourCipher = function ARCFourCipherClosure() {
     }
   };
   ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
+  ARCFourCipher.prototype.encrypt = ARCFourCipher.prototype.encryptBlock;
   return ARCFourCipher;
 }();
 
@@ -622,6 +623,9 @@ var NullCipher = function NullCipherClosure() {
   NullCipher.prototype = {
     decryptBlock: function NullCipher_decryptBlock(data) {
       return data;
+    },
+    encrypt: function NullCipher_encrypt(data) {
+      return data;
     }
   };
   return NullCipher;
@@ -1235,6 +1239,39 @@ var CipherTransform = function CipherTransformClosure() {
       var data = (0, _util.stringToBytes)(s);
       data = cipher.decryptBlock(data, true);
       return (0, _util.bytesToString)(data);
+    },
+    encryptString: function CipherTransform_encryptString(s) {
+      const cipher = new this.StringCipherConstructor();
+
+      if (cipher instanceof AESBaseCipher) {
+        const strLen = s.length;
+        const pad = 16 - strLen % 16;
+
+        if (pad !== 16) {
+          s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad));
+        }
+
+        const iv = new Uint8Array(16);
+
+        if (typeof crypto !== "undefined") {
+          crypto.getRandomValues(iv);
+        } else {
+          for (let i = 0; i < 16; i++) {
+            iv[i] = Math.floor(256 * Math.random());
+          }
+        }
+
+        let data = (0, _util.stringToBytes)(s);
+        data = cipher.encrypt(data, iv);
+        const buf = new Uint8Array(16 + data.length);
+        buf.set(iv);
+        buf.set(data, 16);
+        return (0, _util.bytesToString)(buf);
+      }
+
+      let data = (0, _util.stringToBytes)(s);
+      data = cipher.encrypt(data);
+      return (0, _util.bytesToString)(data);
     }
   };
   return CipherTransform;

+ 128 - 75
lib/core/document.js

@@ -46,8 +46,6 @@ var _operator_list = require("./operator_list.js");
 
 var _evaluator = require("./evaluator.js");
 
-var _function = require("./function.js");
-
 const DEFAULT_USER_UNIT = 1.0;
 const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
 
@@ -62,10 +60,10 @@ class Page {
     pageIndex,
     pageDict,
     ref,
+    globalIdFactory,
     fontCache,
     builtInCMapCache,
-    globalImageCache,
-    pdfFunctionFactory
+    globalImageCache
   }) {
     this.pdfManager = pdfManager;
     this.pageIndex = pageIndex;
@@ -75,19 +73,14 @@ class Page {
     this.fontCache = fontCache;
     this.builtInCMapCache = builtInCMapCache;
     this.globalImageCache = globalImageCache;
-    this.pdfFunctionFactory = pdfFunctionFactory;
     this.evaluatorOptions = pdfManager.evaluatorOptions;
     this.resourcesPromise = null;
     const idCounters = {
       obj: 0
     };
-    this.idFactory = {
-      createObjId() {
+    this._localIdFactory = class extends globalIdFactory {
+      static createObjId() {
         return `p${pageIndex}_${++idCounters.obj}`;
-      },
-
-      getDocId() {
-        return `g_${pdfManager.docId}`;
       }
 
     };
@@ -109,7 +102,10 @@ class Page {
       return value[0];
     }
 
-    return _primitives.Dict.merge(this.xref, value);
+    return _primitives.Dict.merge({
+      xref: this.xref,
+      dictArray: value
+    });
   }
 
   get content() {
@@ -210,6 +206,35 @@ class Page {
     return stream;
   }
 
+  save(handler, task, annotationStorage) {
+    const partialEvaluator = new _evaluator.PartialEvaluator({
+      xref: this.xref,
+      handler,
+      pageIndex: this.pageIndex,
+      idFactory: this._localIdFactory,
+      fontCache: this.fontCache,
+      builtInCMapCache: this.builtInCMapCache,
+      globalImageCache: this.globalImageCache,
+      options: this.evaluatorOptions
+    });
+    return this._parsedAnnotations.then(function (annotations) {
+      const newRefsPromises = [];
+
+      for (const annotation of annotations) {
+        if (!isAnnotationRenderable(annotation, "print")) {
+          continue;
+        }
+
+        newRefsPromises.push(annotation.save(partialEvaluator, task, annotationStorage).catch(function (reason) {
+          (0, _util.warn)("save - ignoring annotation data during " + `"${task.name}" task: "${reason}".`);
+          return null;
+        }));
+      }
+
+      return Promise.all(newRefsPromises);
+    });
+  }
+
   loadResources(keys) {
     if (!this.resourcesPromise) {
       this.resourcesPromise = this.pdfManager.ensure(this, "resources");
@@ -226,7 +251,8 @@ class Page {
     sink,
     task,
     intent,
-    renderInteractiveForms
+    renderInteractiveForms,
+    annotationStorage
   }) {
     const contentStreamPromise = this.pdfManager.ensure(this, "getContentStream");
     const resourcesPromise = this.loadResources(["ExtGState", "ColorSpace", "Pattern", "Shading", "XObject", "Font"]);
@@ -234,16 +260,15 @@ class Page {
       xref: this.xref,
       handler,
       pageIndex: this.pageIndex,
-      idFactory: this.idFactory,
+      idFactory: this._localIdFactory,
       fontCache: this.fontCache,
       builtInCMapCache: this.builtInCMapCache,
       globalImageCache: this.globalImageCache,
-      options: this.evaluatorOptions,
-      pdfFunctionFactory: this.pdfFunctionFactory
+      options: this.evaluatorOptions
     });
     const dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
     const pageListPromise = dataPromises.then(([contentStream]) => {
-      const opList = new _operator_list.OperatorList(intent, sink, this.pageIndex);
+      const opList = new _operator_list.OperatorList(intent, sink);
       handler.send("StartRenderPage", {
         transparency: partialEvaluator.hasBlendModes(this.resources),
         pageIndex: this.pageIndex,
@@ -270,7 +295,7 @@ class Page {
 
       for (const annotation of annotations) {
         if (isAnnotationRenderable(annotation, intent)) {
-          opListPromises.push(annotation.getOperatorList(partialEvaluator, task, renderInteractiveForms).catch(function (reason) {
+          opListPromises.push(annotation.getOperatorList(partialEvaluator, task, renderInteractiveForms, annotationStorage).catch(function (reason) {
             (0, _util.warn)("getOperatorList - ignoring annotation data during " + `"${task.name}" task: "${reason}".`);
             return null;
           }));
@@ -308,12 +333,11 @@ class Page {
         xref: this.xref,
         handler,
         pageIndex: this.pageIndex,
-        idFactory: this.idFactory,
+        idFactory: this._localIdFactory,
         fontCache: this.fontCache,
         builtInCMapCache: this.builtInCMapCache,
         globalImageCache: this.globalImageCache,
-        options: this.evaluatorOptions,
-        pdfFunctionFactory: this.pdfFunctionFactory
+        options: this.evaluatorOptions
       });
       return partialEvaluator.getTextContent({
         stream: contentStream,
@@ -349,7 +373,7 @@ class Page {
       const annotationPromises = [];
 
       for (const annotationRef of this.annotations) {
-        annotationPromises.push(_annotation.AnnotationFactory.create(this.xref, annotationRef, this.pdfManager, this.idFactory).catch(function (reason) {
+        annotationPromises.push(_annotation.AnnotationFactory.create(this.xref, annotationRef, this.pdfManager, this._localIdFactory).catch(function (reason) {
           (0, _util.warn)(`_parsedAnnotations: "${reason}".`);
           return null;
         }));
@@ -440,53 +464,33 @@ class PDFDocument {
     this.pdfManager = pdfManager;
     this.stream = stream;
     this.xref = new _obj.XRef(stream, pdfManager);
-    this.pdfFunctionFactory = new _function.PDFFunctionFactory({
-      xref: this.xref,
-      isEvalSupported: pdfManager.evaluatorOptions.isEvalSupported
-    });
     this._pagePromises = [];
-  }
-
-  parse(recoveryMode) {
-    this.setup(recoveryMode);
-    const version = this.catalog.catDict.get("Version");
-
-    if ((0, _primitives.isName)(version)) {
-      this.pdfFormatVersion = version.name;
-    }
-
-    try {
-      this.acroForm = this.catalog.catDict.get("AcroForm");
-
-      if (this.acroForm) {
-        this.xfa = this.acroForm.get("XFA");
-        const fields = this.acroForm.get("Fields");
-
-        if ((!Array.isArray(fields) || fields.length === 0) && !this.xfa) {
-          this.acroForm = null;
-        }
+    this._version = null;
+    const idCounters = {
+      font: 0
+    };
+    this._globalIdFactory = class {
+      static getDocId() {
+        return `g_${pdfManager.docId}`;
       }
-    } catch (ex) {
-      if (ex instanceof _core_utils.MissingDataException) {
-        throw ex;
+
+      static createFontId() {
+        return `f${++idCounters.font}`;
       }
 
-      (0, _util.info)("Cannot fetch AcroForm entry; assuming no AcroForms are present");
-      this.acroForm = null;
-    }
+      static createObjId() {
+        (0, _util.unreachable)("Abstract method `createObjId` called.");
+      }
 
-    try {
-      const collection = this.catalog.catDict.get("Collection");
+    };
+  }
 
-      if ((0, _primitives.isDict)(collection) && collection.getKeys().length > 0) {
-        this.collection = collection;
-      }
-    } catch (ex) {
-      if (ex instanceof _core_utils.MissingDataException) {
-        throw ex;
-      }
+  parse(recoveryMode) {
+    this.xref.parse(recoveryMode);
+    this.catalog = new _obj.Catalog(this.pdfManager, this.xref);
 
-      (0, _util.info)("Cannot fetch Collection dictionary.");
+    if (this.catalog.version) {
+      this._version = this.catalog.version;
     }
   }
 
@@ -580,8 +584,8 @@ class PDFDocument {
       version += String.fromCharCode(ch);
     }
 
-    if (!this.pdfFormatVersion) {
-      this.pdfFormatVersion = version.substring(5);
+    if (!this._version) {
+      this._version = version.substring(5);
     }
   }
 
@@ -589,17 +593,66 @@ class PDFDocument {
     this.xref.setStartXRef(this.startXRef);
   }
 
-  setup(recoveryMode) {
-    this.xref.parse(recoveryMode);
-    this.catalog = new _obj.Catalog(this.pdfManager, this.xref);
-  }
-
   get numPages() {
     const linearization = this.linearization;
     const num = linearization ? linearization.numPages : this.catalog.numPages;
     return (0, _util.shadow)(this, "numPages", num);
   }
 
+  _hasOnlyDocumentSignatures(fields, recursionDepth = 0) {
+    const RECURSION_LIMIT = 10;
+    return fields.every(field => {
+      field = this.xref.fetchIfRef(field);
+
+      if (field.has("Kids")) {
+        if (++recursionDepth > RECURSION_LIMIT) {
+          (0, _util.warn)("_hasOnlyDocumentSignatures: maximum recursion depth reached");
+          return false;
+        }
+
+        return this._hasOnlyDocumentSignatures(field.get("Kids"), recursionDepth);
+      }
+
+      const isSignature = (0, _primitives.isName)(field.get("FT"), "Sig");
+      const rectangle = field.get("Rect");
+      const isInvisible = Array.isArray(rectangle) && rectangle.every(value => value === 0);
+      return isSignature && isInvisible;
+    });
+  }
+
+  get formInfo() {
+    const formInfo = {
+      hasAcroForm: false,
+      hasXfa: false
+    };
+    const acroForm = this.catalog.acroForm;
+
+    if (!acroForm) {
+      return (0, _util.shadow)(this, "formInfo", formInfo);
+    }
+
+    try {
+      const xfa = acroForm.get("XFA");
+      const hasXfa = Array.isArray(xfa) && xfa.length > 0 || (0, _primitives.isStream)(xfa) && !xfa.isEmpty;
+      formInfo.hasXfa = hasXfa;
+      const fields = acroForm.get("Fields");
+      const hasFields = Array.isArray(fields) && fields.length > 0;
+      const sigFlags = acroForm.get("SigFlags");
+
+      const hasOnlyDocumentSignatures = !!(sigFlags & 0x1) && this._hasOnlyDocumentSignatures(fields);
+
+      formInfo.hasAcroForm = hasFields && !hasOnlyDocumentSignatures;
+    } catch (ex) {
+      if (ex instanceof _core_utils.MissingDataException) {
+        throw ex;
+      }
+
+      (0, _util.info)("Cannot fetch form information.");
+    }
+
+    return (0, _util.shadow)(this, "formInfo", formInfo);
+  }
+
   get documentInfo() {
     const DocumentInfoValidators = {
       Title: _util.isString,
@@ -612,7 +665,7 @@ class PDFDocument {
       ModDate: _util.isString,
       Trapped: _primitives.isName
     };
-    let version = this.pdfFormatVersion;
+    let version = this._version;
 
     if (typeof version !== "string" || !PDF_HEADER_VERSION_REGEXP.test(version)) {
       (0, _util.warn)(`Invalid PDF header version number: ${version}`);
@@ -622,9 +675,9 @@ class PDFDocument {
     const docInfo = {
       PDFFormatVersion: version,
       IsLinearized: !!this.linearization,
-      IsAcroFormPresent: !!this.acroForm,
-      IsXFAPresent: !!this.xfa,
-      IsCollectionPresent: !!this.collection
+      IsAcroFormPresent: this.formInfo.hasAcroForm,
+      IsXFAPresent: this.formInfo.hasXfa,
+      IsCollectionPresent: !!this.catalog.collection
     };
     let infoDict;
 
@@ -733,10 +786,10 @@ class PDFDocument {
         pageIndex,
         pageDict,
         ref,
+        globalIdFactory: this._globalIdFactory,
         fontCache: catalog.fontCache,
         builtInCMapCache: catalog.builtInCMapCache,
-        globalImageCache: catalog.globalImageCache,
-        pdfFunctionFactory: this.pdfFunctionFactory
+        globalImageCache: catalog.globalImageCache
       });
     });
   }

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2423 - 2164
lib/core/evaluator.js


+ 47 - 46
lib/core/fonts.js

@@ -522,13 +522,6 @@ var Font = function FontClosure() {
     this.seacMap = properties.seacMap;
   }
 
-  Font.getFontID = function () {
-    var ID = 1;
-    return function Font_getFontID() {
-      return String(ID++);
-    };
-  }();
-
   function int16(b0, b1) {
     return (b0 << 8) + b1;
   }
@@ -1279,7 +1272,7 @@ var Font = function FontClosure() {
             continue;
           }
 
-          if (platformId === 0 && encodingId === 0) {
+          if (platformId === 0 && (encodingId === 0 || encodingId === 1 || encodingId === 3)) {
             useTable = true;
           } else if (platformId === 1 && encodingId === 0) {
             useTable = true;
@@ -1669,28 +1662,40 @@ var Font = function FontClosure() {
         var oldGlyfData = glyf.data;
         var oldGlyfDataLength = oldGlyfData.length;
         var newGlyfData = new Uint8Array(oldGlyfDataLength);
-        var startOffset = itemDecode(locaData, 0);
-        var writeOffset = 0;
-        var missingGlyphs = Object.create(null);
-        itemEncode(locaData, 0, writeOffset);
         var i, j;
+        const locaEntries = [];
 
-        for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
-          var endOffset = itemDecode(locaData, j);
+        for (i = 0, j = 0; i < numGlyphs + 1; i++, j += itemSize) {
+          let offset = itemDecode(locaData, j);
 
-          if (endOffset === 0) {
-            endOffset = startOffset;
+          if (offset > oldGlyfDataLength) {
+            offset = oldGlyfDataLength;
           }
 
-          if (endOffset > oldGlyfDataLength && (oldGlyfDataLength + 3 & ~3) === endOffset) {
-            endOffset = oldGlyfDataLength;
-          }
+          locaEntries.push({
+            index: i,
+            offset,
+            endOffset: 0
+          });
+        }
 
-          if (endOffset > oldGlyfDataLength) {
-            startOffset = endOffset;
-          }
+        locaEntries.sort((a, b) => {
+          return a.offset - b.offset;
+        });
+
+        for (i = 0; i < numGlyphs; i++) {
+          locaEntries[i].endOffset = locaEntries[i + 1].offset;
+        }
 
-          var glyphProfile = sanitizeGlyph(oldGlyfData, startOffset, endOffset, newGlyfData, writeOffset, hintsValid);
+        locaEntries.sort((a, b) => {
+          return a.index - b.index;
+        });
+        var missingGlyphs = Object.create(null);
+        var writeOffset = 0;
+        itemEncode(locaData, 0, writeOffset);
+
+        for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
+          var glyphProfile = sanitizeGlyph(oldGlyfData, locaEntries[i].offset, locaEntries[i].endOffset, newGlyfData, writeOffset, hintsValid);
           var newLength = glyphProfile.length;
 
           if (newLength === 0) {
@@ -1703,7 +1708,6 @@ var Font = function FontClosure() {
 
           writeOffset += newLength;
           itemEncode(locaData, j, writeOffset);
-          startOffset = endOffset;
         }
 
         if (writeOffset === 0) {
@@ -2348,14 +2352,13 @@ var Font = function FontClosure() {
         var cmapEncodingId = cmapTable.encodingId;
         var cmapMappings = cmapTable.mappings;
         var cmapMappingsLength = cmapMappings.length;
+        let baseEncoding = [];
 
-        if (properties.hasEncoding && (cmapPlatformId === 3 && cmapEncodingId === 1 || cmapPlatformId === 1 && cmapEncodingId === 0) || cmapPlatformId === -1 && cmapEncodingId === -1 && !!(0, _encodings.getEncoding)(properties.baseEncodingName)) {
-          var baseEncoding = [];
-
-          if (properties.baseEncodingName === "MacRomanEncoding" || properties.baseEncodingName === "WinAnsiEncoding") {
-            baseEncoding = (0, _encodings.getEncoding)(properties.baseEncodingName);
-          }
+        if (properties.hasEncoding && (properties.baseEncodingName === "MacRomanEncoding" || properties.baseEncodingName === "WinAnsiEncoding")) {
+          baseEncoding = (0, _encodings.getEncoding)(properties.baseEncodingName);
+        }
 
+        if (properties.hasEncoding && !this.isSymbolicFont && (cmapPlatformId === 3 && cmapEncodingId === 1 || cmapPlatformId === 1 && cmapEncodingId === 0)) {
           var glyphsUnicodeMap = (0, _glyphlist.getGlyphsUnicode)();
 
           for (let charCode = 0; charCode < 256; charCode++) {
@@ -2382,31 +2385,16 @@ var Font = function FontClosure() {
               unicodeOrCharCode = _encodings.MacRomanEncoding.indexOf(standardGlyphName);
             }
 
-            var found = false;
-
             for (let i = 0; i < cmapMappingsLength; ++i) {
               if (cmapMappings[i].charCode !== unicodeOrCharCode) {
                 continue;
               }
 
               charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
-              found = true;
               break;
             }
-
-            if (!found && properties.glyphNames) {
-              var glyphId = properties.glyphNames.indexOf(glyphName);
-
-              if (glyphId === -1 && standardGlyphName !== glyphName) {
-                glyphId = properties.glyphNames.indexOf(standardGlyphName);
-              }
-
-              if (glyphId > 0 && hasGlyph(glyphId)) {
-                charCodeToGlyphId[charCode] = glyphId;
-              }
-            }
           }
-        } else if (cmapPlatformId === 0 && cmapEncodingId === 0) {
+        } else if (cmapPlatformId === 0) {
           for (let i = 0; i < cmapMappingsLength; ++i) {
             charCodeToGlyphId[cmapMappings[i].charCode] = cmapMappings[i].glyphId;
           }
@@ -2421,6 +2409,19 @@ var Font = function FontClosure() {
             charCodeToGlyphId[charCode] = cmapMappings[i].glyphId;
           }
         }
+
+        if (properties.glyphNames && baseEncoding.length) {
+          for (let i = 0; i < 256; ++i) {
+            if (charCodeToGlyphId[i] === undefined && baseEncoding[i]) {
+              glyphName = baseEncoding[i];
+              const glyphId = properties.glyphNames.indexOf(glyphName);
+
+              if (glyphId > 0 && hasGlyph(glyphId)) {
+                charCodeToGlyphId[i] = glyphId;
+              }
+            }
+          }
+        }
       }
 
       if (charCodeToGlyphId.length === 0) {

+ 79 - 6
lib/core/function.js

@@ -27,12 +27,14 @@ Object.defineProperty(exports, "__esModule", {
 exports.isPDFFunction = isPDFFunction;
 exports.PostScriptCompiler = exports.PostScriptEvaluator = exports.PDFFunctionFactory = void 0;
 
-var _util = require("../shared/util.js");
-
 var _primitives = require("./primitives.js");
 
+var _util = require("../shared/util.js");
+
 var _ps_parser = require("./ps_parser.js");
 
+var _image_utils = require("./image_utils.js");
+
 class PDFFunctionFactory {
   constructor({
     xref,
@@ -40,22 +42,93 @@ class PDFFunctionFactory {
   }) {
     this.xref = xref;
     this.isEvalSupported = isEvalSupported !== false;
+    this._localFunctionCache = null;
   }
 
   create(fn) {
-    return PDFFunction.parse({
+    const cachedFunction = this.getCached(fn);
+
+    if (cachedFunction) {
+      return cachedFunction;
+    }
+
+    const parsedFunction = PDFFunction.parse({
       xref: this.xref,
       isEvalSupported: this.isEvalSupported,
-      fn
+      fn: fn instanceof _primitives.Ref ? this.xref.fetch(fn) : fn
     });
+
+    this._cache(fn, parsedFunction);
+
+    return parsedFunction;
   }
 
   createFromArray(fnObj) {
-    return PDFFunction.parseArray({
+    const cachedFunction = this.getCached(fnObj);
+
+    if (cachedFunction) {
+      return cachedFunction;
+    }
+
+    const parsedFunction = PDFFunction.parseArray({
       xref: this.xref,
       isEvalSupported: this.isEvalSupported,
-      fnObj
+      fnObj: fnObj instanceof _primitives.Ref ? this.xref.fetch(fnObj) : fnObj
     });
+
+    this._cache(fnObj, parsedFunction);
+
+    return parsedFunction;
+  }
+
+  getCached(cacheKey) {
+    let fnRef;
+
+    if (cacheKey instanceof _primitives.Ref) {
+      fnRef = cacheKey;
+    } else if (cacheKey instanceof _primitives.Dict) {
+      fnRef = cacheKey.objId;
+    } else if ((0, _primitives.isStream)(cacheKey)) {
+      fnRef = cacheKey.dict && cacheKey.dict.objId;
+    }
+
+    if (fnRef) {
+      if (!this._localFunctionCache) {
+        this._localFunctionCache = new _image_utils.LocalFunctionCache();
+      }
+
+      const localFunction = this._localFunctionCache.getByRef(fnRef);
+
+      if (localFunction) {
+        return localFunction;
+      }
+    }
+
+    return null;
+  }
+
+  _cache(cacheKey, parsedFunction) {
+    if (!parsedFunction) {
+      throw new Error('PDFFunctionFactory._cache - expected "parsedFunction" argument.');
+    }
+
+    let fnRef;
+
+    if (cacheKey instanceof _primitives.Ref) {
+      fnRef = cacheKey;
+    } else if (cacheKey instanceof _primitives.Dict) {
+      fnRef = cacheKey.objId;
+    } else if ((0, _primitives.isStream)(cacheKey)) {
+      fnRef = cacheKey.dict && cacheKey.dict.objId;
+    }
+
+    if (fnRef) {
+      if (!this._localFunctionCache) {
+        this._localFunctionCache = new _image_utils.LocalFunctionCache();
+      }
+
+      this._localFunctionCache.set(null, fnRef, parsedFunction);
+    }
   }
 
 }

+ 364 - 357
lib/core/image.js

@@ -38,58 +38,58 @@ var _jpeg_stream = require("./jpeg_stream.js");
 
 var _jpx = require("./jpx.js");
 
-var PDFImage = function PDFImageClosure() {
-  function decodeAndClamp(value, addend, coefficient, max) {
-    value = addend + value * coefficient;
-
-    if (value < 0) {
-      value = 0;
-    } else if (value > max) {
-      value = max;
-    }
+function decodeAndClamp(value, addend, coefficient, max) {
+  value = addend + value * coefficient;
 
-    return value;
+  if (value < 0) {
+    value = 0;
+  } else if (value > max) {
+    value = max;
   }
 
-  function resizeImageMask(src, bpc, w1, h1, w2, h2) {
-    var length = w2 * h2;
-    let dest;
+  return value;
+}
 
-    if (bpc <= 8) {
-      dest = new Uint8Array(length);
-    } else if (bpc <= 16) {
-      dest = new Uint16Array(length);
-    } else {
-      dest = new Uint32Array(length);
-    }
+function resizeImageMask(src, bpc, w1, h1, w2, h2) {
+  var length = w2 * h2;
+  let dest;
 
-    var xRatio = w1 / w2;
-    var yRatio = h1 / h2;
-    var i,
-        j,
-        py,
-        newIndex = 0,
-        oldIndex;
-    var xScaled = new Uint16Array(w2);
-    var w1Scanline = w1;
-
-    for (i = 0; i < w2; i++) {
-      xScaled[i] = Math.floor(i * xRatio);
-    }
+  if (bpc <= 8) {
+    dest = new Uint8Array(length);
+  } else if (bpc <= 16) {
+    dest = new Uint16Array(length);
+  } else {
+    dest = new Uint32Array(length);
+  }
 
-    for (i = 0; i < h2; i++) {
-      py = Math.floor(i * yRatio) * w1Scanline;
+  var xRatio = w1 / w2;
+  var yRatio = h1 / h2;
+  var i,
+      j,
+      py,
+      newIndex = 0,
+      oldIndex;
+  var xScaled = new Uint16Array(w2);
+  var w1Scanline = w1;
+
+  for (i = 0; i < w2; i++) {
+    xScaled[i] = Math.floor(i * xRatio);
+  }
 
-      for (j = 0; j < w2; j++) {
-        oldIndex = py + xScaled[j];
-        dest[newIndex++] = src[oldIndex];
-      }
-    }
+  for (i = 0; i < h2; i++) {
+    py = Math.floor(i * yRatio) * w1Scanline;
 
-    return dest;
+    for (j = 0; j < w2; j++) {
+      oldIndex = py + xScaled[j];
+      dest[newIndex++] = src[oldIndex];
+    }
   }
 
-  function PDFImage({
+  return dest;
+}
+
+class PDFImage {
+  constructor({
     xref,
     res,
     image,
@@ -97,7 +97,8 @@ var PDFImage = function PDFImageClosure() {
     smask = null,
     mask = null,
     isMask = false,
-    pdfFunctionFactory
+    pdfFunctionFactory,
+    localColorSpaceCache
   }) {
     this.image = image;
     var dict = image.dict;
@@ -157,7 +158,7 @@ var PDFImage = function PDFImageClosure() {
     this.bpc = bitsPerComponent;
 
     if (!this.imageMask) {
-      var colorSpace = dict.get("ColorSpace", "CS");
+      let colorSpace = dict.getRaw("ColorSpace") || dict.getRaw("CS");
 
       if (!colorSpace) {
         (0, _util.info)("JPX images (which do not require color spaces)");
@@ -180,8 +181,13 @@ var PDFImage = function PDFImageClosure() {
         }
       }
 
-      const resources = isInline ? res : null;
-      this.colorSpace = _colorspace.ColorSpace.parse(colorSpace, xref, resources, pdfFunctionFactory);
+      this.colorSpace = _colorspace.ColorSpace.parse({
+        cs: colorSpace,
+        xref,
+        resources: isInline ? res : null,
+        pdfFunctionFactory,
+        localColorSpaceCache
+      });
       this.numComps = this.colorSpace.numComps;
     }
 
@@ -209,7 +215,8 @@ var PDFImage = function PDFImageClosure() {
         res,
         image: smask,
         isInline,
-        pdfFunctionFactory
+        pdfFunctionFactory,
+        localColorSpaceCache
       });
     } else if (mask) {
       if ((0, _primitives.isStream)(mask)) {
@@ -225,7 +232,8 @@ var PDFImage = function PDFImageClosure() {
             image: mask,
             isInline,
             isMask: true,
-            pdfFunctionFactory
+            pdfFunctionFactory,
+            localColorSpaceCache
           });
         }
       } else {
@@ -234,12 +242,13 @@ var PDFImage = function PDFImageClosure() {
     }
   }
 
-  PDFImage.buildImage = function ({
+  static async buildImage({
     xref,
     res,
     image,
     isInline = false,
-    pdfFunctionFactory
+    pdfFunctionFactory,
+    localColorSpaceCache
   }) {
     const imageData = image;
     let smaskData = null;
@@ -257,18 +266,19 @@ var PDFImage = function PDFImageClosure() {
       }
     }
 
-    return Promise.resolve(new PDFImage({
+    return new PDFImage({
       xref,
       res,
       image: imageData,
       isInline,
       smask: smaskData,
       mask: maskData,
-      pdfFunctionFactory
-    }));
-  };
+      pdfFunctionFactory,
+      localColorSpaceCache
+    });
+  }
 
-  PDFImage.createMask = function ({
+  static createMask({
     imgArray,
     width,
     height,
@@ -305,378 +315,375 @@ var PDFImage = function PDFImageClosure() {
       width,
       height
     };
-  };
-
-  PDFImage.prototype = {
-    get drawWidth() {
-      return Math.max(this.width, this.smask && this.smask.width || 0, this.mask && this.mask.width || 0);
-    },
-
-    get drawHeight() {
-      return Math.max(this.height, this.smask && this.smask.height || 0, this.mask && this.mask.height || 0);
-    },
-
-    decodeBuffer(buffer) {
-      var bpc = this.bpc;
-      var numComps = this.numComps;
-      var decodeAddends = this.decodeAddends;
-      var decodeCoefficients = this.decodeCoefficients;
-      var max = (1 << bpc) - 1;
-      var i, ii;
-
-      if (bpc === 1) {
-        for (i = 0, ii = buffer.length; i < ii; i++) {
-          buffer[i] = +!buffer[i];
-        }
+  }
 
-        return;
-      }
+  get drawWidth() {
+    return Math.max(this.width, this.smask && this.smask.width || 0, this.mask && this.mask.width || 0);
+  }
 
-      var index = 0;
+  get drawHeight() {
+    return Math.max(this.height, this.smask && this.smask.height || 0, this.mask && this.mask.height || 0);
+  }
 
-      for (i = 0, ii = this.width * this.height; i < ii; i++) {
-        for (var j = 0; j < numComps; j++) {
-          buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j], decodeCoefficients[j], max);
-          index++;
-        }
+  decodeBuffer(buffer) {
+    var bpc = this.bpc;
+    var numComps = this.numComps;
+    var decodeAddends = this.decodeAddends;
+    var decodeCoefficients = this.decodeCoefficients;
+    var max = (1 << bpc) - 1;
+    var i, ii;
+
+    if (bpc === 1) {
+      for (i = 0, ii = buffer.length; i < ii; i++) {
+        buffer[i] = +!buffer[i];
       }
-    },
 
-    getComponents(buffer) {
-      var bpc = this.bpc;
+      return;
+    }
 
-      if (bpc === 8) {
-        return buffer;
-      }
+    var index = 0;
 
-      var width = this.width;
-      var height = this.height;
-      var numComps = this.numComps;
-      var length = width * height * numComps;
-      var bufferPos = 0;
-      let output;
-
-      if (bpc <= 8) {
-        output = new Uint8Array(length);
-      } else if (bpc <= 16) {
-        output = new Uint16Array(length);
-      } else {
-        output = new Uint32Array(length);
+    for (i = 0, ii = this.width * this.height; i < ii; i++) {
+      for (var j = 0; j < numComps; j++) {
+        buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j], decodeCoefficients[j], max);
+        index++;
       }
+    }
+  }
 
-      var rowComps = width * numComps;
-      var max = (1 << bpc) - 1;
-      var i = 0,
-          ii,
-          buf;
-
-      if (bpc === 1) {
-        var mask, loop1End, loop2End;
-
-        for (var j = 0; j < height; j++) {
-          loop1End = i + (rowComps & ~7);
-          loop2End = i + rowComps;
-
-          while (i < loop1End) {
-            buf = buffer[bufferPos++];
-            output[i] = buf >> 7 & 1;
-            output[i + 1] = buf >> 6 & 1;
-            output[i + 2] = buf >> 5 & 1;
-            output[i + 3] = buf >> 4 & 1;
-            output[i + 4] = buf >> 3 & 1;
-            output[i + 5] = buf >> 2 & 1;
-            output[i + 6] = buf >> 1 & 1;
-            output[i + 7] = buf & 1;
-            i += 8;
-          }
+  getComponents(buffer) {
+    var bpc = this.bpc;
 
-          if (i < loop2End) {
-            buf = buffer[bufferPos++];
-            mask = 128;
+    if (bpc === 8) {
+      return buffer;
+    }
 
-            while (i < loop2End) {
-              output[i++] = +!!(buf & mask);
-              mask >>= 1;
-            }
-          }
+    var width = this.width;
+    var height = this.height;
+    var numComps = this.numComps;
+    var length = width * height * numComps;
+    var bufferPos = 0;
+    let output;
+
+    if (bpc <= 8) {
+      output = new Uint8Array(length);
+    } else if (bpc <= 16) {
+      output = new Uint16Array(length);
+    } else {
+      output = new Uint32Array(length);
+    }
+
+    var rowComps = width * numComps;
+    var max = (1 << bpc) - 1;
+    var i = 0,
+        ii,
+        buf;
+
+    if (bpc === 1) {
+      var mask, loop1End, loop2End;
+
+      for (var j = 0; j < height; j++) {
+        loop1End = i + (rowComps & ~7);
+        loop2End = i + rowComps;
+
+        while (i < loop1End) {
+          buf = buffer[bufferPos++];
+          output[i] = buf >> 7 & 1;
+          output[i + 1] = buf >> 6 & 1;
+          output[i + 2] = buf >> 5 & 1;
+          output[i + 3] = buf >> 4 & 1;
+          output[i + 4] = buf >> 3 & 1;
+          output[i + 5] = buf >> 2 & 1;
+          output[i + 6] = buf >> 1 & 1;
+          output[i + 7] = buf & 1;
+          i += 8;
         }
-      } else {
-        var bits = 0;
-        buf = 0;
 
-        for (i = 0, ii = length; i < ii; ++i) {
-          if (i % rowComps === 0) {
-            buf = 0;
-            bits = 0;
-          }
+        if (i < loop2End) {
+          buf = buffer[bufferPos++];
+          mask = 128;
 
-          while (bits < bpc) {
-            buf = buf << 8 | buffer[bufferPos++];
-            bits += 8;
+          while (i < loop2End) {
+            output[i++] = +!!(buf & mask);
+            mask >>= 1;
           }
+        }
+      }
+    } else {
+      var bits = 0;
+      buf = 0;
 
-          var remainingBits = bits - bpc;
-          let value = buf >> remainingBits;
+      for (i = 0, ii = length; i < ii; ++i) {
+        if (i % rowComps === 0) {
+          buf = 0;
+          bits = 0;
+        }
 
-          if (value < 0) {
-            value = 0;
-          } else if (value > max) {
-            value = max;
-          }
+        while (bits < bpc) {
+          buf = buf << 8 | buffer[bufferPos++];
+          bits += 8;
+        }
+
+        var remainingBits = bits - bpc;
+        let value = buf >> remainingBits;
 
-          output[i] = value;
-          buf = buf & (1 << remainingBits) - 1;
-          bits = remainingBits;
+        if (value < 0) {
+          value = 0;
+        } else if (value > max) {
+          value = max;
         }
+
+        output[i] = value;
+        buf = buf & (1 << remainingBits) - 1;
+        bits = remainingBits;
       }
+    }
 
-      return output;
-    },
+    return output;
+  }
 
-    fillOpacity(rgbaBuf, width, height, actualHeight, image) {
-      var smask = this.smask;
-      var mask = this.mask;
-      var alphaBuf, sw, sh, i, ii, j;
+  fillOpacity(rgbaBuf, width, height, actualHeight, image) {
+    var smask = this.smask;
+    var mask = this.mask;
+    var alphaBuf, sw, sh, i, ii, j;
 
-      if (smask) {
-        sw = smask.width;
-        sh = smask.height;
+    if (smask) {
+      sw = smask.width;
+      sh = smask.height;
+      alphaBuf = new Uint8ClampedArray(sw * sh);
+      smask.fillGrayBuffer(alphaBuf);
+
+      if (sw !== width || sh !== height) {
+        alphaBuf = resizeImageMask(alphaBuf, smask.bpc, sw, sh, width, height);
+      }
+    } else if (mask) {
+      if (mask instanceof PDFImage) {
+        sw = mask.width;
+        sh = mask.height;
         alphaBuf = new Uint8ClampedArray(sw * sh);
-        smask.fillGrayBuffer(alphaBuf);
+        mask.numComps = 1;
+        mask.fillGrayBuffer(alphaBuf);
+
+        for (i = 0, ii = sw * sh; i < ii; ++i) {
+          alphaBuf[i] = 255 - alphaBuf[i];
+        }
 
         if (sw !== width || sh !== height) {
-          alphaBuf = resizeImageMask(alphaBuf, smask.bpc, sw, sh, width, height);
+          alphaBuf = resizeImageMask(alphaBuf, mask.bpc, sw, sh, width, height);
         }
-      } else if (mask) {
-        if (mask instanceof PDFImage) {
-          sw = mask.width;
-          sh = mask.height;
-          alphaBuf = new Uint8ClampedArray(sw * sh);
-          mask.numComps = 1;
-          mask.fillGrayBuffer(alphaBuf);
-
-          for (i = 0, ii = sw * sh; i < ii; ++i) {
-            alphaBuf[i] = 255 - alphaBuf[i];
-          }
+      } else if (Array.isArray(mask)) {
+        alphaBuf = new Uint8ClampedArray(width * height);
+        var numComps = this.numComps;
 
-          if (sw !== width || sh !== height) {
-            alphaBuf = resizeImageMask(alphaBuf, mask.bpc, sw, sh, width, height);
-          }
-        } else if (Array.isArray(mask)) {
-          alphaBuf = new Uint8ClampedArray(width * height);
-          var numComps = this.numComps;
-
-          for (i = 0, ii = width * height; i < ii; ++i) {
-            var opacity = 0;
-            var imageOffset = i * numComps;
-
-            for (j = 0; j < numComps; ++j) {
-              var color = image[imageOffset + j];
-              var maskOffset = j * 2;
-
-              if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
-                opacity = 255;
-                break;
-              }
-            }
+        for (i = 0, ii = width * height; i < ii; ++i) {
+          var opacity = 0;
+          var imageOffset = i * numComps;
+
+          for (j = 0; j < numComps; ++j) {
+            var color = image[imageOffset + j];
+            var maskOffset = j * 2;
 
-            alphaBuf[i] = opacity;
+            if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
+              opacity = 255;
+              break;
+            }
           }
-        } else {
-          throw new _util.FormatError("Unknown mask format.");
-        }
-      }
 
-      if (alphaBuf) {
-        for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
-          rgbaBuf[j] = alphaBuf[i];
+          alphaBuf[i] = opacity;
         }
       } else {
-        for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
-          rgbaBuf[j] = 255;
-        }
+        throw new _util.FormatError("Unknown mask format.");
       }
-    },
-
-    undoPreblend(buffer, width, height) {
-      var matte = this.smask && this.smask.matte;
+    }
 
-      if (!matte) {
-        return;
+    if (alphaBuf) {
+      for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
+        rgbaBuf[j] = alphaBuf[i];
+      }
+    } else {
+      for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
+        rgbaBuf[j] = 255;
       }
+    }
+  }
 
-      var matteRgb = this.colorSpace.getRgb(matte, 0);
-      var matteR = matteRgb[0];
-      var matteG = matteRgb[1];
-      var matteB = matteRgb[2];
-      var length = width * height * 4;
+  undoPreblend(buffer, width, height) {
+    var matte = this.smask && this.smask.matte;
 
-      for (var i = 0; i < length; i += 4) {
-        var alpha = buffer[i + 3];
+    if (!matte) {
+      return;
+    }
 
-        if (alpha === 0) {
-          buffer[i] = 255;
-          buffer[i + 1] = 255;
-          buffer[i + 2] = 255;
-          continue;
-        }
+    var matteRgb = this.colorSpace.getRgb(matte, 0);
+    var matteR = matteRgb[0];
+    var matteG = matteRgb[1];
+    var matteB = matteRgb[2];
+    var length = width * height * 4;
 
-        var k = 255 / alpha;
-        buffer[i] = (buffer[i] - matteR) * k + matteR;
-        buffer[i + 1] = (buffer[i + 1] - matteG) * k + matteG;
-        buffer[i + 2] = (buffer[i + 2] - matteB) * k + matteB;
-      }
-    },
-
-    createImageData(forceRGBA = false) {
-      var drawWidth = this.drawWidth;
-      var drawHeight = this.drawHeight;
-      var imgData = {
-        width: drawWidth,
-        height: drawHeight,
-        kind: 0,
-        data: null
-      };
-      var numComps = this.numComps;
-      var originalWidth = this.width;
-      var originalHeight = this.height;
-      var bpc = this.bpc;
-      var rowBytes = originalWidth * numComps * bpc + 7 >> 3;
-      var imgArray;
-
-      if (!forceRGBA) {
-        var kind;
-
-        if (this.colorSpace.name === "DeviceGray" && bpc === 1) {
-          kind = _util.ImageKind.GRAYSCALE_1BPP;
-        } else if (this.colorSpace.name === "DeviceRGB" && bpc === 8 && !this.needsDecode) {
-          kind = _util.ImageKind.RGB_24BPP;
-        }
+    for (var i = 0; i < length; i += 4) {
+      var alpha = buffer[i + 3];
 
-        if (kind && !this.smask && !this.mask && drawWidth === originalWidth && drawHeight === originalHeight) {
-          imgData.kind = kind;
-          imgArray = this.getImageBytes(originalHeight * rowBytes);
+      if (alpha === 0) {
+        buffer[i] = 255;
+        buffer[i + 1] = 255;
+        buffer[i + 2] = 255;
+        continue;
+      }
 
-          if (this.image instanceof _stream.DecodeStream) {
-            imgData.data = imgArray;
-          } else {
-            var newArray = new Uint8ClampedArray(imgArray.length);
-            newArray.set(imgArray);
-            imgData.data = newArray;
-          }
+      var k = 255 / alpha;
+      buffer[i] = (buffer[i] - matteR) * k + matteR;
+      buffer[i + 1] = (buffer[i + 1] - matteG) * k + matteG;
+      buffer[i + 2] = (buffer[i + 2] - matteB) * k + matteB;
+    }
+  }
 
-          if (this.needsDecode) {
-            (0, _util.assert)(kind === _util.ImageKind.GRAYSCALE_1BPP, "PDFImage.createImageData: The image must be grayscale.");
-            var buffer = imgData.data;
+  createImageData(forceRGBA = false) {
+    var drawWidth = this.drawWidth;
+    var drawHeight = this.drawHeight;
+    var imgData = {
+      width: drawWidth,
+      height: drawHeight,
+      kind: 0,
+      data: null
+    };
+    var numComps = this.numComps;
+    var originalWidth = this.width;
+    var originalHeight = this.height;
+    var bpc = this.bpc;
+    var rowBytes = originalWidth * numComps * bpc + 7 >> 3;
+    var imgArray;
+
+    if (!forceRGBA) {
+      var kind;
+
+      if (this.colorSpace.name === "DeviceGray" && bpc === 1) {
+        kind = _util.ImageKind.GRAYSCALE_1BPP;
+      } else if (this.colorSpace.name === "DeviceRGB" && bpc === 8 && !this.needsDecode) {
+        kind = _util.ImageKind.RGB_24BPP;
+      }
 
-            for (var i = 0, ii = buffer.length; i < ii; i++) {
-              buffer[i] ^= 0xff;
-            }
-          }
+      if (kind && !this.smask && !this.mask && drawWidth === originalWidth && drawHeight === originalHeight) {
+        imgData.kind = kind;
+        imgArray = this.getImageBytes(originalHeight * rowBytes);
 
-          return imgData;
+        if (this.image instanceof _stream.DecodeStream) {
+          imgData.data = imgArray;
+        } else {
+          var newArray = new Uint8ClampedArray(imgArray.length);
+          newArray.set(imgArray);
+          imgData.data = newArray;
         }
 
-        if (this.image instanceof _jpeg_stream.JpegStream && !this.smask && !this.mask) {
-          let imageLength = originalHeight * rowBytes;
-
-          switch (this.colorSpace.name) {
-            case "DeviceGray":
-              imageLength *= 3;
+        if (this.needsDecode) {
+          (0, _util.assert)(kind === _util.ImageKind.GRAYSCALE_1BPP, "PDFImage.createImageData: The image must be grayscale.");
+          var buffer = imgData.data;
 
-            case "DeviceRGB":
-            case "DeviceCMYK":
-              imgData.kind = _util.ImageKind.RGB_24BPP;
-              imgData.data = this.getImageBytes(imageLength, drawWidth, drawHeight, true);
-              return imgData;
+          for (var i = 0, ii = buffer.length; i < ii; i++) {
+            buffer[i] ^= 0xff;
           }
         }
+
+        return imgData;
       }
 
-      imgArray = this.getImageBytes(originalHeight * rowBytes);
-      var actualHeight = 0 | imgArray.length / rowBytes * drawHeight / originalHeight;
-      var comps = this.getComponents(imgArray);
-      var alpha01, maybeUndoPreblend;
+      if (this.image instanceof _jpeg_stream.JpegStream && !this.smask && !this.mask) {
+        let imageLength = originalHeight * rowBytes;
 
-      if (!forceRGBA && !this.smask && !this.mask) {
-        imgData.kind = _util.ImageKind.RGB_24BPP;
-        imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 3);
-        alpha01 = 0;
-        maybeUndoPreblend = false;
-      } else {
-        imgData.kind = _util.ImageKind.RGBA_32BPP;
-        imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 4);
-        alpha01 = 1;
-        maybeUndoPreblend = true;
-        this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight, comps);
-      }
+        switch (this.colorSpace.name) {
+          case "DeviceGray":
+            imageLength *= 3;
 
-      if (this.needsDecode) {
-        this.decodeBuffer(comps);
+          case "DeviceRGB":
+          case "DeviceCMYK":
+            imgData.kind = _util.ImageKind.RGB_24BPP;
+            imgData.data = this.getImageBytes(imageLength, drawWidth, drawHeight, true);
+            return imgData;
+        }
       }
+    }
 
-      this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight, drawWidth, drawHeight, actualHeight, bpc, comps, alpha01);
+    imgArray = this.getImageBytes(originalHeight * rowBytes);
+    var actualHeight = 0 | imgArray.length / rowBytes * drawHeight / originalHeight;
+    var comps = this.getComponents(imgArray);
+    var alpha01, maybeUndoPreblend;
 
-      if (maybeUndoPreblend) {
-        this.undoPreblend(imgData.data, drawWidth, actualHeight);
-      }
+    if (!forceRGBA && !this.smask && !this.mask) {
+      imgData.kind = _util.ImageKind.RGB_24BPP;
+      imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 3);
+      alpha01 = 0;
+      maybeUndoPreblend = false;
+    } else {
+      imgData.kind = _util.ImageKind.RGBA_32BPP;
+      imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 4);
+      alpha01 = 1;
+      maybeUndoPreblend = true;
+      this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight, comps);
+    }
 
-      return imgData;
-    },
+    if (this.needsDecode) {
+      this.decodeBuffer(comps);
+    }
 
-    fillGrayBuffer(buffer) {
-      var numComps = this.numComps;
+    this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight, drawWidth, drawHeight, actualHeight, bpc, comps, alpha01);
 
-      if (numComps !== 1) {
-        throw new _util.FormatError(`Reading gray scale from a color image: ${numComps}`);
-      }
+    if (maybeUndoPreblend) {
+      this.undoPreblend(imgData.data, drawWidth, actualHeight);
+    }
 
-      var width = this.width;
-      var height = this.height;
-      var bpc = this.bpc;
-      var rowBytes = width * numComps * bpc + 7 >> 3;
-      var imgArray = this.getImageBytes(height * rowBytes);
-      var comps = this.getComponents(imgArray);
-      var i, length;
+    return imgData;
+  }
 
-      if (bpc === 1) {
-        length = width * height;
+  fillGrayBuffer(buffer) {
+    var numComps = this.numComps;
 
-        if (this.needsDecode) {
-          for (i = 0; i < length; ++i) {
-            buffer[i] = comps[i] - 1 & 255;
-          }
-        } else {
-          for (i = 0; i < length; ++i) {
-            buffer[i] = -comps[i] & 255;
-          }
-        }
+    if (numComps !== 1) {
+      throw new _util.FormatError(`Reading gray scale from a color image: ${numComps}`);
+    }
 
-        return;
-      }
+    var width = this.width;
+    var height = this.height;
+    var bpc = this.bpc;
+    var rowBytes = width * numComps * bpc + 7 >> 3;
+    var imgArray = this.getImageBytes(height * rowBytes);
+    var comps = this.getComponents(imgArray);
+    var i, length;
+
+    if (bpc === 1) {
+      length = width * height;
 
       if (this.needsDecode) {
-        this.decodeBuffer(comps);
+        for (i = 0; i < length; ++i) {
+          buffer[i] = comps[i] - 1 & 255;
+        }
+      } else {
+        for (i = 0; i < length; ++i) {
+          buffer[i] = -comps[i] & 255;
+        }
       }
 
-      length = width * height;
-      var scale = 255 / ((1 << bpc) - 1);
+      return;
+    }
 
-      for (i = 0; i < length; ++i) {
-        buffer[i] = scale * comps[i];
-      }
-    },
-
-    getImageBytes(length, drawWidth, drawHeight, forceRGB = false) {
-      this.image.reset();
-      this.image.drawWidth = drawWidth || this.width;
-      this.image.drawHeight = drawHeight || this.height;
-      this.image.forceRGB = !!forceRGB;
-      return this.image.getBytes(length, true);
+    if (this.needsDecode) {
+      this.decodeBuffer(comps);
+    }
+
+    length = width * height;
+    var scale = 255 / ((1 << bpc) - 1);
+
+    for (i = 0; i < length; ++i) {
+      buffer[i] = scale * comps[i];
     }
+  }
+
+  getImageBytes(length, drawWidth, drawHeight, forceRGB = false) {
+    this.image.reset();
+    this.image.drawWidth = drawWidth || this.width;
+    this.image.drawHeight = drawHeight || this.height;
+    this.image.forceRGB = !!forceRGB;
+    return this.image.getBytes(length, true);
+  }
 
-  };
-  return PDFImage;
-}();
+}
 
 exports.PDFImage = PDFImage;

+ 109 - 8
lib/core/image_utils.js

@@ -24,16 +24,23 @@
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.GlobalImageCache = exports.LocalImageCache = void 0;
+exports.GlobalImageCache = exports.LocalGStateCache = exports.LocalFunctionCache = exports.LocalColorSpaceCache = exports.LocalImageCache = void 0;
 
 var _util = require("../shared/util.js");
 
 var _primitives = require("./primitives.js");
 
-class LocalImageCache {
-  constructor() {
-    this._nameRefMap = new Map();
-    this._imageMap = new Map();
+class BaseLocalCache {
+  constructor(options) {
+    if (this.constructor === BaseLocalCache) {
+      (0, _util.unreachable)("Cannot initialize BaseLocalCache.");
+    }
+
+    if (!options || !options.onlyRefs) {
+      this._nameRefMap = new Map();
+      this._imageMap = new Map();
+    }
+
     this._imageCache = new _primitives.RefSetCache();
   }
 
@@ -51,6 +58,13 @@ class LocalImageCache {
     return this._imageCache.get(ref) || null;
   }
 
+  set(name, ref, data) {
+    (0, _util.unreachable)("Abstract method `set` called.");
+  }
+
+}
+
+class LocalImageCache extends BaseLocalCache {
   set(name, ref = null, data) {
     if (!name) {
       throw new Error('LocalImageCache.set - expected "name" argument.');
@@ -79,6 +93,93 @@ class LocalImageCache {
 
 exports.LocalImageCache = LocalImageCache;
 
+class LocalColorSpaceCache extends BaseLocalCache {
+  set(name = null, ref = null, data) {
+    if (!name && !ref) {
+      throw new Error('LocalColorSpaceCache.set - expected "name" and/or "ref" argument.');
+    }
+
+    if (ref) {
+      if (this._imageCache.has(ref)) {
+        return;
+      }
+
+      if (name) {
+        this._nameRefMap.set(name, ref);
+      }
+
+      this._imageCache.put(ref, data);
+
+      return;
+    }
+
+    if (this._imageMap.has(name)) {
+      return;
+    }
+
+    this._imageMap.set(name, data);
+  }
+
+}
+
+exports.LocalColorSpaceCache = LocalColorSpaceCache;
+
+class LocalFunctionCache extends BaseLocalCache {
+  constructor(options) {
+    super({
+      onlyRefs: true
+    });
+  }
+
+  getByName(name) {
+    (0, _util.unreachable)("Should not call `getByName` method.");
+  }
+
+  set(name = null, ref, data) {
+    if (!ref) {
+      throw new Error('LocalFunctionCache.set - expected "ref" argument.');
+    }
+
+    if (this._imageCache.has(ref)) {
+      return;
+    }
+
+    this._imageCache.put(ref, data);
+  }
+
+}
+
+exports.LocalFunctionCache = LocalFunctionCache;
+
+class LocalGStateCache extends BaseLocalCache {
+  set(name, ref = null, data) {
+    if (!name) {
+      throw new Error('LocalGStateCache.set - expected "name" argument.');
+    }
+
+    if (ref) {
+      if (this._imageCache.has(ref)) {
+        return;
+      }
+
+      this._nameRefMap.set(name, ref);
+
+      this._imageCache.put(ref, data);
+
+      return;
+    }
+
+    if (this._imageMap.has(name)) {
+      return;
+    }
+
+    this._imageMap.set(name, data);
+  }
+
+}
+
+exports.LocalGStateCache = LocalGStateCache;
+
 class GlobalImageCache {
   static get NUM_PAGES_THRESHOLD() {
     return (0, _util.shadow)(this, "NUM_PAGES_THRESHOLD", 2);
@@ -122,12 +223,12 @@ class GlobalImageCache {
   }
 
   getData(ref, pageIndex) {
-    if (!this._refCache.has(ref)) {
+    const pageIndexSet = this._refCache.get(ref);
+
+    if (!pageIndexSet) {
       return null;
     }
 
-    const pageIndexSet = this._refCache.get(ref);
-
     if (pageIndexSet.size < GlobalImageCache.NUM_PAGES_THRESHOLD) {
       return null;
     }

+ 8 - 3
lib/core/jpg.js

@@ -154,9 +154,9 @@ var JpegImage = function JpegImageClosure() {
             }
           } else if (nextByte === 0xd9) {
             if (parseDNLMarker) {
-              const maybeScanLines = blockRow * 8;
+              const maybeScanLines = blockRow * (frame.precision === 8 ? 8 : 0);
 
-              if (maybeScanLines > 0 && maybeScanLines < frame.scanLines / 10) {
+              if (maybeScanLines > 0 && Math.round(frame.scanLines / maybeScanLines) >= 10) {
                 throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, " + "possibly caused by incorrect `scanLines` parameter", maybeScanLines);
               }
             }
@@ -976,8 +976,10 @@ var JpegImage = function JpegImageClosure() {
                 component;
 
             for (i = 0; i < selectorsCount; i++) {
-              var componentIndex = frame.componentIds[data[offset++]];
+              const index = data[offset++];
+              var componentIndex = frame.componentIds[index];
               component = frame.components[componentIndex];
+              component.index = index;
               var tableSpec = data[offset++];
               component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4];
               component.huffmanTableAC = huffmanTablesAC[tableSpec & 15];
@@ -1054,6 +1056,7 @@ var JpegImage = function JpegImageClosure() {
         }
 
         this.components.push({
+          index: component.index,
           output: buildComponentData(frame, component),
           scaleX: component.h / frame.maxH,
           scaleY: component.v / frame.maxV,
@@ -1134,6 +1137,8 @@ var JpegImage = function JpegImageClosure() {
       if (this.numComponents === 3) {
         if (this._colorTransform === 0) {
           return false;
+        } else if (this.components[0].index === 0x52 && this.components[1].index === 0x47 && this.components[2].index === 0x42) {
+          return false;
         }
 
         return true;

+ 255 - 34
lib/core/obj.js

@@ -48,9 +48,9 @@ class Catalog {
   constructor(pdfManager, xref) {
     this.pdfManager = pdfManager;
     this.xref = xref;
-    this.catDict = xref.getCatalogObj();
+    this._catDict = xref.getCatalogObj();
 
-    if (!(0, _primitives.isDict)(this.catDict)) {
+    if (!(0, _primitives.isDict)(this._catDict)) {
       throw new _util.FormatError("Catalog object is not a dictionary.");
     }
 
@@ -60,8 +60,58 @@ class Catalog {
     this.pageKidsCountCache = new _primitives.RefSetCache();
   }
 
+  get version() {
+    const version = this._catDict.get("Version");
+
+    if (!(0, _primitives.isName)(version)) {
+      return (0, _util.shadow)(this, "version", null);
+    }
+
+    return (0, _util.shadow)(this, "version", version.name);
+  }
+
+  get collection() {
+    let collection = null;
+
+    try {
+      const obj = this._catDict.get("Collection");
+
+      if ((0, _primitives.isDict)(obj) && obj.size > 0) {
+        collection = obj;
+      }
+    } catch (ex) {
+      if (ex instanceof _core_utils.MissingDataException) {
+        throw ex;
+      }
+
+      (0, _util.info)("Cannot fetch Collection entry; assuming no collection is present.");
+    }
+
+    return (0, _util.shadow)(this, "collection", collection);
+  }
+
+  get acroForm() {
+    let acroForm = null;
+
+    try {
+      const obj = this._catDict.get("AcroForm");
+
+      if ((0, _primitives.isDict)(obj) && obj.size > 0) {
+        acroForm = obj;
+      }
+    } catch (ex) {
+      if (ex instanceof _core_utils.MissingDataException) {
+        throw ex;
+      }
+
+      (0, _util.info)("Cannot fetch AcroForm entry; assuming no forms are present.");
+    }
+
+    return (0, _util.shadow)(this, "acroForm", acroForm);
+  }
+
   get metadata() {
-    const streamRef = this.catDict.getRaw("Metadata");
+    const streamRef = this._catDict.getRaw("Metadata");
 
     if (!(0, _primitives.isRef)(streamRef)) {
       return (0, _util.shadow)(this, "metadata", null);
@@ -92,7 +142,7 @@ class Catalog {
   }
 
   get toplevelPagesDict() {
-    const pagesObj = this.catDict.get("Pages");
+    const pagesObj = this._catDict.get("Pages");
 
     if (!(0, _primitives.isDict)(pagesObj)) {
       throw new _util.FormatError("Invalid top-level pages dictionary.");
@@ -118,7 +168,7 @@ class Catalog {
   }
 
   _readDocumentOutline() {
-    let obj = this.catDict.get("Outlines");
+    let obj = this._catDict.get("Outlines");
 
     if (!(0, _primitives.isDict)(obj)) {
       return null;
@@ -253,6 +303,166 @@ class Catalog {
     return permissions;
   }
 
+  get optionalContentConfig() {
+    let config = null;
+
+    try {
+      const properties = this._catDict.get("OCProperties");
+
+      if (!properties) {
+        return (0, _util.shadow)(this, "optionalContentConfig", null);
+      }
+
+      const defaultConfig = properties.get("D");
+
+      if (!defaultConfig) {
+        return (0, _util.shadow)(this, "optionalContentConfig", null);
+      }
+
+      const groupsData = properties.get("OCGs");
+
+      if (!Array.isArray(groupsData)) {
+        return (0, _util.shadow)(this, "optionalContentConfig", null);
+      }
+
+      const groups = [];
+      const groupRefs = [];
+
+      for (const groupRef of groupsData) {
+        if (!(0, _primitives.isRef)(groupRef)) {
+          continue;
+        }
+
+        groupRefs.push(groupRef);
+        const group = this.xref.fetchIfRef(groupRef);
+        groups.push({
+          id: groupRef.toString(),
+          name: (0, _util.isString)(group.get("Name")) ? (0, _util.stringToPDFString)(group.get("Name")) : null,
+          intent: (0, _util.isString)(group.get("Intent")) ? (0, _util.stringToPDFString)(group.get("Intent")) : null
+        });
+      }
+
+      config = this._readOptionalContentConfig(defaultConfig, groupRefs);
+      config.groups = groups;
+    } catch (ex) {
+      if (ex instanceof _core_utils.MissingDataException) {
+        throw ex;
+      }
+
+      (0, _util.warn)(`Unable to read optional content config: ${ex}`);
+    }
+
+    return (0, _util.shadow)(this, "optionalContentConfig", config);
+  }
+
+  _readOptionalContentConfig(config, contentGroupRefs) {
+    function parseOnOff(refs) {
+      const onParsed = [];
+
+      if (Array.isArray(refs)) {
+        for (const value of refs) {
+          if (!(0, _primitives.isRef)(value)) {
+            continue;
+          }
+
+          if (contentGroupRefs.includes(value)) {
+            onParsed.push(value.toString());
+          }
+        }
+      }
+
+      return onParsed;
+    }
+
+    function parseOrder(refs, nestedLevels = 0) {
+      if (!Array.isArray(refs)) {
+        return null;
+      }
+
+      const order = [];
+
+      for (const value of refs) {
+        if ((0, _primitives.isRef)(value) && contentGroupRefs.includes(value)) {
+          parsedOrderRefs.put(value);
+          order.push(value.toString());
+          continue;
+        }
+
+        const nestedOrder = parseNestedOrder(value, nestedLevels);
+
+        if (nestedOrder) {
+          order.push(nestedOrder);
+        }
+      }
+
+      if (nestedLevels > 0) {
+        return order;
+      }
+
+      const hiddenGroups = [];
+
+      for (const groupRef of contentGroupRefs) {
+        if (parsedOrderRefs.has(groupRef)) {
+          continue;
+        }
+
+        hiddenGroups.push(groupRef.toString());
+      }
+
+      if (hiddenGroups.length) {
+        order.push({
+          name: null,
+          order: hiddenGroups
+        });
+      }
+
+      return order;
+    }
+
+    function parseNestedOrder(ref, nestedLevels) {
+      if (++nestedLevels > MAX_NESTED_LEVELS) {
+        (0, _util.warn)("parseNestedOrder - reached MAX_NESTED_LEVELS.");
+        return null;
+      }
+
+      const value = xref.fetchIfRef(ref);
+
+      if (!Array.isArray(value)) {
+        return null;
+      }
+
+      const nestedName = xref.fetchIfRef(value[0]);
+
+      if (typeof nestedName !== "string") {
+        return null;
+      }
+
+      const nestedOrder = parseOrder(value.slice(1), nestedLevels);
+
+      if (!nestedOrder || !nestedOrder.length) {
+        return null;
+      }
+
+      return {
+        name: (0, _util.stringToPDFString)(nestedName),
+        order: nestedOrder
+      };
+    }
+
+    const xref = this.xref,
+          parsedOrderRefs = new _primitives.RefSet(),
+          MAX_NESTED_LEVELS = 10;
+    return {
+      name: (0, _util.isString)(config.get("Name")) ? (0, _util.stringToPDFString)(config.get("Name")) : null,
+      creator: (0, _util.isString)(config.get("Creator")) ? (0, _util.stringToPDFString)(config.get("Creator")) : null,
+      baseState: (0, _primitives.isName)(config.get("BaseState")) ? config.get("BaseState").name : null,
+      on: parseOnOff(config.get("ON")),
+      off: parseOnOff(config.get("OFF")),
+      order: parseOrder(config.get("Order")),
+      groups: null
+    };
+  }
+
   get numPages() {
     const obj = this.toplevelPagesDict.get("Count");
 
@@ -295,12 +505,12 @@ class Catalog {
   }
 
   _readDests() {
-    const obj = this.catDict.get("Names");
+    const obj = this._catDict.get("Names");
 
     if (obj && obj.has("Dests")) {
       return new NameTree(obj.getRaw("Dests"), this.xref);
-    } else if (this.catDict.has("Dests")) {
-      return this.catDict.get("Dests");
+    } else if (this._catDict.has("Dests")) {
+      return this._catDict.get("Dests");
     }
 
     return undefined;
@@ -323,7 +533,7 @@ class Catalog {
   }
 
   _readPageLabels() {
-    const obj = this.catDict.getRaw("PageLabels");
+    const obj = this._catDict.getRaw("PageLabels");
 
     if (!obj) {
       return null;
@@ -429,7 +639,8 @@ class Catalog {
   }
 
   get pageLayout() {
-    const obj = this.catDict.get("PageLayout");
+    const obj = this._catDict.get("PageLayout");
+
     let pageLayout = "";
 
     if ((0, _primitives.isName)(obj)) {
@@ -448,7 +659,8 @@ class Catalog {
   }
 
   get pageMode() {
-    const obj = this.catDict.get("PageMode");
+    const obj = this._catDict.get("PageMode");
+
     let pageMode = "UseNone";
 
     if ((0, _primitives.isName)(obj)) {
@@ -486,7 +698,9 @@ class Catalog {
       PrintPageRange: Array.isArray,
       NumCopies: Number.isInteger
     };
-    const obj = this.catDict.get("ViewerPreferences");
+
+    const obj = this._catDict.get("ViewerPreferences");
+
     let prefs = null;
 
     if ((0, _primitives.isDict)(obj)) {
@@ -627,7 +841,8 @@ class Catalog {
   }
 
   get openAction() {
-    const obj = this.catDict.get("OpenAction");
+    const obj = this._catDict.get("OpenAction");
+
     let openAction = null;
 
     if ((0, _primitives.isDict)(obj)) {
@@ -668,7 +883,8 @@ class Catalog {
   }
 
   get attachments() {
-    const obj = this.catDict.get("Names");
+    const obj = this._catDict.get("Names");
+
     let attachments = null;
 
     if (obj && obj.has("EmbeddedFiles")) {
@@ -690,7 +906,8 @@ class Catalog {
   }
 
   get javaScript() {
-    const obj = this.catDict.get("Names");
+    const obj = this._catDict.get("Names");
+
     let javaScript = null;
 
     function appendIfJavaScriptDict(jsDict) {
@@ -728,7 +945,7 @@ class Catalog {
       }
     }
 
-    const openAction = this.catDict.get("OpenAction");
+    const openAction = this._catDict.get("OpenAction");
 
     if ((0, _primitives.isDict)(openAction) && (0, _primitives.isName)(openAction.get("S"), "JavaScript")) {
       appendIfJavaScriptDict(openAction);
@@ -774,7 +991,7 @@ class Catalog {
 
   getPageDict(pageIndex) {
     const capability = (0, _util.createPromiseCapability)();
-    const nodesToVisit = [this.catDict.getRaw("Pages")];
+    const nodesToVisit = [this._catDict.getRaw("Pages")];
     const visitedNodes = new _primitives.RefSet();
     const xref = this.xref,
           pageKidsCountCache = this.pageKidsCountCache;
@@ -1139,9 +1356,20 @@ var XRef = function XRefClosure() {
       streamTypes: Object.create(null),
       fontTypes: Object.create(null)
     };
+    this._newRefNum = null;
   }
 
   XRef.prototype = {
+    getNewRef: function XRef_getNewRef() {
+      if (this._newRefNum === null) {
+        this._newRefNum = this.entries.length;
+      }
+
+      return _primitives.Ref.get(this._newRefNum++, 0);
+    },
+    resetNewRef: function XRef_resetNewRef() {
+      this._newRefNum = null;
+    },
     setStartXRef: function XRef_setStartXRef(startXRef) {
       this.startXRefQueue = [startXRef];
     },
@@ -2143,24 +2371,17 @@ const ObjectLoader = function () {
   }
 
   function addChildren(node, nodesToVisit) {
-    if (node instanceof _primitives.Dict || (0, _primitives.isStream)(node)) {
-      const dict = node instanceof _primitives.Dict ? node : node.dict;
-      const dictKeys = dict.getKeys();
-
-      for (let i = 0, ii = dictKeys.length; i < ii; i++) {
-        const rawValue = dict.getRaw(dictKeys[i]);
-
-        if (mayHaveChildren(rawValue)) {
-          nodesToVisit.push(rawValue);
-        }
-      }
-    } else if (Array.isArray(node)) {
-      for (let i = 0, ii = node.length; i < ii; i++) {
-        const value = node[i];
+    if (node instanceof _primitives.Dict) {
+      node = node.getRawValues();
+    } else if ((0, _primitives.isStream)(node)) {
+      node = node.dict.getRawValues();
+    } else if (!Array.isArray(node)) {
+      return;
+    }
 
-        if (mayHaveChildren(value)) {
-          nodesToVisit.push(value);
-        }
+    for (const rawValue of node) {
+      if (mayHaveChildren(rawValue)) {
+        nodesToVisit.push(rawValue);
       }
     }
   }

+ 17 - 15
lib/core/operator_list.js

@@ -212,18 +212,20 @@ var QueueOptimizer = function QueueOptimizerClosure() {
     var isSameImage = false;
     var iTransform, transformArgs;
     var firstPIMXOArg0 = argsArray[iFirstPIMXO][0];
+    const firstTransformArg0 = argsArray[iFirstTransform][0],
+          firstTransformArg1 = argsArray[iFirstTransform][1],
+          firstTransformArg2 = argsArray[iFirstTransform][2],
+          firstTransformArg3 = argsArray[iFirstTransform][3];
 
-    if (argsArray[iFirstTransform][1] === 0 && argsArray[iFirstTransform][2] === 0) {
+    if (firstTransformArg1 === firstTransformArg2) {
       isSameImage = true;
-      var firstTransformArg0 = argsArray[iFirstTransform][0];
-      var firstTransformArg3 = argsArray[iFirstTransform][3];
       iTransform = iFirstTransform + 4;
       var iPIMXO = iFirstPIMXO + 4;
 
       for (q = 1; q < count; q++, iTransform += 4, iPIMXO += 4) {
         transformArgs = argsArray[iTransform];
 
-        if (argsArray[iPIMXO][0] !== firstPIMXOArg0 || transformArgs[0] !== firstTransformArg0 || transformArgs[1] !== 0 || transformArgs[2] !== 0 || transformArgs[3] !== firstTransformArg3) {
+        if (argsArray[iPIMXO][0] !== firstPIMXOArg0 || transformArgs[0] !== firstTransformArg0 || transformArgs[1] !== firstTransformArg1 || transformArgs[2] !== firstTransformArg2 || transformArgs[3] !== firstTransformArg3) {
           if (q < MIN_IMAGES_IN_MASKS_BLOCK) {
             isSameImage = false;
           } else {
@@ -247,7 +249,7 @@ var QueueOptimizer = function QueueOptimizerClosure() {
       }
 
       fnArray.splice(iFirstSave, count * 4, _util.OPS.paintImageMaskXObjectRepeat);
-      argsArray.splice(iFirstSave, count * 4, [firstPIMXOArg0, firstTransformArg0, firstTransformArg3, positions]);
+      argsArray.splice(iFirstSave, count * 4, [firstPIMXOArg0, firstTransformArg0, firstTransformArg1, firstTransformArg2, firstTransformArg3, positions]);
     } else {
       count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK);
       var images = [];
@@ -543,7 +545,7 @@ var OperatorList = function OperatorListClosure() {
   var CHUNK_SIZE = 1000;
   var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5;
 
-  function OperatorList(intent, streamSink, pageIndex) {
+  function OperatorList(intent, streamSink) {
     this._streamSink = streamSink;
     this.fnArray = [];
     this.argsArray = [];
@@ -554,10 +556,8 @@ var OperatorList = function OperatorListClosure() {
       this.optimizer = new NullOptimizer(this);
     }
 
-    this.dependencies = Object.create(null);
+    this.dependencies = new Set();
     this._totalLength = 0;
-    this.pageIndex = pageIndex;
-    this.intent = intent;
     this.weight = 0;
     this._resolved = streamSink ? null : Promise.resolve();
   }
@@ -589,17 +589,17 @@ var OperatorList = function OperatorListClosure() {
     },
 
     addDependency(dependency) {
-      if (dependency in this.dependencies) {
+      if (this.dependencies.has(dependency)) {
         return;
       }
 
-      this.dependencies[dependency] = true;
+      this.dependencies.add(dependency);
       this.addOp(_util.OPS.dependency, [dependency]);
     },
 
     addDependencies(dependencies) {
-      for (var key in dependencies) {
-        this.addDependency(key);
+      for (const dependency of dependencies) {
+        this.addDependency(dependency);
       }
     },
 
@@ -609,7 +609,9 @@ var OperatorList = function OperatorListClosure() {
         return;
       }
 
-      Object.assign(this.dependencies, opList.dependencies);
+      for (const dependency of opList.dependencies) {
+        this.dependencies.add(dependency);
+      }
 
       for (var i = 0, ii = opList.length; i < ii; i++) {
         this.addOp(opList.fnArray[i], opList.argsArray[i]);
@@ -663,7 +665,7 @@ var OperatorList = function OperatorListClosure() {
         length
       }, 1, this._transfers);
 
-      this.dependencies = Object.create(null);
+      this.dependencies.clear();
       this.fnArray.length = 0;
       this.argsArray.length = 0;
       this.weight = 0;

+ 40 - 3
lib/core/parser.js

@@ -196,10 +196,11 @@ class Parser {
           I = 0x49,
           SPACE = 0x20,
           LF = 0xa,
-          CR = 0xd;
-    const n = 10,
+          CR = 0xd,
           NUL = 0x0;
-    const startPos = stream.pos;
+    const lexer = this.lexer,
+          startPos = stream.pos,
+          n = 10;
     let state = 0,
         ch,
         maybeEIPos;
@@ -229,6 +230,20 @@ class Parser {
             }
           }
 
+          if (state !== 2) {
+            continue;
+          }
+
+          if (lexer.knownCommands) {
+            const nextObj = lexer.peekObj();
+
+            if (nextObj instanceof _primitives.Cmd && !lexer.knownCommands[nextObj.cmd]) {
+              state = 0;
+            }
+          } else {
+            (0, _util.warn)("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");
+          }
+
           if (state === 2) {
             break;
           }
@@ -1234,6 +1249,28 @@ class Lexer {
     return _primitives.Cmd.get(str);
   }
 
+  peekObj() {
+    const streamPos = this.stream.pos,
+          currentChar = this.currentChar,
+          beginInlineImagePos = this.beginInlineImagePos;
+    let nextObj;
+
+    try {
+      nextObj = this.getObj();
+    } catch (ex) {
+      if (ex instanceof _core_utils.MissingDataException) {
+        throw ex;
+      }
+
+      (0, _util.warn)(`peekObj: ${ex}`);
+    }
+
+    this.stream.pos = streamPos;
+    this.currentChar = currentChar;
+    this.beginInlineImagePos = beginInlineImagePos;
+    return nextObj;
+  }
+
   skipToNextLine() {
     let ch = this.currentChar;
 

+ 24 - 11
lib/core/pattern.js

@@ -56,7 +56,7 @@ var Pattern = function PatternClosure() {
     }
   };
 
-  Pattern.parseShading = function (shading, matrix, xref, res, handler, pdfFunctionFactory) {
+  Pattern.parseShading = function (shading, matrix, xref, res, handler, pdfFunctionFactory, localColorSpaceCache) {
     var dict = (0, _primitives.isStream)(shading) ? shading.dict : shading;
     var type = dict.get("ShadingType");
 
@@ -64,13 +64,13 @@ var Pattern = function PatternClosure() {
       switch (type) {
         case ShadingType.AXIAL:
         case ShadingType.RADIAL:
-          return new Shadings.RadialAxial(dict, matrix, xref, res, pdfFunctionFactory);
+          return new Shadings.RadialAxial(dict, matrix, xref, res, pdfFunctionFactory, localColorSpaceCache);
 
         case ShadingType.FREE_FORM_MESH:
         case ShadingType.LATTICE_FORM_MESH:
         case ShadingType.COONS_PATCH_MESH:
         case ShadingType.TENSOR_PATCH_MESH:
-          return new Shadings.Mesh(shading, matrix, xref, res, pdfFunctionFactory);
+          return new Shadings.Mesh(shading, matrix, xref, res, pdfFunctionFactory, localColorSpaceCache);
 
         default:
           throw new _util.FormatError("Unsupported ShadingType: " + type);
@@ -96,13 +96,20 @@ var Shadings = {};
 Shadings.SMALL_NUMBER = 1e-6;
 
 Shadings.RadialAxial = function RadialAxialClosure() {
-  function RadialAxial(dict, matrix, xref, res, pdfFunctionFactory) {
+  function RadialAxial(dict, matrix, xref, resources, pdfFunctionFactory, localColorSpaceCache) {
     this.matrix = matrix;
     this.coordsArr = dict.getArray("Coords");
     this.shadingType = dict.get("ShadingType");
     this.type = "Pattern";
-    var cs = dict.get("ColorSpace", "CS");
-    cs = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+
+    const cs = _colorspace.ColorSpace.parse({
+      cs: dict.getRaw("ColorSpace") || dict.getRaw("CS"),
+      xref,
+      resources,
+      pdfFunctionFactory,
+      localColorSpaceCache
+    });
+
     this.cs = cs;
     const bbox = dict.getArray("BBox");
 
@@ -146,7 +153,7 @@ Shadings.RadialAxial = function RadialAxialClosure() {
 
     this.extendStart = extendStart;
     this.extendEnd = extendEnd;
-    var fnObj = dict.get("Function");
+    var fnObj = dict.getRaw("Function");
     var fn = pdfFunctionFactory.createFromArray(fnObj);
     const NUMBER_OF_SAMPLES = 10;
     const step = (t1 - t0) / NUMBER_OF_SAMPLES;
@@ -835,7 +842,7 @@ Shadings.Mesh = function MeshClosure() {
     }
   }
 
-  function Mesh(stream, matrix, xref, res, pdfFunctionFactory) {
+  function Mesh(stream, matrix, xref, resources, pdfFunctionFactory, localColorSpaceCache) {
     if (!(0, _primitives.isStream)(stream)) {
       throw new _util.FormatError("Mesh data is not a stream");
     }
@@ -852,11 +859,17 @@ Shadings.Mesh = function MeshClosure() {
       this.bbox = null;
     }
 
-    var cs = dict.get("ColorSpace", "CS");
-    cs = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+    const cs = _colorspace.ColorSpace.parse({
+      cs: dict.getRaw("ColorSpace") || dict.getRaw("CS"),
+      xref,
+      resources,
+      pdfFunctionFactory,
+      localColorSpaceCache
+    });
+
     this.cs = cs;
     this.background = dict.has("Background") ? cs.getRgb(dict.get("Background"), 0) : null;
-    var fnObj = dict.get("Function");
+    var fnObj = dict.getRaw("Function");
     var fn = fnObj ? pdfFunctionFactory.createFromArray(fnObj) : null;
     this.coords = [];
     this.colors = [];

+ 112 - 53
lib/core/primitives.js

@@ -103,6 +103,10 @@ var Dict = function DictClosure() {
       this.xref = newXref;
     },
 
+    get size() {
+      return Object.keys(this._map).length;
+    },
+
     get(key1, key2, key3) {
       let value = this._map[key1];
 
@@ -165,6 +169,9 @@ var Dict = function DictClosure() {
     getKeys: function Dict_getKeys() {
       return Object.keys(this._map);
     },
+    getRawValues: function Dict_getRawValues() {
+      return Object.values(this._map);
+    },
     set: function Dict_set(key, value) {
       this._map[key] = value;
     },
@@ -179,26 +186,75 @@ var Dict = function DictClosure() {
   };
   Dict.empty = new Dict(null);
 
-  Dict.merge = function (xref, dictArray) {
+  Dict.merge = function ({
+    xref,
+    dictArray,
+    mergeSubDicts = false
+  }) {
     const mergedDict = new Dict(xref);
 
-    for (let i = 0, ii = dictArray.length; i < ii; i++) {
-      const dict = dictArray[i];
+    if (!mergeSubDicts) {
+      for (const dict of dictArray) {
+        if (!(dict instanceof Dict)) {
+          continue;
+        }
+
+        for (const [key, value] of Object.entries(dict._map)) {
+          if (mergedDict._map[key] === undefined) {
+            mergedDict._map[key] = value;
+          }
+        }
+      }
+
+      return mergedDict.size > 0 ? mergedDict : Dict.empty;
+    }
+
+    const properties = new Map();
 
-      if (!isDict(dict)) {
+    for (const dict of dictArray) {
+      if (!(dict instanceof Dict)) {
         continue;
       }
 
-      for (const keyName in dict._map) {
-        if (mergedDict._map[keyName] !== undefined) {
+      for (const [key, value] of Object.entries(dict._map)) {
+        let property = properties.get(key);
+
+        if (property === undefined) {
+          property = [];
+          properties.set(key, property);
+        }
+
+        property.push(value);
+      }
+    }
+
+    for (const [name, values] of properties) {
+      if (values.length === 1 || !(values[0] instanceof Dict)) {
+        mergedDict._map[name] = values[0];
+        continue;
+      }
+
+      const subDict = new Dict(xref);
+
+      for (const dict of values) {
+        if (!(dict instanceof Dict)) {
           continue;
         }
 
-        mergedDict._map[keyName] = dict._map[keyName];
+        for (const [key, value] of Object.entries(dict._map)) {
+          if (subDict._map[key] === undefined) {
+            subDict._map[key] = value;
+          }
+        }
+      }
+
+      if (subDict.size > 0) {
+        mergedDict._map[name] = subDict;
       }
     }
 
-    return mergedDict;
+    properties.clear();
+    return mergedDict.size > 0 ? mergedDict : Dict.empty;
   };
 
   return Dict;
@@ -239,60 +295,63 @@ var Ref = function RefClosure() {
 
 exports.Ref = Ref;
 
-var RefSet = function RefSetClosure() {
-  function RefSet() {
-    this.dict = Object.create(null);
+class RefSet {
+  constructor() {
+    this._set = new Set();
   }
 
-  RefSet.prototype = {
-    has: function RefSet_has(ref) {
-      return ref.toString() in this.dict;
-    },
-    put: function RefSet_put(ref) {
-      this.dict[ref.toString()] = true;
-    },
-    remove: function RefSet_remove(ref) {
-      delete this.dict[ref.toString()];
-    }
-  };
-  return RefSet;
-}();
+  has(ref) {
+    return this._set.has(ref.toString());
+  }
+
+  put(ref) {
+    this._set.add(ref.toString());
+  }
+
+  remove(ref) {
+    this._set.delete(ref.toString());
+  }
+
+}
 
 exports.RefSet = RefSet;
 
-var RefSetCache = function RefSetCacheClosure() {
-  function RefSetCache() {
-    this.dict = Object.create(null);
+class RefSetCache {
+  constructor() {
+    this._map = new Map();
   }
 
-  RefSetCache.prototype = {
-    get size() {
-      return Object.keys(this.dict).length;
-    },
+  get size() {
+    return this._map.size;
+  }
 
-    get: function RefSetCache_get(ref) {
-      return this.dict[ref.toString()];
-    },
-    has: function RefSetCache_has(ref) {
-      return ref.toString() in this.dict;
-    },
-    put: function RefSetCache_put(ref, obj) {
-      this.dict[ref.toString()] = obj;
-    },
-    putAlias: function RefSetCache_putAlias(ref, aliasRef) {
-      this.dict[ref.toString()] = this.get(aliasRef);
-    },
-    forEach: function RefSetCache_forEach(callback) {
-      for (const i in this.dict) {
-        callback(this.dict[i]);
-      }
-    },
-    clear: function RefSetCache_clear() {
-      this.dict = Object.create(null);
+  get(ref) {
+    return this._map.get(ref.toString());
+  }
+
+  has(ref) {
+    return this._map.has(ref.toString());
+  }
+
+  put(ref, obj) {
+    this._map.set(ref.toString(), obj);
+  }
+
+  putAlias(ref, aliasRef) {
+    this._map.set(ref.toString(), this.get(aliasRef));
+  }
+
+  forEach(callback) {
+    for (const value of this._map.values()) {
+      callback(value);
     }
-  };
-  return RefSetCache;
-}();
+  }
+
+  clear() {
+    this._map.clear();
+  }
+
+}
 
 exports.RefSetCache = RefSetCache;
 

+ 2 - 0
lib/core/type1_parser.js

@@ -217,7 +217,9 @@ var Type1CharString = function Type1CharStringClosure() {
 
             case (12 << 8) + 6:
               if (seacAnalysisEnabled) {
+                const asb = this.stack[this.stack.length - 5];
                 this.seac = this.stack.splice(-4, 4);
+                this.seac[0] += this.lsb - asb;
                 error = this.executeCommand(0, COMMAND_MAP.endchar);
               } else {
                 error = this.executeCommand(4, COMMAND_MAP.endchar);

+ 102 - 37
lib/core/worker.js

@@ -32,6 +32,8 @@ var _primitives = require("./primitives.js");
 
 var _pdf_manager = require("./pdf_manager.js");
 
+var _writer = require("./writer.js");
+
 var _is_node = require("../shared/is_node.js");
 
 var _message_handler = require("../shared/message_handler.js");
@@ -40,39 +42,37 @@ var _worker_stream = require("./worker_stream.js");
 
 var _core_utils = require("./core_utils.js");
 
-var WorkerTask = function WorkerTaskClosure() {
-  function WorkerTask(name) {
+class WorkerTask {
+  constructor(name) {
     this.name = name;
     this.terminated = false;
     this._capability = (0, _util.createPromiseCapability)();
   }
 
-  WorkerTask.prototype = {
-    get finished() {
-      return this._capability.promise;
-    },
+  get finished() {
+    return this._capability.promise;
+  }
 
-    finish() {
-      this._capability.resolve();
-    },
+  finish() {
+    this._capability.resolve();
+  }
 
-    terminate() {
-      this.terminated = true;
-    },
+  terminate() {
+    this.terminated = true;
+  }
 
-    ensureNotTerminated() {
-      if (this.terminated) {
-        throw new Error("Worker task was terminated");
-      }
+  ensureNotTerminated() {
+    if (this.terminated) {
+      throw new Error("Worker task was terminated");
     }
+  }
 
-  };
-  return WorkerTask;
-}();
+}
 
 exports.WorkerTask = WorkerTask;
-var WorkerMessageHandler = {
-  setup(handler, port) {
+
+class WorkerMessageHandler {
+  static setup(handler, port) {
     var testMessageProcessed = false;
     handler.on("test", function wphSetupTest(data) {
       if (testMessageProcessed) {
@@ -98,16 +98,16 @@ var WorkerMessageHandler = {
     handler.on("GetDocRequest", function wphSetupDoc(data) {
       return WorkerMessageHandler.createDocumentHandler(data, port);
     });
-  },
+  }
 
-  createDocumentHandler(docParams, port) {
+  static createDocumentHandler(docParams, port) {
     var pdfManager;
     var terminated = false;
     var cancelXHRs = null;
     var WorkerTasks = [];
     const verbosity = (0, _util.getVerbosityLevel)();
     const apiVersion = docParams.apiVersion;
-    const workerVersion = '2.5.207';
+    const workerVersion = '2.6.347';
 
     if (apiVersion !== workerVersion) {
       throw new Error(`The API version "${apiVersion}" does not match ` + `the Worker version "${workerVersion}".`);
@@ -321,11 +321,11 @@ var WorkerMessageHandler = {
 
       function pdfManagerReady() {
         ensureNotTerminated();
-        loadDocument(false).then(onSuccess, function loadFailure(ex) {
+        loadDocument(false).then(onSuccess, function (reason) {
           ensureNotTerminated();
 
-          if (!(ex instanceof _core_utils.XRefParseException)) {
-            onFailure(ex);
+          if (!(reason instanceof _core_utils.XRefParseException)) {
+            onFailure(reason);
             return;
           }
 
@@ -334,7 +334,7 @@ var WorkerMessageHandler = {
             ensureNotTerminated();
             loadDocument(true).then(onSuccess, onFailure);
           });
-        }, onFailure);
+        });
       }
 
       ensureNotTerminated();
@@ -372,11 +372,12 @@ var WorkerMessageHandler = {
         });
       });
     });
-    handler.on("GetPageIndex", function wphSetupGetPageIndex(data) {
-      var ref = _primitives.Ref.get(data.ref.num, data.ref.gen);
+    handler.on("GetPageIndex", function wphSetupGetPageIndex({
+      ref
+    }) {
+      const pageRef = _primitives.Ref.get(ref.num, ref.gen);
 
-      var catalog = pdfManager.pdfDocument.catalog;
-      return catalog.getPageIndex(ref);
+      return pdfManager.ensureCatalog("getPageIndex", [pageRef]);
     });
     handler.on("GetDestinations", function wphSetupGetDestinations(data) {
       return pdfManager.ensureCatalog("destinations");
@@ -408,6 +409,9 @@ var WorkerMessageHandler = {
     handler.on("GetOutline", function wphSetupGetOutline(data) {
       return pdfManager.ensureCatalog("documentOutline");
     });
+    handler.on("GetOptionalContentConfig", function (data) {
+      return pdfManager.ensureCatalog("optionalContentConfig");
+    });
     handler.on("GetPermissions", function (data) {
       return pdfManager.ensureCatalog("permissions");
     });
@@ -421,7 +425,7 @@ var WorkerMessageHandler = {
       });
     });
     handler.on("GetStats", function wphSetupGetStats(data) {
-      return pdfManager.pdfDocument.xref.stats;
+      return pdfManager.ensureXRef("stats");
     });
     handler.on("GetAnnotations", function ({
       pageIndex,
@@ -431,6 +435,65 @@ var WorkerMessageHandler = {
         return page.getAnnotationsData(intent);
       });
     });
+    handler.on("SaveDocument", function ({
+      numPages,
+      annotationStorage,
+      filename
+    }) {
+      pdfManager.requestLoadedStream();
+      const promises = [pdfManager.onLoadedStream()];
+      const document = pdfManager.pdfDocument;
+
+      for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
+        promises.push(pdfManager.getPage(pageIndex).then(function (page) {
+          const task = new WorkerTask(`Save: page ${pageIndex}`);
+          return page.save(handler, task, annotationStorage);
+        }));
+      }
+
+      return Promise.all(promises).then(([stream, ...refs]) => {
+        let newRefs = [];
+
+        for (const ref of refs) {
+          newRefs = ref.filter(x => x !== null).reduce((a, b) => a.concat(b), newRefs);
+        }
+
+        if (newRefs.length === 0) {
+          return stream.bytes;
+        }
+
+        const xref = document.xref;
+        let newXrefInfo = Object.create(null);
+
+        if (xref.trailer) {
+          const _info = Object.create(null);
+
+          const xrefInfo = xref.trailer.get("Info") || null;
+
+          if (xrefInfo) {
+            xrefInfo.forEach((key, value) => {
+              if ((0, _util.isString)(key) && (0, _util.isString)(value)) {
+                _info[key] = (0, _util.stringToPDFString)(value);
+              }
+            });
+          }
+
+          newXrefInfo = {
+            rootRef: xref.trailer.getRaw("Root") || null,
+            encrypt: xref.trailer.getRaw("Encrypt") || null,
+            newRef: xref.getNewRef(),
+            infoRef: xref.trailer.getRaw("Info") || null,
+            info: _info,
+            fileIds: xref.trailer.getRaw("ID") || null,
+            startXRef: document.startXRef,
+            filename
+          };
+        }
+
+        xref.resetNewRef();
+        return (0, _writer.incrementalUpdate)(stream.bytes, newXrefInfo, newRefs);
+      });
+    });
     handler.on("GetOperatorList", function wphSetupRenderPage(data, sink) {
       var pageIndex = data.pageIndex;
       pdfManager.getPage(pageIndex).then(function (page) {
@@ -442,7 +505,8 @@ var WorkerMessageHandler = {
           sink,
           task,
           intent: data.intent,
-          renderInteractiveForms: data.renderInteractiveForms
+          renderInteractiveForms: data.renderInteractiveForms,
+          annotationStorage: data.annotationStorage
         }).then(function (operatorListInfo) {
           finishWorkerTask(task);
 
@@ -538,15 +602,16 @@ var WorkerMessageHandler = {
       docParams = null;
     });
     return workerHandlerName;
-  },
+  }
 
-  initializeFromPort(port) {
+  static initializeFromPort(port) {
     var handler = new _message_handler.MessageHandler("worker", "main", port);
     WorkerMessageHandler.setup(handler, port);
     handler.send("ready", null);
   }
 
-};
+}
+
 exports.WorkerMessageHandler = WorkerMessageHandler;
 
 function isMessagePort(maybePort) {

+ 242 - 0
lib/core/writer.js

@@ -0,0 +1,242 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.writeDict = writeDict;
+exports.incrementalUpdate = incrementalUpdate;
+
+var _util = require("../shared/util.js");
+
+var _primitives = require("./primitives.js");
+
+var _crypto = require("./crypto.js");
+
+function writeDict(dict, buffer, transform) {
+  buffer.push("<<");
+
+  for (const key of dict.getKeys()) {
+    buffer.push(` /${key} `);
+    writeValue(dict.getRaw(key), buffer, transform);
+  }
+
+  buffer.push(">>");
+}
+
+function writeStream(stream, buffer, transform) {
+  writeDict(stream.dict, buffer, transform);
+  buffer.push(" stream\n");
+  let string = (0, _util.bytesToString)(stream.getBytes());
+
+  if (transform !== null) {
+    string = transform.encryptString(string);
+  }
+
+  buffer.push(string);
+  buffer.push("\nendstream\n");
+}
+
+function writeArray(array, buffer, transform) {
+  buffer.push("[");
+  let first = true;
+
+  for (const val of array) {
+    if (!first) {
+      buffer.push(" ");
+    } else {
+      first = false;
+    }
+
+    writeValue(val, buffer, transform);
+  }
+
+  buffer.push("]");
+}
+
+function numberToString(value) {
+  if (Number.isInteger(value)) {
+    return value.toString();
+  }
+
+  const roundedValue = Math.round(value * 100);
+
+  if (roundedValue % 100 === 0) {
+    return (roundedValue / 100).toString();
+  }
+
+  if (roundedValue % 10 === 0) {
+    return value.toFixed(1);
+  }
+
+  return value.toFixed(2);
+}
+
+function writeValue(value, buffer, transform) {
+  if ((0, _primitives.isName)(value)) {
+    buffer.push(`/${value.name}`);
+  } else if ((0, _primitives.isRef)(value)) {
+    buffer.push(`${value.num} ${value.gen} R`);
+  } else if (Array.isArray(value)) {
+    writeArray(value, buffer, transform);
+  } else if (typeof value === "string") {
+    if (transform !== null) {
+      value = transform.encryptString(value);
+    }
+
+    buffer.push(`(${(0, _util.escapeString)(value)})`);
+  } else if (typeof value === "number") {
+    buffer.push(numberToString(value));
+  } else if ((0, _primitives.isDict)(value)) {
+    writeDict(value, buffer, transform);
+  } else if ((0, _primitives.isStream)(value)) {
+    writeStream(value, buffer, transform);
+  }
+}
+
+function writeInt(number, size, offset, buffer) {
+  for (let i = size + offset - 1; i > offset - 1; i--) {
+    buffer[i] = number & 0xff;
+    number >>= 8;
+  }
+
+  return offset + size;
+}
+
+function writeString(string, offset, buffer) {
+  for (let i = 0, len = string.length; i < len; i++) {
+    buffer[offset + i] = string.charCodeAt(i) & 0xff;
+  }
+}
+
+function computeMD5(filesize, xrefInfo) {
+  const time = Math.floor(Date.now() / 1000);
+  const filename = xrefInfo.filename || "";
+  const md5Buffer = [time.toString(), filename, filesize.toString()];
+  let md5BufferLen = md5Buffer.reduce((a, str) => a + str.length, 0);
+
+  for (const value of Object.values(xrefInfo.info)) {
+    md5Buffer.push(value);
+    md5BufferLen += value.length;
+  }
+
+  const array = new Uint8Array(md5BufferLen);
+  let offset = 0;
+
+  for (const str of md5Buffer) {
+    writeString(str, offset, array);
+    offset += str.length;
+  }
+
+  return (0, _util.bytesToString)((0, _crypto.calculateMD5)(array));
+}
+
+function incrementalUpdate(originalData, xrefInfo, newRefs) {
+  const newXref = new _primitives.Dict(null);
+  const refForXrefTable = xrefInfo.newRef;
+  let buffer, baseOffset;
+  const lastByte = originalData[originalData.length - 1];
+
+  if (lastByte === 0x0a || lastByte === 0x0d) {
+    buffer = [];
+    baseOffset = originalData.length;
+  } else {
+    buffer = ["\n"];
+    baseOffset = originalData.length + 1;
+  }
+
+  newXref.set("Size", refForXrefTable.num + 1);
+  newXref.set("Prev", xrefInfo.startXRef);
+  newXref.set("Type", _primitives.Name.get("XRef"));
+
+  if (xrefInfo.rootRef !== null) {
+    newXref.set("Root", xrefInfo.rootRef);
+  }
+
+  if (xrefInfo.infoRef !== null) {
+    newXref.set("Info", xrefInfo.infoRef);
+  }
+
+  if (xrefInfo.encrypt !== null) {
+    newXref.set("Encrypt", xrefInfo.encrypt);
+  }
+
+  newRefs.push({
+    ref: refForXrefTable,
+    data: ""
+  });
+  newRefs = newRefs.sort((a, b) => {
+    return a.ref.num - b.ref.num;
+  });
+  const xrefTableData = [[0, 1, 0xffff]];
+  const indexes = [0, 1];
+  let maxOffset = 0;
+
+  for (const {
+    ref,
+    data
+  } of newRefs) {
+    maxOffset = Math.max(maxOffset, baseOffset);
+    xrefTableData.push([1, baseOffset, Math.min(ref.gen, 0xffff)]);
+    baseOffset += data.length;
+    indexes.push(ref.num);
+    indexes.push(1);
+    buffer.push(data);
+  }
+
+  newXref.set("Index", indexes);
+
+  if (xrefInfo.fileIds.length !== 0) {
+    const md5 = computeMD5(baseOffset, xrefInfo);
+    newXref.set("ID", [xrefInfo.fileIds[0], md5]);
+  }
+
+  const offsetSize = Math.ceil(Math.log2(maxOffset) / 8);
+  const sizes = [1, offsetSize, 2];
+  const structSize = sizes[0] + sizes[1] + sizes[2];
+  const tableLength = structSize * xrefTableData.length;
+  newXref.set("W", sizes);
+  newXref.set("Length", tableLength);
+  buffer.push(`${refForXrefTable.num} ${refForXrefTable.gen} obj\n`);
+  writeDict(newXref, buffer, null);
+  buffer.push(" stream\n");
+  const bufferLen = buffer.reduce((a, str) => a + str.length, 0);
+  const footer = `\nendstream\nendobj\nstartxref\n${baseOffset}\n%%EOF\n`;
+  const array = new Uint8Array(originalData.length + bufferLen + tableLength + footer.length);
+  array.set(originalData);
+  let offset = originalData.length;
+
+  for (const str of buffer) {
+    writeString(str, offset, array);
+    offset += str.length;
+  }
+
+  for (const [type, objOffset, gen] of xrefTableData) {
+    offset = writeInt(type, sizes[0], offset, array);
+    offset = writeInt(objOffset, sizes[1], offset, array);
+    offset = writeInt(gen, sizes[2], offset, array);
+  }
+
+  writeString(footer, offset, array);
+  return array;
+}

+ 51 - 10
lib/display/annotation_layer.js

@@ -30,6 +30,8 @@ var _display_utils = require("./display_utils.js");
 
 var _util = require("../shared/util.js");
 
+var _annotation_storage = require("./annotation_storage.js");
+
 class AnnotationElementFactory {
   static create(parameters) {
     const subtype = parameters.data.annotationType;
@@ -127,6 +129,7 @@ class AnnotationElement {
     this.imageResourcesPath = parameters.imageResourcesPath;
     this.renderInteractiveForms = parameters.renderInteractiveForms;
     this.svgFactory = parameters.svgFactory;
+    this.annotationStorage = parameters.annotationStorage;
 
     if (isRenderable) {
       this.container = this._createContainer(ignoreBorder);
@@ -333,19 +336,26 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
 
   render() {
     const TEXT_ALIGNMENT = ["left", "center", "right"];
+    const storage = this.annotationStorage;
+    const id = this.data.id;
     this.container.className = "textWidgetAnnotation";
     let element = null;
 
     if (this.renderInteractiveForms) {
+      const textContent = storage.getOrCreateValue(id, this.data.fieldValue);
+
       if (this.data.multiLine) {
         element = document.createElement("textarea");
-        element.textContent = this.data.fieldValue;
+        element.textContent = textContent;
       } else {
         element = document.createElement("input");
         element.type = "text";
-        element.setAttribute("value", this.data.fieldValue);
+        element.setAttribute("value", textContent);
       }
 
+      element.addEventListener("input", function (event) {
+        storage.setValue(id, event.target.value);
+      });
       element.disabled = this.data.readOnly;
       element.name = this.data.fieldName;
 
@@ -413,16 +423,23 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
   }
 
   render() {
+    const storage = this.annotationStorage;
+    const data = this.data;
+    const id = data.id;
+    const value = storage.getOrCreateValue(id, data.fieldValue && data.fieldValue !== "Off");
     this.container.className = "buttonWidgetAnnotation checkBox";
     const element = document.createElement("input");
-    element.disabled = this.data.readOnly;
+    element.disabled = data.readOnly;
     element.type = "checkbox";
     element.name = this.data.fieldName;
 
-    if (this.data.fieldValue && this.data.fieldValue !== "Off") {
+    if (value) {
       element.setAttribute("checked", true);
     }
 
+    element.addEventListener("change", function (event) {
+      storage.setValue(id, event.target.checked);
+    });
     this.container.appendChild(element);
     return this.container;
   }
@@ -436,15 +453,30 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
 
   render() {
     this.container.className = "buttonWidgetAnnotation radioButton";
+    const storage = this.annotationStorage;
+    const data = this.data;
+    const id = data.id;
+    const value = storage.getOrCreateValue(id, data.fieldValue === data.buttonValue);
     const element = document.createElement("input");
-    element.disabled = this.data.readOnly;
+    element.disabled = data.readOnly;
     element.type = "radio";
-    element.name = this.data.fieldName;
+    element.name = data.fieldName;
 
-    if (this.data.fieldValue === this.data.buttonValue) {
+    if (value) {
       element.setAttribute("checked", true);
     }
 
+    element.addEventListener("change", function (event) {
+      const name = event.target.name;
+
+      for (const radio of document.getElementsByName(name)) {
+        if (radio !== event.target) {
+          storage.setValue(radio.parentNode.getAttribute("data-annotation-id"), false);
+        }
+      }
+
+      storage.setValue(id, event.target.checked);
+    });
     this.container.appendChild(element);
     return this.container;
   }
@@ -467,6 +499,9 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
 
   render() {
     this.container.className = "choiceWidgetAnnotation";
+    const storage = this.annotationStorage;
+    const id = this.data.id;
+    storage.getOrCreateValue(id, this.data.fieldValue.length > 0 ? this.data.fieldValue[0] : null);
     const selectElement = document.createElement("select");
     selectElement.disabled = this.data.readOnly;
     selectElement.name = this.data.fieldName;
@@ -484,13 +519,18 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
       optionElement.textContent = option.displayValue;
       optionElement.value = option.exportValue;
 
-      if (this.data.fieldValue.includes(option.displayValue)) {
+      if (this.data.fieldValue.includes(option.exportValue)) {
         optionElement.setAttribute("selected", true);
       }
 
       selectElement.appendChild(optionElement);
     }
 
+    selectElement.addEventListener("input", function (event) {
+      const options = event.target.options;
+      const value = options[options.selectedIndex].value;
+      storage.setValue(id, value);
+    });
     this.container.appendChild(selectElement);
     return this.container;
   }
@@ -1024,8 +1064,9 @@ class AnnotationLayer {
         linkService: parameters.linkService,
         downloadManager: parameters.downloadManager,
         imageResourcesPath: parameters.imageResourcesPath || "",
-        renderInteractiveForms: parameters.renderInteractiveForms || false,
-        svgFactory: new _display_utils.DOMSVGFactory()
+        renderInteractiveForms: typeof parameters.renderInteractiveForms === "boolean" ? parameters.renderInteractiveForms : true,
+        svgFactory: new _display_utils.DOMSVGFactory(),
+        annotationStorage: parameters.annotationStorage || new _annotation_storage.AnnotationStorage()
       });
 
       if (element.isRenderable) {

+ 89 - 0
lib/display/annotation_storage.js

@@ -0,0 +1,89 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.AnnotationStorage = void 0;
+
+class AnnotationStorage {
+  constructor() {
+    this._storage = new Map();
+    this._modified = false;
+    this.onSetModified = null;
+    this.onResetModified = null;
+  }
+
+  getOrCreateValue(key, defaultValue) {
+    if (this._storage.has(key)) {
+      return this._storage.get(key);
+    }
+
+    this._storage.set(key, defaultValue);
+
+    return defaultValue;
+  }
+
+  setValue(key, value) {
+    if (this._storage.get(key) !== value) {
+      this._setModified();
+    }
+
+    this._storage.set(key, value);
+  }
+
+  getAll() {
+    if (this._storage.size === 0) {
+      return null;
+    }
+
+    return Object.fromEntries(this._storage);
+  }
+
+  get size() {
+    return this._storage.size;
+  }
+
+  _setModified() {
+    if (!this._modified) {
+      this._modified = true;
+
+      if (typeof this.onSetModified === "function") {
+        this.onSetModified();
+      }
+    }
+  }
+
+  resetModified() {
+    if (this._modified) {
+      this._modified = false;
+
+      if (typeof this.onResetModified === "function") {
+        this.onResetModified();
+      }
+    }
+  }
+
+}
+
+exports.AnnotationStorage = AnnotationStorage;

+ 140 - 54
lib/display/api.js

@@ -34,6 +34,10 @@ var _display_utils = require("./display_utils.js");
 
 var _font_loader = require("./font_loader.js");
 
+var _node_utils = require("./node_utils.js");
+
+var _annotation_storage = require("./annotation_storage.js");
+
 var _api_compatibility = require("./api_compatibility.js");
 
 var _canvas = require("./canvas.js");
@@ -46,12 +50,16 @@ var _message_handler = require("../shared/message_handler.js");
 
 var _metadata = require("./metadata.js");
 
+var _optional_content_config = require("./optional_content_config.js");
+
 var _transport_stream = require("./transport_stream.js");
 
 var _webgl = require("./webgl.js");
 
 const DEFAULT_RANGE_CHUNK_SIZE = 65536;
 const RENDERING_CANCELLED_TIMEOUT = 100;
+const DefaultCanvasFactory = _is_node.isNodeJS ? _node_utils.NodeCanvasFactory : _display_utils.DOMCanvasFactory;
+const DefaultCMapReaderFactory = _is_node.isNodeJS ? _node_utils.NodeCMapReaderFactory : _display_utils.DOMCMapReaderFactory;
 let createPDFNetworkStream;
 
 function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) {
@@ -120,7 +128,7 @@ function getDocument(src) {
   }
 
   params.rangeChunkSize = params.rangeChunkSize || DEFAULT_RANGE_CHUNK_SIZE;
-  params.CMapReaderFactory = params.CMapReaderFactory || _display_utils.DOMCMapReaderFactory;
+  params.CMapReaderFactory = params.CMapReaderFactory || DefaultCMapReaderFactory;
   params.ignoreErrors = params.stopAtErrors !== true;
   params.fontExtraProperties = params.fontExtraProperties === true;
   params.pdfBug = params.pdfBug === true;
@@ -137,6 +145,10 @@ function getDocument(src) {
     params.disableFontFace = _api_compatibility.apiCompatibilityParams.disableFontFace || false;
   }
 
+  if (typeof params.ownerDocument === "undefined") {
+    params.ownerDocument = globalThis.document;
+  }
+
   if (typeof params.disableRange !== "boolean") {
     params.disableRange = false;
   }
@@ -221,7 +233,7 @@ function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
 
   return worker.messageHandler.sendWithPromise("GetDocRequest", {
     docId,
-    apiVersion: '2.5.207',
+    apiVersion: '2.6.347',
     source: {
       data: source.data,
       url: source.url,
@@ -362,6 +374,10 @@ class PDFDocumentProxy {
     this._transport = transport;
   }
 
+  get annotationStorage() {
+    return (0, _util.shadow)(this, "annotationStorage", new _annotation_storage.AnnotationStorage());
+  }
+
   get numPages() {
     return this._pdfInfo.numPages;
   }
@@ -406,13 +422,6 @@ class PDFDocumentProxy {
     return this._transport.getOpenAction();
   }
 
-  getOpenActionDestination() {
-    (0, _display_utils.deprecated)("getOpenActionDestination, use getOpenAction instead.");
-    return this.getOpenAction().then(function (openAction) {
-      return openAction && openAction.dest ? openAction.dest : null;
-    });
-  }
-
   getAttachments() {
     return this._transport.getAttachments();
   }
@@ -425,6 +434,10 @@ class PDFDocumentProxy {
     return this._transport.getOutline();
   }
 
+  getOptionalContentConfig() {
+    return this._transport.getOptionalContentConfig();
+  }
+
   getPermissions() {
     return this._transport.getPermissions();
   }
@@ -461,14 +474,19 @@ class PDFDocumentProxy {
     return this._transport.loadingTask;
   }
 
+  saveDocument(annotationStorage) {
+    return this._transport.saveDocument(annotationStorage);
+  }
+
 }
 
 exports.PDFDocumentProxy = PDFDocumentProxy;
 
 class PDFPageProxy {
-  constructor(pageIndex, pageInfo, transport, pdfBug = false) {
+  constructor(pageIndex, pageInfo, transport, ownerDocument, pdfBug = false) {
     this._pageIndex = pageIndex;
     this._pageInfo = pageInfo;
+    this._ownerDocument = ownerDocument;
     this._transport = transport;
     this._stats = pdfBug ? new _display_utils.StatTimer() : null;
     this._pdfBug = pdfBug;
@@ -476,7 +494,7 @@ class PDFPageProxy {
     this.objs = new PDFObjects();
     this.cleanupAfterRender = false;
     this.pendingCleanup = false;
-    this.intentStates = Object.create(null);
+    this._intentStates = new Map();
     this.destroyed = false;
   }
 
@@ -537,7 +555,9 @@ class PDFPageProxy {
     transform = null,
     imageLayer = null,
     canvasFactory = null,
-    background = null
+    background = null,
+    annotationStorage = null,
+    optionalContentConfigPromise = null
   }) {
     if (this._stats) {
       this._stats.time("Overall");
@@ -546,18 +566,26 @@ class PDFPageProxy {
     const renderingIntent = intent === "print" ? "print" : "display";
     this.pendingCleanup = false;
 
-    if (!this.intentStates[renderingIntent]) {
-      this.intentStates[renderingIntent] = Object.create(null);
+    if (!optionalContentConfigPromise) {
+      optionalContentConfigPromise = this._transport.getOptionalContentConfig();
     }
 
-    const intentState = this.intentStates[renderingIntent];
+    let intentState = this._intentStates.get(renderingIntent);
+
+    if (!intentState) {
+      intentState = Object.create(null);
+
+      this._intentStates.set(renderingIntent, intentState);
+    }
 
     if (intentState.streamReaderCancelTimeout) {
       clearTimeout(intentState.streamReaderCancelTimeout);
       intentState.streamReaderCancelTimeout = null;
     }
 
-    const canvasFactoryInstance = canvasFactory || new _display_utils.DOMCanvasFactory();
+    const canvasFactoryInstance = canvasFactory || new DefaultCanvasFactory({
+      ownerDocument: this._ownerDocument
+    });
     const webGLContext = new _webgl.WebGLContext({
       enable: enableWebGL
     });
@@ -577,7 +605,8 @@ class PDFPageProxy {
       this._pumpOperatorList({
         pageIndex: this._pageIndex,
         intent: renderingIntent,
-        renderInteractiveForms: renderInteractiveForms === true
+        renderInteractiveForms: renderInteractiveForms === true,
+        annotationStorage: annotationStorage && annotationStorage.getAll() || null
       });
     }
 
@@ -637,7 +666,7 @@ class PDFPageProxy {
 
     intentState.renderTasks.push(internalRenderTask);
     const renderTask = internalRenderTask.task;
-    intentState.displayReadyCapability.promise.then(transparency => {
+    Promise.all([intentState.displayReadyCapability.promise, optionalContentConfigPromise]).then(([transparency, optionalContentConfig]) => {
       if (this.pendingCleanup) {
         complete();
         return;
@@ -647,7 +676,10 @@ class PDFPageProxy {
         this._stats.time("Rendering");
       }
 
-      internalRenderTask.initializeGraphics(transparency);
+      internalRenderTask.initializeGraphics({
+        transparency,
+        optionalContentConfig
+      });
       internalRenderTask.operatorListChanged();
     }).catch(complete);
     return renderTask;
@@ -667,15 +699,18 @@ class PDFPageProxy {
 
     const renderingIntent = "oplist";
 
-    if (!this.intentStates[renderingIntent]) {
-      this.intentStates[renderingIntent] = Object.create(null);
+    let intentState = this._intentStates.get(renderingIntent);
+
+    if (!intentState) {
+      intentState = Object.create(null);
+
+      this._intentStates.set(renderingIntent, intentState);
     }
 
-    const intentState = this.intentStates[renderingIntent];
     let opListTask;
 
     if (!intentState.opListReadCapability) {
-      opListTask = {};
+      opListTask = Object.create(null);
       opListTask.operatorListChanged = operatorListChanged;
       intentState.opListReadCapability = (0, _util.createPromiseCapability)();
       intentState.renderTasks = [];
@@ -750,9 +785,8 @@ class PDFPageProxy {
     this.destroyed = true;
     this._transport.pageCache[this._pageIndex] = null;
     const waitOn = [];
-    Object.keys(this.intentStates).forEach(intent => {
-      const intentState = this.intentStates[intent];
 
+    for (const [intent, intentState] of this._intentStates) {
       this._abortOperatorList({
         intentState,
         reason: new Error("Page was destroyed."),
@@ -760,15 +794,15 @@ class PDFPageProxy {
       });
 
       if (intent === "oplist") {
-        return;
+        continue;
       }
 
-      intentState.renderTasks.forEach(function (renderTask) {
-        const renderCompleted = renderTask.capability.promise.catch(function () {});
-        waitOn.push(renderCompleted);
-        renderTask.cancel();
-      });
-    });
+      for (const internalRenderTask of intentState.renderTasks) {
+        waitOn.push(internalRenderTask.completed);
+        internalRenderTask.cancel();
+      }
+    }
+
     this.objs.clear();
     this.annotationsPromise = null;
     this.pendingCleanup = false;
@@ -781,16 +815,21 @@ class PDFPageProxy {
   }
 
   _tryCleanup(resetStats = false) {
-    if (!this.pendingCleanup || Object.keys(this.intentStates).some(intent => {
-      const intentState = this.intentStates[intent];
-      return intentState.renderTasks.length !== 0 || !intentState.operatorList.lastChunk;
-    })) {
+    if (!this.pendingCleanup) {
       return false;
     }
 
-    Object.keys(this.intentStates).forEach(intent => {
-      delete this.intentStates[intent];
-    });
+    for (const {
+      renderTasks,
+      operatorList
+    } of this._intentStates.values()) {
+      if (renderTasks.length !== 0 || !operatorList.lastChunk) {
+        return false;
+      }
+    }
+
+    this._intentStates.clear();
+
     this.objs.clear();
     this.annotationsPromise = null;
 
@@ -803,7 +842,7 @@ class PDFPageProxy {
   }
 
   _startRenderPage(transparency, intent) {
-    const intentState = this.intentStates[intent];
+    const intentState = this._intentStates.get(intent);
 
     if (!intentState) {
       return;
@@ -841,7 +880,9 @@ class PDFPageProxy {
     const readableStream = this._transport.messageHandler.sendWithStream("GetOperatorList", args);
 
     const reader = readableStream.getReader();
-    const intentState = this.intentStates[args.intent];
+
+    const intentState = this._intentStates.get(args.intent);
+
     intentState.streamReader = reader;
 
     const pump = () => {
@@ -928,14 +969,14 @@ class PDFPageProxy {
       return;
     }
 
-    Object.keys(this.intentStates).some(intent => {
-      if (this.intentStates[intent] === intentState) {
-        delete this.intentStates[intent];
-        return true;
+    for (const [intent, curIntentState] of this._intentStates) {
+      if (curIntentState === intentState) {
+        this._intentStates.delete(intent);
+
+        break;
       }
+    }
 
-      return false;
-    });
     this.cleanup();
   }
 
@@ -1352,7 +1393,8 @@ class WorkerTransport {
     this.commonObjs = new PDFObjects();
     this.fontLoader = new _font_loader.FontLoader({
       docId: loadingTask.docId,
-      onUnsupportedFeature: this._onUnsupportedFeature.bind(this)
+      onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
+      ownerDocument: params.ownerDocument
     });
     this._params = params;
     this.CMapReaderFactory = new params.CMapReaderFactory({
@@ -1445,6 +1487,14 @@ class WorkerTransport {
 
       sink.onCancel = reason => {
         this._fullReader.cancel(reason);
+
+        sink.ready.catch(readyReason => {
+          if (this.destroyed) {
+            return;
+          }
+
+          throw readyReason;
+        });
       };
     });
     messageHandler.on("ReaderHeadersReady", data => {
@@ -1503,6 +1553,13 @@ class WorkerTransport {
 
       sink.onCancel = reason => {
         rangeReader.cancel(reason);
+        sink.ready.catch(readyReason => {
+          if (this.destroyed) {
+            return;
+          }
+
+          throw readyReason;
+        });
       };
     });
     messageHandler.on("GetDoc", ({
@@ -1537,6 +1594,11 @@ class WorkerTransport {
           break;
       }
 
+      if (!(reason instanceof Error)) {
+        const msg = "DocException - expected a valid Error.";
+        (0, _util.warn)(msg);
+      }
+
       loadingTask._capability.reject(reason);
     });
     messageHandler.on("PasswordRequest", exception => {
@@ -1633,7 +1695,6 @@ class WorkerTransport {
           break;
 
         case "FontPath":
-        case "FontType3Res":
         case "Image":
           this.commonObjs.resolve(id, exportedData);
           break;
@@ -1742,7 +1803,7 @@ class WorkerTransport {
         throw new Error("Transport destroyed");
       }
 
-      const page = new PDFPageProxy(pageIndex, pageInfo, this, this._params.pdfBug);
+      const page = new PDFPageProxy(pageIndex, pageInfo, this, this._params.ownerDocument, this._params.pdfBug);
       this.pageCache[pageIndex] = page;
       return page;
     });
@@ -1765,6 +1826,18 @@ class WorkerTransport {
     });
   }
 
+  saveDocument(annotationStorage) {
+    return this.messageHandler.sendWithPromise("SaveDocument", {
+      numPages: this._numPages,
+      annotationStorage: annotationStorage && annotationStorage.getAll() || null,
+      filename: this._fullReader ? this._fullReader.filename : null
+    }).finally(() => {
+      if (annotationStorage) {
+        annotationStorage.resetModified();
+      }
+    });
+  }
+
   getDestinations() {
     return this.messageHandler.sendWithPromise("GetDestinations", null);
   }
@@ -1811,6 +1884,12 @@ class WorkerTransport {
     return this.messageHandler.sendWithPromise("GetOutline", null);
   }
 
+  getOptionalContentConfig() {
+    return this.messageHandler.sendWithPromise("GetOptionalContentConfig", null).then(results => {
+      return new _optional_content_config.OptionalContentConfig(results);
+    });
+  }
+
   getPermissions() {
     return this.messageHandler.sendWithPromise("GetPermissions", null);
   }
@@ -1965,7 +2044,14 @@ const InternalRenderTask = function InternalRenderTaskClosure() {
       this._canvas = params.canvasContext.canvas;
     }
 
-    initializeGraphics(transparency = false) {
+    get completed() {
+      return this.capability.promise.catch(function () {});
+    }
+
+    initializeGraphics({
+      transparency = false,
+      optionalContentConfig
+    }) {
       if (this.cancelled) {
         return;
       }
@@ -1991,7 +2077,7 @@ const InternalRenderTask = function InternalRenderTaskClosure() {
         imageLayer,
         background
       } = this.params;
-      this.gfx = new _canvas.CanvasGraphics(canvasContext, this.commonObjs, this.objs, this.canvasFactory, this.webGLContext, imageLayer);
+      this.gfx = new _canvas.CanvasGraphics(canvasContext, this.commonObjs, this.objs, this.canvasFactory, this.webGLContext, imageLayer, optionalContentConfig);
       this.gfx.beginDrawing({
         transform,
         viewport,
@@ -2092,7 +2178,7 @@ const InternalRenderTask = function InternalRenderTaskClosure() {
   return InternalRenderTask;
 }();
 
-const version = '2.5.207';
+const version = '2.6.347';
 exports.version = version;
-const build = '0974d605';
+const build = '3be9c65f';
 exports.build = build;

+ 215 - 46
lib/display/canvas.js

@@ -373,6 +373,7 @@ var CanvasExtraState = function CanvasExtraStateClosure() {
     this.lineWidth = 1;
     this.activeSMask = null;
     this.resumeSMaskCtx = null;
+    this.transferMaps = null;
   }
 
   CanvasExtraState.prototype = {
@@ -391,7 +392,7 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
   var EXECUTION_TIME = 15;
   var EXECUTION_STEPS = 10;
 
-  function CanvasGraphics(canvasCtx, commonObjs, objs, canvasFactory, webGLContext, imageLayer) {
+  function CanvasGraphics(canvasCtx, commonObjs, objs, canvasFactory, webGLContext, imageLayer, optionalContentConfig) {
     this.ctx = canvasCtx;
     this.current = new CanvasExtraState();
     this.stateStack = [];
@@ -412,6 +413,9 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
     this.smaskStack = [];
     this.smaskCounter = 0;
     this.tempSMask = null;
+    this.contentVisible = true;
+    this.markedContentStack = [];
+    this.optionalContentConfig = optionalContentConfig;
     this.cachedCanvases = new CachedCanvases(this.canvasFactory);
 
     if (canvasCtx) {
@@ -421,7 +425,7 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
     this._cachedGetSinglePixelWidth = null;
   }
 
-  function putBinaryImageData(ctx, imgData) {
+  function putBinaryImageData(ctx, imgData, transferMaps = null) {
     if (typeof ImageData !== "undefined" && imgData instanceof ImageData) {
       ctx.putImageData(imgData, 0, 0);
       return;
@@ -438,6 +442,25 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
     var src = imgData.data;
     var dest = chunkImgData.data;
     var i, j, thisChunkHeight, elemsInThisChunk;
+    let transferMapRed, transferMapGreen, transferMapBlue, transferMapGray;
+
+    if (transferMaps) {
+      switch (transferMaps.length) {
+        case 1:
+          transferMapRed = transferMaps[0];
+          transferMapGreen = transferMaps[0];
+          transferMapBlue = transferMaps[0];
+          transferMapGray = transferMaps[0];
+          break;
+
+        case 4:
+          transferMapRed = transferMaps[0];
+          transferMapGreen = transferMaps[1];
+          transferMapBlue = transferMaps[2];
+          transferMapGray = transferMaps[3];
+          break;
+      }
+    }
 
     if (imgData.kind === _util.ImageKind.GRAYSCALE_1BPP) {
       var srcLength = src.byteLength;
@@ -447,13 +470,19 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       var white = 0xffffffff;
       var black = _util.IsLittleEndianCached.value ? 0xff000000 : 0x000000ff;
 
+      if (transferMapGray) {
+        if (transferMapGray[0] === 0xff && transferMapGray[0xff] === 0) {
+          [white, black] = [black, white];
+        }
+      }
+
       for (i = 0; i < totalChunks; i++) {
         thisChunkHeight = i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight;
         destPos = 0;
 
         for (j = 0; j < thisChunkHeight; j++) {
           var srcDiff = srcLength - srcPos;
-          var k = 0;
+          let k = 0;
           var kEnd = srcDiff > fullSrcDiff ? width : srcDiff * 8 - 7;
           var kEndUnrolled = kEnd & ~7;
           var mask = 0;
@@ -489,12 +518,30 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
         ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
       }
     } else if (imgData.kind === _util.ImageKind.RGBA_32BPP) {
+      const hasTransferMaps = !!(transferMapRed || transferMapGreen || transferMapBlue);
       j = 0;
       elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
 
       for (i = 0; i < fullChunks; i++) {
         dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
         srcPos += elemsInThisChunk;
+
+        if (hasTransferMaps) {
+          for (let k = 0; k < elemsInThisChunk; k += 4) {
+            if (transferMapRed) {
+              dest[k + 0] = transferMapRed[dest[k + 0]];
+            }
+
+            if (transferMapGreen) {
+              dest[k + 1] = transferMapGreen[dest[k + 1]];
+            }
+
+            if (transferMapBlue) {
+              dest[k + 2] = transferMapBlue[dest[k + 2]];
+            }
+          }
+        }
+
         ctx.putImageData(chunkImgData, 0, j);
         j += FULL_CHUNK_HEIGHT;
       }
@@ -502,9 +549,27 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       if (i < totalChunks) {
         elemsInThisChunk = width * partialChunkHeight * 4;
         dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
+
+        if (hasTransferMaps) {
+          for (let k = 0; k < elemsInThisChunk; k += 4) {
+            if (transferMapRed) {
+              dest[k + 0] = transferMapRed[dest[k + 0]];
+            }
+
+            if (transferMapGreen) {
+              dest[k + 1] = transferMapGreen[dest[k + 1]];
+            }
+
+            if (transferMapBlue) {
+              dest[k + 2] = transferMapBlue[dest[k + 2]];
+            }
+          }
+        }
+
         ctx.putImageData(chunkImgData, 0, j);
       }
     } else if (imgData.kind === _util.ImageKind.RGB_24BPP) {
+      const hasTransferMaps = !!(transferMapRed || transferMapGreen || transferMapBlue);
       thisChunkHeight = FULL_CHUNK_HEIGHT;
       elemsInThisChunk = width * thisChunkHeight;
 
@@ -523,6 +588,22 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
           dest[destPos++] = 255;
         }
 
+        if (hasTransferMaps) {
+          for (let k = 0; k < destPos; k += 4) {
+            if (transferMapRed) {
+              dest[k + 0] = transferMapRed[dest[k + 0]];
+            }
+
+            if (transferMapGreen) {
+              dest[k + 1] = transferMapGreen[dest[k + 1]];
+            }
+
+            if (transferMapBlue) {
+              dest[k + 2] = transferMapBlue[dest[k + 2]];
+            }
+          }
+        }
+
         ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
       }
     } else {
@@ -905,6 +986,9 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
 
             this.tempSMask = null;
             break;
+
+          case "TR":
+            this.current.transferMaps = value;
         }
       }
     },
@@ -1006,22 +1090,22 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
             var width = args[j++];
             var height = args[j++];
 
-            if (width === 0) {
+            if (width === 0 && ctx.lineWidth < this.getSinglePixelWidth()) {
               width = this.getSinglePixelWidth();
             }
 
-            if (height === 0) {
+            if (height === 0 && ctx.lineWidth < this.getSinglePixelWidth()) {
               height = this.getSinglePixelWidth();
             }
 
             var xw = x + width;
             var yh = y + height;
-            this.ctx.moveTo(x, y);
-            this.ctx.lineTo(xw, y);
-            this.ctx.lineTo(xw, yh);
-            this.ctx.lineTo(x, yh);
-            this.ctx.lineTo(x, y);
-            this.ctx.closePath();
+            ctx.moveTo(x, y);
+            ctx.lineTo(xw, y);
+            ctx.lineTo(xw, yh);
+            ctx.lineTo(x, yh);
+            ctx.lineTo(x, y);
+            ctx.closePath();
             break;
 
           case _util.OPS.moveTo:
@@ -1074,19 +1158,21 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       var strokeColor = this.current.strokeColor;
       ctx.globalAlpha = this.current.strokeAlpha;
 
-      if (strokeColor && strokeColor.hasOwnProperty("type") && strokeColor.type === "Pattern") {
-        ctx.save();
-        const transform = ctx.mozCurrentTransform;
+      if (this.contentVisible) {
+        if (strokeColor && strokeColor.hasOwnProperty("type") && strokeColor.type === "Pattern") {
+          ctx.save();
+          const transform = ctx.mozCurrentTransform;
 
-        const scale = _util.Util.singularValueDecompose2dScale(transform)[0];
+          const scale = _util.Util.singularValueDecompose2dScale(transform)[0];
 
-        ctx.strokeStyle = strokeColor.getPattern(ctx, this);
-        ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, this.current.lineWidth * scale);
-        ctx.stroke();
-        ctx.restore();
-      } else {
-        ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, this.current.lineWidth);
-        ctx.stroke();
+          ctx.strokeStyle = strokeColor.getPattern(ctx, this);
+          ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, this.current.lineWidth * scale);
+          ctx.stroke();
+          ctx.restore();
+        } else {
+          ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, this.current.lineWidth);
+          ctx.stroke();
+        }
       }
 
       if (consumePath) {
@@ -1117,11 +1203,13 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
         needRestore = true;
       }
 
-      if (this.pendingEOFill) {
-        ctx.fill("evenodd");
-        this.pendingEOFill = false;
-      } else {
-        ctx.fill();
+      if (this.contentVisible) {
+        if (this.pendingEOFill) {
+          ctx.fill("evenodd");
+          this.pendingEOFill = false;
+        } else {
+          ctx.fill();
+        }
       }
 
       if (needRestore) {
@@ -1466,15 +1554,15 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
           }
         }
 
-        if (glyph.isInFont || font.missingFile) {
+        if (this.contentVisible && (glyph.isInFont || font.missingFile)) {
           if (simpleFillText && !accent) {
             ctx.fillText(character, scaledX, scaledY);
           } else {
             this.paintChar(character, scaledX, scaledY, patternTransform);
 
             if (accent) {
-              scaledAccentX = scaledX + accent.offset.x / fontSizeScale;
-              scaledAccentY = scaledY - accent.offset.y / fontSizeScale;
+              scaledAccentX = scaledX + fontSize * accent.offset.x / fontSizeScale;
+              scaledAccentY = scaledY - fontSize * accent.offset.y / fontSizeScale;
               this.paintChar(accent.fontChar, scaledAccentX, scaledAccentY, patternTransform);
             }
           }
@@ -1546,12 +1634,14 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
           continue;
         }
 
-        this.processingType3 = glyph;
-        this.save();
-        ctx.scale(fontSize, fontSize);
-        ctx.transform.apply(ctx, fontMatrix);
-        this.executeOperatorList(operatorList);
-        this.restore();
+        if (this.contentVisible) {
+          this.processingType3 = glyph;
+          this.save();
+          ctx.scale(fontSize, fontSize);
+          ctx.transform.apply(ctx, fontMatrix);
+          this.executeOperatorList(operatorList);
+          this.restore();
+        }
 
         var transformed = _util.Util.applyTransform([glyph.width, 0], fontMatrix);
 
@@ -1608,6 +1698,10 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       this.current.patternFill = false;
     },
     shadingFill: function CanvasGraphics_shadingFill(patternIR) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       var ctx = this.ctx;
       this.save();
       var pattern = (0, _pattern_helper.getShadingPatternFromIR)(patternIR);
@@ -1645,6 +1739,10 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       (0, _util.unreachable)("Should not call beginImageData");
     },
     paintFormXObjectBegin: function CanvasGraphics_paintFormXObjectBegin(matrix, bbox) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       this.save();
       this.baseTransformStack.push(this.baseTransform);
 
@@ -1663,10 +1761,18 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       }
     },
     paintFormXObjectEnd: function CanvasGraphics_paintFormXObjectEnd() {
+      if (!this.contentVisible) {
+        return;
+      }
+
       this.restore();
       this.baseTransform = this.baseTransformStack.pop();
     },
     beginGroup: function CanvasGraphics_beginGroup(group) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       this.save();
       var currentCtx = this.ctx;
 
@@ -1748,6 +1854,10 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       this.current.activeSMask = null;
     },
     endGroup: function CanvasGraphics_endGroup(group) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       this.groupLevel--;
       var groupCtx = this.ctx;
       this.ctx = this.groupStack.pop();
@@ -1796,6 +1906,10 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       this.restore();
     },
     paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       var ctx = this.ctx;
       var width = img.width,
           height = img.height;
@@ -1830,7 +1944,12 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       maskCtx.restore();
       this.paintInlineImageXObject(maskCanvas.canvas);
     },
-    paintImageMaskXObjectRepeat: function CanvasGraphics_paintImageMaskXObjectRepeat(imgData, scaleX, scaleY, positions) {
+
+    paintImageMaskXObjectRepeat(imgData, scaleX, skewX = 0, skewY = 0, scaleY, positions) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       var width = imgData.width;
       var height = imgData.height;
       var fillColor = this.current.fillColor;
@@ -1847,13 +1966,18 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
 
       for (var i = 0, ii = positions.length; i < ii; i += 2) {
         ctx.save();
-        ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]);
+        ctx.transform(scaleX, skewX, skewY, scaleY, positions[i], positions[i + 1]);
         ctx.scale(1, -1);
         ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1);
         ctx.restore();
       }
     },
+
     paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup(images) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       var ctx = this.ctx;
       var fillColor = this.current.fillColor;
       var isPatternFill = this.current.patternFill;
@@ -1878,6 +2002,10 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       }
     },
     paintImageXObject: function CanvasGraphics_paintImageXObject(objId) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       const imgData = objId.startsWith("g_") ? this.commonObjs.get(objId) : this.objs.get(objId);
 
       if (!imgData) {
@@ -1888,6 +2016,10 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       this.paintInlineImageXObject(imgData);
     },
     paintImageXObjectRepeat: function CanvasGraphics_paintImageXObjectRepeat(objId, scaleX, scaleY, positions) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       const imgData = objId.startsWith("g_") ? this.commonObjs.get(objId) : this.objs.get(objId);
 
       if (!imgData) {
@@ -1912,6 +2044,10 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       this.paintInlineImageXObjectGroup(imgData, map);
     },
     paintInlineImageXObject: function CanvasGraphics_paintInlineImageXObject(imgData) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       var width = imgData.width;
       var height = imgData.height;
       var ctx = this.ctx;
@@ -1931,7 +2067,7 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       } else {
         tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", width, height);
         var tmpCtx = tmpCanvas.context;
-        putBinaryImageData(tmpCtx, imgData);
+        putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
         imgToPaint = tmpCanvas.canvas;
       }
 
@@ -1979,12 +2115,16 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       this.restore();
     },
     paintInlineImageXObjectGroup: function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) {
+      if (!this.contentVisible) {
+        return;
+      }
+
       var ctx = this.ctx;
       var w = imgData.width;
       var h = imgData.height;
       var tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h);
       var tmpCtx = tmpCanvas.context;
-      putBinaryImageData(tmpCtx, imgData);
+      putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
 
       for (var i = 0, ii = map.length; i < ii; i++) {
         var entry = map[i];
@@ -2008,16 +2148,36 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
       }
     },
     paintSolidColorImageMask: function CanvasGraphics_paintSolidColorImageMask() {
+      if (!this.contentVisible) {
+        return;
+      }
+
       this.ctx.fillRect(0, 0, 1, 1);
     },
-    paintXObject: function CanvasGraphics_paintXObject() {
-      (0, _util.warn)("Unsupported 'paintXObject' command.");
-    },
     markPoint: function CanvasGraphics_markPoint(tag) {},
     markPointProps: function CanvasGraphics_markPointProps(tag, properties) {},
-    beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) {},
-    beginMarkedContentProps: function CanvasGraphics_beginMarkedContentProps(tag, properties) {},
-    endMarkedContent: function CanvasGraphics_endMarkedContent() {},
+    beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) {
+      this.markedContentStack.push({
+        visible: true
+      });
+    },
+    beginMarkedContentProps: function CanvasGraphics_beginMarkedContentProps(tag, properties) {
+      if (tag === "OC") {
+        this.markedContentStack.push({
+          visible: this.optionalContentConfig.isVisible(properties)
+        });
+      } else {
+        this.markedContentStack.push({
+          visible: true
+        });
+      }
+
+      this.contentVisible = this.isContentVisible();
+    },
+    endMarkedContent: function CanvasGraphics_endMarkedContent() {
+      this.markedContentStack.pop();
+      this.contentVisible = this.isContentVisible();
+    },
     beginCompat: function CanvasGraphics_beginCompat() {},
     endCompat: function CanvasGraphics_endCompat() {},
     consumePath: function CanvasGraphics_consumePath() {
@@ -2048,6 +2208,15 @@ var CanvasGraphics = function CanvasGraphicsClosure() {
     getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) {
       var transform = this.ctx.mozCurrentTransform;
       return [transform[0] * x + transform[2] * y + transform[4], transform[1] * x + transform[3] * y + transform[5]];
+    },
+    isContentVisible: function CanvasGraphics_isContentVisible() {
+      for (let i = this.markedContentStack.length - 1; i >= 0; i--) {
+        if (!this.markedContentStack[i].visible) {
+          return false;
+        }
+      }
+
+      return true;
     }
   };
 

+ 55 - 18
lib/display/display_utils.js

@@ -30,7 +30,7 @@ exports.isFetchSupported = isFetchSupported;
 exports.isValidFetchUrl = isValidFetchUrl;
 exports.loadScript = loadScript;
 exports.deprecated = deprecated;
-exports.PDFDateString = exports.StatTimer = exports.DOMSVGFactory = exports.DOMCMapReaderFactory = exports.DOMCanvasFactory = exports.DEFAULT_LINK_REL = exports.LinkTarget = exports.RenderingCancelledException = exports.PageViewport = void 0;
+exports.PDFDateString = exports.StatTimer = exports.DOMSVGFactory = exports.DOMCMapReaderFactory = exports.BaseCMapReaderFactory = exports.DOMCanvasFactory = exports.BaseCanvasFactory = exports.DEFAULT_LINK_REL = exports.LinkTarget = exports.RenderingCancelledException = exports.PageViewport = void 0;
 
 var _util = require("../shared/util.js");
 
@@ -38,20 +38,15 @@ const DEFAULT_LINK_REL = "noopener noreferrer nofollow";
 exports.DEFAULT_LINK_REL = DEFAULT_LINK_REL;
 const SVG_NS = "http://www.w3.org/2000/svg";
 
-class DOMCanvasFactory {
-  create(width, height) {
-    if (width <= 0 || height <= 0) {
-      throw new Error("Invalid canvas size");
+class BaseCanvasFactory {
+  constructor() {
+    if (this.constructor === BaseCanvasFactory) {
+      (0, _util.unreachable)("Cannot initialize BaseCanvasFactory.");
     }
+  }
 
-    const canvas = document.createElement("canvas");
-    const context = canvas.getContext("2d");
-    canvas.width = width;
-    canvas.height = height;
-    return {
-      canvas,
-      context
-    };
+  create(width, height) {
+    (0, _util.unreachable)("Abstract method `create` called.");
   }
 
   reset(canvasAndContext, width, height) {
@@ -80,13 +75,45 @@ class DOMCanvasFactory {
 
 }
 
+exports.BaseCanvasFactory = BaseCanvasFactory;
+
+class DOMCanvasFactory extends BaseCanvasFactory {
+  constructor({
+    ownerDocument = globalThis.document
+  } = {}) {
+    super();
+    this._document = ownerDocument;
+  }
+
+  create(width, height) {
+    if (width <= 0 || height <= 0) {
+      throw new Error("Invalid canvas size");
+    }
+
+    const canvas = this._document.createElement("canvas");
+
+    const context = canvas.getContext("2d");
+    canvas.width = width;
+    canvas.height = height;
+    return {
+      canvas,
+      context
+    };
+  }
+
+}
+
 exports.DOMCanvasFactory = DOMCanvasFactory;
 
-class DOMCMapReaderFactory {
+class BaseCMapReaderFactory {
   constructor({
     baseUrl = null,
     isCompressed = false
   }) {
+    if (this.constructor === BaseCMapReaderFactory) {
+      (0, _util.unreachable)("Cannot initialize BaseCMapReaderFactory.");
+    }
+
     this.baseUrl = baseUrl;
     this.isCompressed = isCompressed;
   }
@@ -104,7 +131,21 @@ class DOMCMapReaderFactory {
 
     const url = this.baseUrl + name + (this.isCompressed ? ".bcmap" : "");
     const compressionType = this.isCompressed ? _util.CMapCompressionType.BINARY : _util.CMapCompressionType.NONE;
+    return this._fetchData(url, compressionType).catch(reason => {
+      throw new Error(`Unable to load ${this.isCompressed ? "binary " : ""}CMap at: ${url}`);
+    });
+  }
+
+  _fetchData(url, compressionType) {
+    (0, _util.unreachable)("Abstract method `_fetchData` called.");
+  }
+
+}
+
+exports.BaseCMapReaderFactory = BaseCMapReaderFactory;
 
+class DOMCMapReaderFactory extends BaseCMapReaderFactory {
+  _fetchData(url, compressionType) {
     if (isFetchSupported() && isValidFetchUrl(url, document.baseURI)) {
       return fetch(url).then(async response => {
         if (!response.ok) {
@@ -123,8 +164,6 @@ class DOMCMapReaderFactory {
           cMapData,
           compressionType
         };
-      }).catch(reason => {
-        throw new Error(`Unable to load ${this.isCompressed ? "binary " : ""}` + `CMap at: ${url}`);
       });
     }
 
@@ -163,8 +202,6 @@ class DOMCMapReaderFactory {
       };
 
       request.send(null);
-    }).catch(reason => {
-      throw new Error(`Unable to load ${this.isCompressed ? "binary " : ""}` + `CMap at: ${url}`);
     });
   }
 

+ 6 - 0
lib/display/fetch_stream.js

@@ -253,6 +253,12 @@ class PDFFetchStreamRangeReader {
       this._readCapability.resolve();
 
       this._reader = response.body.getReader();
+    }).catch(reason => {
+      if (reason && reason.name === "AbortError") {
+        return;
+      }
+
+      throw reason;
     });
     this.onProgress = null;
   }

+ 26 - 15
lib/display/font_loader.js

@@ -31,7 +31,8 @@ var _util = require("../shared/util.js");
 class BaseFontLoader {
   constructor({
     docId,
-    onUnsupportedFeature
+    onUnsupportedFeature,
+    ownerDocument = globalThis.document
   }) {
     if (this.constructor === BaseFontLoader) {
       (0, _util.unreachable)("Cannot initialize BaseFontLoader.");
@@ -39,22 +40,25 @@ class BaseFontLoader {
 
     this.docId = docId;
     this._onUnsupportedFeature = onUnsupportedFeature;
+    this._document = ownerDocument;
     this.nativeFontFaces = [];
     this.styleElement = null;
   }
 
   addNativeFontFace(nativeFontFace) {
     this.nativeFontFaces.push(nativeFontFace);
-    document.fonts.add(nativeFontFace);
+
+    this._document.fonts.add(nativeFontFace);
   }
 
   insertRule(rule) {
     let styleElement = this.styleElement;
 
     if (!styleElement) {
-      styleElement = this.styleElement = document.createElement("style");
+      styleElement = this.styleElement = this._document.createElement("style");
       styleElement.id = `PDFJS_FONT_STYLE_TAG_${this.docId}`;
-      document.documentElement.getElementsByTagName("head")[0].appendChild(styleElement);
+
+      this._document.documentElement.getElementsByTagName("head")[0].appendChild(styleElement);
     }
 
     const styleSheet = styleElement.sheet;
@@ -62,8 +66,8 @@ class BaseFontLoader {
   }
 
   clear() {
-    this.nativeFontFaces.forEach(function (nativeFontFace) {
-      document.fonts.delete(nativeFontFace);
+    this.nativeFontFaces.forEach(nativeFontFace => {
+      this._document.fonts.delete(nativeFontFace);
     });
     this.nativeFontFaces.length = 0;
 
@@ -124,7 +128,7 @@ class BaseFontLoader {
   }
 
   get isFontLoadingAPISupported() {
-    const supported = typeof document !== "undefined" && !!document.fonts;
+    const supported = typeof this._document !== "undefined" && !!this._document.fonts;
     return (0, _util.shadow)(this, "isFontLoadingAPISupported", supported);
   }
 
@@ -146,8 +150,8 @@ let FontLoader;
 exports.FontLoader = FontLoader;
 {
   exports.FontLoader = FontLoader = class GenericFontLoader extends BaseFontLoader {
-    constructor(docId) {
-      super(docId);
+    constructor(params) {
+      super(params);
       this.loadingContext = {
         requests: [],
         nextRequestId: 0
@@ -213,7 +217,9 @@ exports.FontLoader = FontLoader;
       }
 
       let i, ii;
-      const canvas = document.createElement("canvas");
+
+      const canvas = this._document.createElement("canvas");
+
       canvas.width = 1;
       canvas.height = 1;
       const ctx = canvas.getContext("2d");
@@ -267,22 +273,27 @@ exports.FontLoader = FontLoader;
       }
 
       names.push(loadTestFontId);
-      const div = document.createElement("div");
+
+      const div = this._document.createElement("div");
+
       div.style.visibility = "hidden";
       div.style.width = div.style.height = "10px";
       div.style.position = "absolute";
       div.style.top = div.style.left = "0px";
 
       for (i = 0, ii = names.length; i < ii; ++i) {
-        const span = document.createElement("span");
+        const span = this._document.createElement("span");
+
         span.textContent = "Hi";
         span.style.fontFamily = names[i];
         div.appendChild(span);
       }
 
-      document.body.appendChild(div);
-      isFontReady(loadTestFontId, function () {
-        document.body.removeChild(div);
+      this._document.body.appendChild(div);
+
+      isFontReady(loadTestFontId, () => {
+        this._document.body.removeChild(div);
+
         request.complete();
       });
     }

+ 1 - 7
lib/display/metadata.js

@@ -125,13 +125,7 @@ class Metadata {
   }
 
   getAll() {
-    const obj = Object.create(null);
-
-    for (const [key, value] of this._metadataMap) {
-      obj[key] = value;
-    }
-
-    return obj;
+    return Object.fromEntries(this._metadataMap);
   }
 
   has(name) {

+ 0 - 14
lib/display/network.js

@@ -196,14 +196,6 @@ class NetworkManager {
     }
   }
 
-  hasPendingRequests() {
-    for (const xhrId in this.pendingRequests) {
-      return true;
-    }
-
-    return false;
-  }
-
   getRequestXhr(xhrId) {
     return this.pendingRequests[xhrId].xhr;
   }
@@ -212,12 +204,6 @@ class NetworkManager {
     return xhrId in this.pendingRequests;
   }
 
-  abortAllRequests() {
-    for (const xhrId in this.pendingRequests) {
-      this.abortRequest(xhrId | 0);
-    }
-  }
-
   abortRequest(xhrId) {
     const xhr = this.pendingRequests[xhrId].xhr;
     delete this.pendingRequests[xhrId];

+ 87 - 0
lib/display/node_utils.js

@@ -0,0 +1,87 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.NodeCMapReaderFactory = exports.NodeCanvasFactory = void 0;
+
+var _display_utils = require("./display_utils.js");
+
+var _is_node = require("../shared/is_node.js");
+
+var _util = require("../shared/util.js");
+
+let NodeCanvasFactory = class {
+  constructor() {
+    (0, _util.unreachable)("Not implemented: NodeCanvasFactory");
+  }
+
+};
+exports.NodeCanvasFactory = NodeCanvasFactory;
+let NodeCMapReaderFactory = class {
+  constructor() {
+    (0, _util.unreachable)("Not implemented: NodeCMapReaderFactory");
+  }
+
+};
+exports.NodeCMapReaderFactory = NodeCMapReaderFactory;
+
+if (_is_node.isNodeJS) {
+  exports.NodeCanvasFactory = NodeCanvasFactory = class extends _display_utils.BaseCanvasFactory {
+    create(width, height) {
+      if (width <= 0 || height <= 0) {
+        throw new Error("Invalid canvas size");
+      }
+
+      const Canvas = require("canvas");
+
+      const canvas = Canvas.createCanvas(width, height);
+      return {
+        canvas,
+        context: canvas.getContext("2d")
+      };
+    }
+
+  };
+  exports.NodeCMapReaderFactory = NodeCMapReaderFactory = class extends _display_utils.BaseCMapReaderFactory {
+    _fetchData(url, compressionType) {
+      return new Promise((resolve, reject) => {
+        const fs = require("fs");
+
+        fs.readFile(url, (error, data) => {
+          if (error || !data) {
+            reject(new Error(error));
+            return;
+          }
+
+          resolve({
+            cMapData: new Uint8Array(data),
+            compressionType
+          });
+        });
+      });
+    }
+
+  };
+}

+ 184 - 0
lib/display/optional_content_config.js

@@ -0,0 +1,184 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.OptionalContentConfig = void 0;
+
+var _util = require("../shared/util.js");
+
+class OptionalContentGroup {
+  constructor(name, intent) {
+    this.visible = true;
+    this.name = name;
+    this.intent = intent;
+  }
+
+}
+
+class OptionalContentConfig {
+  constructor(data) {
+    this.name = null;
+    this.creator = null;
+    this._order = null;
+    this._groups = new Map();
+
+    if (data === null) {
+      return;
+    }
+
+    this.name = data.name;
+    this.creator = data.creator;
+    this._order = data.order;
+
+    for (const group of data.groups) {
+      this._groups.set(group.id, new OptionalContentGroup(group.name, group.intent));
+    }
+
+    if (data.baseState === "OFF") {
+      for (const group of this._groups) {
+        group.visible = false;
+      }
+    }
+
+    for (const on of data.on) {
+      this._groups.get(on).visible = true;
+    }
+
+    for (const off of data.off) {
+      this._groups.get(off).visible = false;
+    }
+  }
+
+  isVisible(group) {
+    if (group.type === "OCG") {
+      if (!this._groups.has(group.id)) {
+        (0, _util.warn)(`Optional content group not found: ${group.id}`);
+        return true;
+      }
+
+      return this._groups.get(group.id).visible;
+    } else if (group.type === "OCMD") {
+      if (group.expression) {
+        (0, _util.warn)("Visibility expression not supported yet.");
+      }
+
+      if (!group.policy || group.policy === "AnyOn") {
+        for (const id of group.ids) {
+          if (!this._groups.has(id)) {
+            (0, _util.warn)(`Optional content group not found: ${id}`);
+            return true;
+          }
+
+          if (this._groups.get(id).visible) {
+            return true;
+          }
+        }
+
+        return false;
+      } else if (group.policy === "AllOn") {
+        for (const id of group.ids) {
+          if (!this._groups.has(id)) {
+            (0, _util.warn)(`Optional content group not found: ${id}`);
+            return true;
+          }
+
+          if (!this._groups.get(id).visible) {
+            return false;
+          }
+        }
+
+        return true;
+      } else if (group.policy === "AnyOff") {
+        for (const id of group.ids) {
+          if (!this._groups.has(id)) {
+            (0, _util.warn)(`Optional content group not found: ${id}`);
+            return true;
+          }
+
+          if (!this._groups.get(id).visible) {
+            return true;
+          }
+        }
+
+        return false;
+      } else if (group.policy === "AllOff") {
+        for (const id of group.ids) {
+          if (!this._groups.has(id)) {
+            (0, _util.warn)(`Optional content group not found: ${id}`);
+            return true;
+          }
+
+          if (this._groups.get(id).visible) {
+            return false;
+          }
+        }
+
+        return true;
+      }
+
+      (0, _util.warn)(`Unknown optional content policy ${group.policy}.`);
+      return true;
+    }
+
+    (0, _util.warn)(`Unknown group type ${group.type}.`);
+    return true;
+  }
+
+  setVisibility(id, visible = true) {
+    if (!this._groups.has(id)) {
+      (0, _util.warn)(`Optional content group not found: ${id}`);
+      return;
+    }
+
+    this._groups.get(id).visible = !!visible;
+  }
+
+  getOrder() {
+    if (!this._groups.size) {
+      return null;
+    }
+
+    if (this._order) {
+      return this._order.slice();
+    }
+
+    return Array.from(this._groups.keys());
+  }
+
+  getGroups() {
+    if (!this._groups.size) {
+      return null;
+    }
+
+    return Object.fromEntries(this._groups);
+  }
+
+  getGroup(id) {
+    return this._groups.get(id) || null;
+  }
+
+}
+
+exports.OptionalContentConfig = OptionalContentConfig;

+ 1 - 1
lib/display/svg.js

@@ -1420,7 +1420,7 @@ exports.SVGGraphics = SVGGraphics;
     }
 
     paintImageXObject(objId) {
-      const imgData = this.objs.get(objId);
+      const imgData = objId.startsWith("g_") ? this.commonObjs.get(objId) : this.objs.get(objId);
 
       if (!imgData) {
         (0, _util.warn)(`Dependent image with object ID ${objId} is not ready yet`);

+ 4 - 1
lib/display/text_layer.js

@@ -462,6 +462,7 @@ var renderTextLayer = function renderTextLayerClosure() {
     this._textContent = textContent;
     this._textContentStream = textContentStream;
     this._container = container;
+    this._document = container.ownerDocument;
     this._viewport = viewport;
     this._textDivs = textDivs || [];
     this._textContentItemsStr = textContentItemsStr || [];
@@ -568,7 +569,9 @@ var renderTextLayer = function renderTextLayerClosure() {
     _render: function TextLayer_render(timeout) {
       const capability = (0, _util.createPromiseCapability)();
       let styleCache = Object.create(null);
-      const canvas = document.createElement("canvas");
+
+      const canvas = this._document.createElement("canvas");
+
       canvas.mozOpaque = true;
       this._layoutTextCtx = canvas.getContext("2d", {
         alpha: false

+ 2 - 2
lib/pdf.js

@@ -233,8 +233,8 @@ var _text_layer = require("./display/text_layer.js");
 
 var _svg = require("./display/svg.js");
 
-const pdfjsVersion = '2.5.207';
-const pdfjsBuild = '0974d605';
+const pdfjsVersion = '2.6.347';
+const pdfjsBuild = '3be9c65f';
 {
   const {
     isNodeJS

+ 2 - 2
lib/pdf.worker.js

@@ -33,5 +33,5 @@ Object.defineProperty(exports, "WorkerMessageHandler", {
 
 var _worker = require("./core/worker.js");
 
-const pdfjsVersion = '2.5.207';
-const pdfjsBuild = '0974d605';
+const pdfjsVersion = '2.6.347';
+const pdfjsBuild = '3be9c65f';

+ 1 - 1
lib/shared/is_node.js

@@ -25,5 +25,5 @@ Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.isNodeJS = void 0;
-const isNodeJS = typeof process === "object" && process + "" === "[object process]" && !process.versions.nw && !process.versions.electron;
+const isNodeJS = typeof process === "object" && process + "" === "[object process]" && !process.versions.nw && !(process.versions.electron && process.type && process.type !== "browser");
 exports.isNodeJS = isNodeJS;

+ 13 - 10
lib/shared/util.js

@@ -29,12 +29,13 @@ exports.arraysToBytes = arraysToBytes;
 exports.assert = assert;
 exports.bytesToString = bytesToString;
 exports.createPromiseCapability = createPromiseCapability;
+exports.escapeString = escapeString;
+exports.getModificationDate = getModificationDate;
 exports.getVerbosityLevel = getVerbosityLevel;
 exports.info = info;
 exports.isArrayBuffer = isArrayBuffer;
 exports.isArrayEqual = isArrayEqual;
 exports.isBool = isBool;
-exports.isEmptyObj = isEmptyObj;
 exports.isNum = isNum;
 exports.isString = isString;
 exports.isSameOrigin = isSameOrigin;
@@ -333,7 +334,8 @@ const UNSUPPORTED_FEATURES = {
   errorOperatorList: "errorOperatorList",
   errorFontToUnicode: "errorFontToUnicode",
   errorFontLoadNative: "errorFontLoadNative",
-  errorFontGetPath: "errorFontGetPath"
+  errorFontGetPath: "errorFontGetPath",
+  errorMarkedContent: "errorMarkedContent"
 };
 exports.UNSUPPORTED_FEATURES = UNSUPPORTED_FEATURES;
 const PasswordResponses = {
@@ -748,6 +750,10 @@ function stringToPDFString(str) {
   return strBuf.join("");
 }
 
+function escapeString(str) {
+  return str.replace(/([\(\)\\])/g, "\\$1");
+}
+
 function stringToUTF8String(str) {
   return decodeURIComponent(escape(str));
 }
@@ -756,14 +762,6 @@ function utf8StringToString(str) {
   return unescape(encodeURIComponent(str));
 }
 
-function isEmptyObj(obj) {
-  for (const key in obj) {
-    return false;
-  }
-
-  return true;
-}
-
 function isBool(v) {
   return typeof v === "boolean";
 }
@@ -790,6 +788,11 @@ function isArrayEqual(arr1, arr2) {
   });
 }
 
+function getModificationDate(date = new Date(Date.now())) {
+  const buffer = [date.getUTCFullYear().toString(), (date.getUTCMonth() + 1).toString().padStart(2, "0"), (date.getUTCDate() + 1).toString().padStart(2, "0"), date.getUTCHours().toString().padStart(2, "0"), date.getUTCMinutes().toString().padStart(2, "0"), date.getUTCSeconds().toString().padStart(2, "0")];
+  return buffer.join("");
+}
+
 function createPromiseCapability() {
   const capability = Object.create(null);
   let isSettled = false;

+ 520 - 50
lib/test/unit/annotation_spec.js

@@ -31,12 +31,21 @@ var _primitives = require("../../core/primitives.js");
 
 var _parser = require("../../core/parser.js");
 
+var _evaluator = require("../../core/evaluator.js");
+
 var _stream = require("../../core/stream.js");
 
+var _worker = require("../../core/worker.js");
+
 describe("annotation", function () {
   class PDFManagerMock {
     constructor(params) {
       this.docBaseUrl = params.docBaseUrl || null;
+      this.pdfDocument = {
+        catalog: {
+          acroForm: new _primitives.Dict()
+        }
+      };
     }
 
     ensure(obj, prop, args) {
@@ -51,19 +60,44 @@ describe("annotation", function () {
       });
     }
 
+    ensureCatalog(prop, args) {
+      return this.ensure(this.pdfDocument.catalog, prop, args);
+    }
+
+  }
+
+  function HandlerMock() {
+    this.inputs = [];
   }
 
-  let pdfManagerMock, idFactoryMock;
+  HandlerMock.prototype = {
+    send(name, data) {
+      this.inputs.push({
+        name,
+        data
+      });
+    }
+
+  };
+  let pdfManagerMock, idFactoryMock, partialEvaluator;
   beforeAll(function (done) {
     pdfManagerMock = new PDFManagerMock({
       docBaseUrl: null
     });
     idFactoryMock = (0, _test_utils.createIdFactory)(0);
+    partialEvaluator = new _evaluator.PartialEvaluator({
+      xref: new _test_utils.XRefMock(),
+      handler: new HandlerMock(),
+      pageIndex: 0,
+      idFactory: (0, _test_utils.createIdFactory)(0),
+      fontCache: new _primitives.RefSetCache()
+    });
     done();
   });
   afterAll(function () {
     pdfManagerMock = null;
     idFactoryMock = null;
+    partialEvaluator = null;
   });
   describe("AnnotationFactory", function () {
     it("should get id for annotation", function (done) {
@@ -1210,16 +1244,34 @@ describe("annotation", function () {
     });
   });
   describe("TextWidgetAnnotation", function () {
-    let textWidgetDict;
+    let textWidgetDict, fontRefObj;
     beforeEach(function (done) {
       textWidgetDict = new _primitives.Dict();
       textWidgetDict.set("Type", _primitives.Name.get("Annot"));
       textWidgetDict.set("Subtype", _primitives.Name.get("Widget"));
       textWidgetDict.set("FT", _primitives.Name.get("Tx"));
+      const helvDict = new _primitives.Dict();
+      helvDict.set("BaseFont", _primitives.Name.get("Helvetica"));
+      helvDict.set("Type", _primitives.Name.get("Font"));
+      helvDict.set("Subtype", _primitives.Name.get("Type1"));
+
+      const fontRef = _primitives.Ref.get(314, 0);
+
+      fontRefObj = {
+        ref: fontRef,
+        data: helvDict
+      };
+      const resourceDict = new _primitives.Dict();
+      const fontDict = new _primitives.Dict();
+      fontDict.set("Helv", fontRef);
+      resourceDict.set("Font", fontDict);
+      textWidgetDict.set("DA", "/Helv 5 Tf");
+      textWidgetDict.set("DR", resourceDict);
+      textWidgetDict.set("Rect", [0, 0, 32, 10]);
       done();
     });
     afterEach(function () {
-      textWidgetDict = null;
+      textWidgetDict = fontRefObj = null;
     });
     it("should handle unknown text alignment, maximum length and flags", function (done) {
       const textWidgetRef = _primitives.Ref.get(124, 0);
@@ -1357,6 +1409,164 @@ describe("annotation", function () {
 
       promise.then(done, done.fail);
     });
+    it("should render regular text for printing", function (done) {
+      const textWidgetRef = _primitives.Ref.get(271, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }, fontRefObj]);
+      const task = new _worker.WorkerTask("test print");
+      partialEvaluator.xref = xref;
+
+      _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const id = annotation.data.id;
+        const annotationStorage = {};
+        annotationStorage[id] = "test\\print";
+        return annotation._getAppearance(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(appearance => {
+        expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" + " 2.00 2.00 Td (test\\\\print) Tj ET Q EMC");
+        done();
+      }, done.fail);
+    });
+    it("should render auto-sized text for printing", function (done) {
+      textWidgetDict.set("DA", "/Helv 0 Tf");
+
+      const textWidgetRef = _primitives.Ref.get(271, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }, fontRefObj]);
+      const task = new _worker.WorkerTask("test print");
+      partialEvaluator.xref = xref;
+
+      _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const id = annotation.data.id;
+        const annotationStorage = {};
+        annotationStorage[id] = "test (print)";
+        return annotation._getAppearance(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(appearance => {
+        expect(appearance).toEqual("/Tx BMC q BT /Helv 11 Tf 1 0 0 1 0 0 Tm" + " 2.00 2.00 Td (test \\(print\\)) Tj ET Q EMC");
+        done();
+      }, done.fail);
+    });
+    it("should not render a password for printing", function (done) {
+      textWidgetDict.set("Ff", _util.AnnotationFieldFlag.PASSWORD);
+
+      const textWidgetRef = _primitives.Ref.get(271, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }, fontRefObj]);
+      const task = new _worker.WorkerTask("test print");
+      partialEvaluator.xref = xref;
+
+      _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const id = annotation.data.id;
+        const annotationStorage = {};
+        annotationStorage[id] = "mypassword";
+        return annotation._getAppearance(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(appearance => {
+        expect(appearance).toEqual(null);
+        done();
+      }, done.fail);
+    });
+    it("should render multiline text for printing", function (done) {
+      textWidgetDict.set("Ff", _util.AnnotationFieldFlag.MULTILINE);
+
+      const textWidgetRef = _primitives.Ref.get(271, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }, fontRefObj]);
+      const task = new _worker.WorkerTask("test print");
+      partialEvaluator.xref = xref;
+
+      _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const id = annotation.data.id;
+        const annotationStorage = {};
+        annotationStorage[id] = "a aa aaa aaaa aaaaa aaaaaa " + "pneumonoultramicroscopicsilicovolcanoconiosis";
+        return annotation._getAppearance(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(appearance => {
+        expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " + "2.00 -5.00 Td (a aa aaa ) Tj\n" + "0.00 -5.00 Td (aaaa aaaaa ) Tj\n" + "0.00 -5.00 Td (aaaaaa ) Tj\n" + "0.00 -5.00 Td (pneumonoultr) Tj\n" + "0.00 -5.00 Td (amicroscopi) Tj\n" + "0.00 -5.00 Td (csilicovolca) Tj\n" + "0.00 -5.00 Td (noconiosis) Tj ET Q EMC");
+        done();
+      }, done.fail);
+    });
+    it("should render multiline text with various EOL for printing", function (done) {
+      textWidgetDict.set("Ff", _util.AnnotationFieldFlag.MULTILINE);
+      textWidgetDict.set("Rect", [0, 0, 128, 10]);
+
+      const textWidgetRef = _primitives.Ref.get(271, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }, fontRefObj]);
+      const task = new _worker.WorkerTask("test print");
+      partialEvaluator.xref = xref;
+      const expectedAppearance = "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " + "2.00 -5.00 Td " + "(Lorem ipsum dolor sit amet, consectetur adipiscing elit.) Tj\n" + "0.00 -5.00 Td " + "(Aliquam vitae felis ac lectus bibendum ultricies quis non) Tj\n" + "0.00 -5.00 Td " + "( diam.) Tj\n" + "0.00 -5.00 Td " + "(Morbi id porttitor quam, a iaculis dui.) Tj\n" + "0.00 -5.00 Td " + "(Pellentesque habitant morbi tristique senectus et netus ) Tj\n" + "0.00 -5.00 Td " + "(et malesuada fames ac turpis egestas.) Tj\n" + "0.00 -5.00 Td () Tj\n" + "0.00 -5.00 Td () Tj\n" + "0.00 -5.00 Td " + "(Nulla consectetur, ligula in tincidunt placerat, velit ) Tj\n" + "0.00 -5.00 Td " + "(augue consectetur orci, sed mattis libero nunc ut massa.) Tj\n" + "0.00 -5.00 Td " + "(Etiam facilisis tempus interdum.) Tj ET Q EMC";
+
+      _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const id = annotation.data.id;
+        const annotationStorage = {};
+        annotationStorage[id] = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\r" + "Aliquam vitae felis ac lectus bibendum ultricies quis non diam.\n" + "Morbi id porttitor quam, a iaculis dui.\r\n" + "Pellentesque habitant morbi tristique senectus et " + "netus et malesuada fames ac turpis egestas.\n\r\n\r" + "Nulla consectetur, ligula in tincidunt placerat, " + "velit augue consectetur orci, sed mattis libero nunc ut massa.\r" + "Etiam facilisis tempus interdum.";
+        return annotation._getAppearance(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(appearance => {
+        expect(appearance).toEqual(expectedAppearance);
+        done();
+      }, done.fail);
+    });
+    it("should render comb for printing", function (done) {
+      textWidgetDict.set("Ff", _util.AnnotationFieldFlag.COMB);
+      textWidgetDict.set("MaxLen", 4);
+
+      const textWidgetRef = _primitives.Ref.get(271, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }, fontRefObj]);
+      const task = new _worker.WorkerTask("test print");
+      partialEvaluator.xref = xref;
+
+      _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const id = annotation.data.id;
+        const annotationStorage = {};
+        annotationStorage[id] = "aa(aa)a\\";
+        return annotation._getAppearance(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(appearance => {
+        expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 2 Tm" + " (a) Tj 8.00 0 Td (a) Tj 8.00 0 Td (\\() Tj" + " 8.00 0 Td (a) Tj 8.00 0 Td (a) Tj" + " 8.00 0 Td (\\)) Tj 8.00 0 Td (a) Tj" + " 8.00 0 Td (\\\\) Tj ET Q EMC");
+        done();
+      }, done.fail);
+    });
+    it("should save text", function (done) {
+      const textWidgetRef = _primitives.Ref.get(123, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }]);
+      partialEvaluator.xref = xref;
+      const task = new _worker.WorkerTask("test save");
+
+      _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = "hello world";
+        return annotation.save(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(data => {
+        expect(data.length).toEqual(2);
+        const [oldData, newData] = data;
+        expect(oldData.ref).toEqual(_primitives.Ref.get(123, 0));
+        expect(newData.ref).toEqual(_primitives.Ref.get(1, 0));
+        oldData.data = oldData.data.replace(/\(D:[0-9]+\)/, "(date)");
+        expect(oldData.data).toEqual("123 0 obj\n" + "<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " + "<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " + "/V (hello world) /AP << /N 1 0 R>> /M (date)>>\nendobj\n");
+        expect(newData.data).toEqual("1 0 obj\n<< /Length 77 /Subtype /Form /Resources " + "<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10]>> stream\n" + "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 2.00 Td (hello world) Tj " + "ET Q EMC\nendstream\nendobj\n");
+        done();
+      }, done.fail);
+    });
   });
   describe("ButtonWidgetAnnotation", function () {
     let buttonWidgetDict;
@@ -1373,10 +1583,10 @@ describe("annotation", function () {
     it("should handle checkboxes with export value", function (done) {
       buttonWidgetDict.set("V", _primitives.Name.get("1"));
       const appearanceStatesDict = new _primitives.Dict();
-      const exportValueOptionsDict = new _primitives.Dict();
-      exportValueOptionsDict.set("Off", 0);
-      exportValueOptionsDict.set("Checked", 1);
-      appearanceStatesDict.set("D", exportValueOptionsDict);
+      const normalAppearanceDict = new _primitives.Dict();
+      normalAppearanceDict.set("Off", 0);
+      normalAppearanceDict.set("Checked", 1);
+      appearanceStatesDict.set("N", normalAppearanceDict);
       buttonWidgetDict.set("AP", appearanceStatesDict);
 
       const buttonWidgetRef = _primitives.Ref.get(124, 0);
@@ -1417,6 +1627,113 @@ describe("annotation", function () {
         done();
       }, done.fail);
     });
+    it("should handle checkboxes without /Off appearance", function (done) {
+      buttonWidgetDict.set("V", _primitives.Name.get("1"));
+      const appearanceStatesDict = new _primitives.Dict();
+      const normalAppearanceDict = new _primitives.Dict();
+      normalAppearanceDict.set("Checked", 1);
+      appearanceStatesDict.set("N", normalAppearanceDict);
+      buttonWidgetDict.set("AP", appearanceStatesDict);
+
+      const buttonWidgetRef = _primitives.Ref.get(124, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: buttonWidgetRef,
+        data: buttonWidgetDict
+      }]);
+
+      _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock).then(({
+        data
+      }) => {
+        expect(data.annotationType).toEqual(_util.AnnotationType.WIDGET);
+        expect(data.checkBox).toEqual(true);
+        expect(data.fieldValue).toEqual("1");
+        expect(data.radioButton).toEqual(false);
+        expect(data.exportValue).toEqual("Checked");
+        done();
+      }, done.fail);
+    });
+    it("should render checkboxes for printing", function (done) {
+      const appearanceStatesDict = new _primitives.Dict();
+      const normalAppearanceDict = new _primitives.Dict();
+      const checkedAppearanceDict = new _primitives.Dict();
+      const uncheckedAppearanceDict = new _primitives.Dict();
+      const checkedStream = new _stream.StringStream("0.1 0.2 0.3 rg");
+      checkedStream.dict = checkedAppearanceDict;
+      const uncheckedStream = new _stream.StringStream("0.3 0.2 0.1 rg");
+      uncheckedStream.dict = uncheckedAppearanceDict;
+      checkedAppearanceDict.set("BBox", [0, 0, 8, 8]);
+      checkedAppearanceDict.set("FormType", 1);
+      checkedAppearanceDict.set("Matrix", [1, 0, 0, 1, 0, 0]);
+      normalAppearanceDict.set("Checked", checkedStream);
+      normalAppearanceDict.set("Off", uncheckedStream);
+      appearanceStatesDict.set("N", normalAppearanceDict);
+      buttonWidgetDict.set("AP", appearanceStatesDict);
+
+      const buttonWidgetRef = _primitives.Ref.get(124, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: buttonWidgetRef,
+        data: buttonWidgetDict
+      }]);
+      const task = new _worker.WorkerTask("test print");
+
+      _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = true;
+        return Promise.all([annotation, annotation.getOperatorList(partialEvaluator, task, false, annotationStorage)]);
+      }, done.fail).then(([annotation, opList]) => {
+        expect(opList.argsArray.length).toEqual(3);
+        expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+        expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
+        return annotation;
+      }, done.fail).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = false;
+        return annotation.getOperatorList(partialEvaluator, task, false, annotationStorage);
+      }, done.fail).then(opList => {
+        expect(opList.argsArray.length).toEqual(3);
+        expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+        expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
+        done();
+      }, done.fail);
+    });
+    it("should save checkboxes", function (done) {
+      const appearanceStatesDict = new _primitives.Dict();
+      const normalAppearanceDict = new _primitives.Dict();
+      normalAppearanceDict.set("Checked", _primitives.Ref.get(314, 0));
+      normalAppearanceDict.set("Off", _primitives.Ref.get(271, 0));
+      appearanceStatesDict.set("N", normalAppearanceDict);
+      buttonWidgetDict.set("AP", appearanceStatesDict);
+      buttonWidgetDict.set("V", _primitives.Name.get("Off"));
+
+      const buttonWidgetRef = _primitives.Ref.get(123, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: buttonWidgetRef,
+        data: buttonWidgetDict
+      }]);
+      partialEvaluator.xref = xref;
+      const task = new _worker.WorkerTask("test save");
+
+      _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = true;
+        return Promise.all([annotation, annotation.save(partialEvaluator, task, annotationStorage)]);
+      }, done.fail).then(([annotation, [oldData]]) => {
+        oldData.data = oldData.data.replace(/\(D:[0-9]+\)/, "(date)");
+        expect(oldData.ref).toEqual(_primitives.Ref.get(123, 0));
+        expect(oldData.data).toEqual("123 0 obj\n" + "<< /Type /Annot /Subtype /Widget /FT /Btn " + "/AP << /N << /Checked 314 0 R /Off 271 0 R>>>> " + "/V /Checked /AS /Checked /M (date)>>\nendobj\n");
+        return annotation;
+      }, done.fail).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = false;
+        return annotation.save(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(data => {
+        expect(data).toEqual(null);
+        done();
+      }, done.fail);
+    });
     it("should handle radio buttons with a field value", function (done) {
       const parentDict = new _primitives.Dict();
       parentDict.set("V", _primitives.Name.get("1"));
@@ -1472,18 +1789,133 @@ describe("annotation", function () {
         done();
       }, done.fail);
     });
+    it("should render radio buttons for printing", function (done) {
+      const appearanceStatesDict = new _primitives.Dict();
+      const normalAppearanceDict = new _primitives.Dict();
+      const checkedAppearanceDict = new _primitives.Dict();
+      const uncheckedAppearanceDict = new _primitives.Dict();
+      const checkedStream = new _stream.StringStream("0.1 0.2 0.3 rg");
+      checkedStream.dict = checkedAppearanceDict;
+      const uncheckedStream = new _stream.StringStream("0.3 0.2 0.1 rg");
+      uncheckedStream.dict = uncheckedAppearanceDict;
+      checkedAppearanceDict.set("BBox", [0, 0, 8, 8]);
+      checkedAppearanceDict.set("FormType", 1);
+      checkedAppearanceDict.set("Matrix", [1, 0, 0, 1, 0, 0]);
+      normalAppearanceDict.set("Checked", checkedStream);
+      normalAppearanceDict.set("Off", uncheckedStream);
+      appearanceStatesDict.set("N", normalAppearanceDict);
+      buttonWidgetDict.set("Ff", _util.AnnotationFieldFlag.RADIO);
+      buttonWidgetDict.set("AP", appearanceStatesDict);
+
+      const buttonWidgetRef = _primitives.Ref.get(124, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: buttonWidgetRef,
+        data: buttonWidgetDict
+      }]);
+      const task = new _worker.WorkerTask("test print");
+
+      _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = true;
+        return Promise.all([annotation, annotation.getOperatorList(partialEvaluator, task, false, annotationStorage)]);
+      }, done.fail).then(([annotation, opList]) => {
+        expect(opList.argsArray.length).toEqual(3);
+        expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+        expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
+        return annotation;
+      }, done.fail).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = false;
+        return annotation.getOperatorList(partialEvaluator, task, false, annotationStorage);
+      }, done.fail).then(opList => {
+        expect(opList.argsArray.length).toEqual(3);
+        expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+        expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
+        done();
+      }, done.fail);
+    });
+    it("should save radio buttons", function (done) {
+      const appearanceStatesDict = new _primitives.Dict();
+      const normalAppearanceDict = new _primitives.Dict();
+      normalAppearanceDict.set("Checked", _primitives.Ref.get(314, 0));
+      normalAppearanceDict.set("Off", _primitives.Ref.get(271, 0));
+      appearanceStatesDict.set("N", normalAppearanceDict);
+      buttonWidgetDict.set("Ff", _util.AnnotationFieldFlag.RADIO);
+      buttonWidgetDict.set("AP", appearanceStatesDict);
+
+      const buttonWidgetRef = _primitives.Ref.get(123, 0);
+
+      const parentRef = _primitives.Ref.get(456, 0);
+
+      const parentDict = new _primitives.Dict();
+      parentDict.set("V", _primitives.Name.get("Off"));
+      parentDict.set("Kids", [buttonWidgetRef]);
+      buttonWidgetDict.set("Parent", parentRef);
+      const xref = new _test_utils.XRefMock([{
+        ref: buttonWidgetRef,
+        data: buttonWidgetDict
+      }, {
+        ref: parentRef,
+        data: parentDict
+      }]);
+      parentDict.xref = xref;
+      buttonWidgetDict.xref = xref;
+      partialEvaluator.xref = xref;
+      const task = new _worker.WorkerTask("test save");
+
+      _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = true;
+        return Promise.all([annotation, annotation.save(partialEvaluator, task, annotationStorage)]);
+      }, done.fail).then(([annotation, data]) => {
+        expect(data.length).toEqual(2);
+        const [radioData, parentData] = data;
+        radioData.data = radioData.data.replace(/\(D:[0-9]+\)/, "(date)");
+        expect(radioData.ref).toEqual(_primitives.Ref.get(123, 0));
+        expect(radioData.data).toEqual("123 0 obj\n" + "<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 " + "/AP << /N << /Checked 314 0 R /Off 271 0 R>>>> " + "/Parent 456 0 R /AS /Checked /M (date)>>\nendobj\n");
+        expect(parentData.ref).toEqual(_primitives.Ref.get(456, 0));
+        expect(parentData.data).toEqual("456 0 obj\n<< /V /Checked /Kids [123 0 R]>>\nendobj\n");
+        return annotation;
+      }, done.fail).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = false;
+        return annotation.save(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(data => {
+        expect(data).toEqual(null);
+        done();
+      }, done.fail);
+    });
   });
   describe("ChoiceWidgetAnnotation", function () {
-    let choiceWidgetDict;
+    let choiceWidgetDict, fontRefObj;
     beforeEach(function (done) {
       choiceWidgetDict = new _primitives.Dict();
       choiceWidgetDict.set("Type", _primitives.Name.get("Annot"));
       choiceWidgetDict.set("Subtype", _primitives.Name.get("Widget"));
       choiceWidgetDict.set("FT", _primitives.Name.get("Ch"));
+      const helvDict = new _primitives.Dict();
+      helvDict.set("BaseFont", _primitives.Name.get("Helvetica"));
+      helvDict.set("Type", _primitives.Name.get("Font"));
+      helvDict.set("Subtype", _primitives.Name.get("Type1"));
+
+      const fontRef = _primitives.Ref.get(314, 0);
+
+      fontRefObj = {
+        ref: fontRef,
+        data: helvDict
+      };
+      const resourceDict = new _primitives.Dict();
+      const fontDict = new _primitives.Dict();
+      fontDict.set("Helv", fontRef);
+      resourceDict.set("Font", fontDict);
+      choiceWidgetDict.set("DA", "/Helv 5 Tf");
+      choiceWidgetDict.set("DR", resourceDict);
+      choiceWidgetDict.set("Rect", [0, 0, 32, 10]);
       done();
     });
     afterEach(function () {
-      choiceWidgetDict = null;
+      choiceWidgetDict = fontRefObj = null;
     });
     it("should handle missing option arrays", function (done) {
       const choiceWidgetRef = _primitives.Ref.get(122, 0);
@@ -1600,13 +2032,11 @@ describe("annotation", function () {
         done();
       }, done.fail);
     });
-    it("should sanitize display values in option arrays (issue 8947)", function (done) {
-      const options = ["\xFE\xFF\x00F\x00o\x00o"];
-      const expected = [{
-        exportValue: "\xFE\xFF\x00F\x00o\x00o",
-        displayValue: "Foo"
-      }];
-      choiceWidgetDict.set("Opt", options);
+    it("should decode form values", function (done) {
+      const encodedString = "\xFE\xFF\x00F\x00o\x00o";
+      const decodedString = "Foo";
+      choiceWidgetDict.set("Opt", [encodedString]);
+      choiceWidgetDict.set("V", encodedString);
 
       const choiceWidgetRef = _primitives.Ref.get(984, 0);
 
@@ -1619,47 +2049,39 @@ describe("annotation", function () {
         data
       }) => {
         expect(data.annotationType).toEqual(_util.AnnotationType.WIDGET);
-        expect(data.options).toEqual(expected);
+        expect(data.fieldValue).toEqual([decodedString]);
+        expect(data.options).toEqual([{
+          exportValue: decodedString,
+          displayValue: decodedString
+        }]);
         done();
       }, done.fail);
     });
-    it("should handle array field values", function (done) {
-      const fieldValue = ["Foo", "Bar"];
-      choiceWidgetDict.set("V", fieldValue);
-
-      const choiceWidgetRef = _primitives.Ref.get(968, 0);
-
-      const xref = new _test_utils.XRefMock([{
-        ref: choiceWidgetRef,
-        data: choiceWidgetDict
-      }]);
+    it("should convert the field value to an array", function (done) {
+      const inputs = [null, "Foo", ["Foo", "Bar"]];
+      const outputs = [[], ["Foo"], ["Foo", "Bar"]];
+      let promise = Promise.resolve();
 
-      _annotation.AnnotationFactory.create(xref, choiceWidgetRef, pdfManagerMock, idFactoryMock).then(({
-        data
-      }) => {
-        expect(data.annotationType).toEqual(_util.AnnotationType.WIDGET);
-        expect(data.fieldValue).toEqual(fieldValue);
-        done();
-      }, done.fail);
-    });
-    it("should handle string field values", function (done) {
-      const fieldValue = "Foo";
-      choiceWidgetDict.set("V", fieldValue);
+      for (let i = 0, ii = inputs.length; i < ii; i++) {
+        promise = promise.then(() => {
+          choiceWidgetDict.set("V", inputs[i]);
 
-      const choiceWidgetRef = _primitives.Ref.get(978, 0);
+          const choiceWidgetRef = _primitives.Ref.get(968, 0);
 
-      const xref = new _test_utils.XRefMock([{
-        ref: choiceWidgetRef,
-        data: choiceWidgetDict
-      }]);
+          const xref = new _test_utils.XRefMock([{
+            ref: choiceWidgetRef,
+            data: choiceWidgetDict
+          }]);
+          return _annotation.AnnotationFactory.create(xref, choiceWidgetRef, pdfManagerMock, idFactoryMock).then(({
+            data
+          }) => {
+            expect(data.annotationType).toEqual(_util.AnnotationType.WIDGET);
+            expect(data.fieldValue).toEqual(outputs[i]);
+          });
+        });
+      }
 
-      _annotation.AnnotationFactory.create(xref, choiceWidgetRef, pdfManagerMock, idFactoryMock).then(({
-        data
-      }) => {
-        expect(data.annotationType).toEqual(_util.AnnotationType.WIDGET);
-        expect(data.fieldValue).toEqual([fieldValue]);
-        done();
-      }, done.fail);
+      promise.then(done, done.fail);
     });
     it("should handle unknown flags", function (done) {
       const choiceWidgetRef = _primitives.Ref.get(166, 0);
@@ -1719,6 +2141,54 @@ describe("annotation", function () {
         done();
       }, done.fail);
     });
+    it("should render choice for printing", function (done) {
+      const choiceWidgetRef = _primitives.Ref.get(271, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: choiceWidgetRef,
+        data: choiceWidgetDict
+      }, fontRefObj]);
+      const task = new _worker.WorkerTask("test print");
+      partialEvaluator.xref = xref;
+
+      _annotation.AnnotationFactory.create(xref, choiceWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const id = annotation.data.id;
+        const annotationStorage = {};
+        annotationStorage[id] = "a value";
+        return annotation._getAppearance(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(appearance => {
+        expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" + " 2.00 2.00 Td (a value) Tj ET Q EMC");
+        done();
+      }, done.fail);
+    });
+    it("should save choice", function (done) {
+      choiceWidgetDict.set("Opt", ["A", "B", "C"]);
+      choiceWidgetDict.set("V", "A");
+
+      const choiceWidgetRef = _primitives.Ref.get(123, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: choiceWidgetRef,
+        data: choiceWidgetDict
+      }]);
+      partialEvaluator.xref = xref;
+      const task = new _worker.WorkerTask("test save");
+
+      _annotation.AnnotationFactory.create(xref, choiceWidgetRef, pdfManagerMock, idFactoryMock).then(annotation => {
+        const annotationStorage = {};
+        annotationStorage[annotation.data.id] = "C";
+        return annotation.save(partialEvaluator, task, annotationStorage);
+      }, done.fail).then(data => {
+        expect(data.length).toEqual(2);
+        const [oldData, newData] = data;
+        expect(oldData.ref).toEqual(_primitives.Ref.get(123, 0));
+        expect(newData.ref).toEqual(_primitives.Ref.get(1, 0));
+        oldData.data = oldData.data.replace(/\(D:[0-9]+\)/, "(date)");
+        expect(oldData.data).toEqual("123 0 obj\n" + "<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " + "<< /Font << /Helv 314 0 R>>>> " + "/Rect [0 0 32 10] /Opt [(A) (B) (C)] /V (C) " + "/AP << /N 1 0 R>> /M (date)>>\nendobj\n");
+        expect(newData.data).toEqual("1 0 obj\n" + "<< /Length 67 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10]>> stream\n" + "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 2.00 Td (C) Tj ET Q EMC\n" + "endstream\nendobj\n");
+        done();
+      }, done.fail);
+    });
   });
   describe("LineAnnotation", function () {
     it("should set the line coordinates", function (done) {

+ 83 - 0
lib/test/unit/annotation_storage_spec.js

@@ -0,0 +1,83 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+var _annotation_storage = require("../../display/annotation_storage.js");
+
+describe("AnnotationStorage", function () {
+  describe("GetOrCreateValue", function () {
+    it("should get and set a new value in the annotation storage", function (done) {
+      const annotationStorage = new _annotation_storage.AnnotationStorage();
+      let value = annotationStorage.getOrCreateValue("123A", "hello world");
+      expect(value).toEqual("hello world");
+      value = annotationStorage.getOrCreateValue("123A", "an other string");
+      expect(value).toEqual("hello world");
+      done();
+    });
+  });
+  describe("SetValue", function () {
+    it("should set a new value in the annotation storage", function (done) {
+      const annotationStorage = new _annotation_storage.AnnotationStorage();
+      annotationStorage.setValue("123A", "an other string");
+      const value = annotationStorage.getAll()["123A"];
+      expect(value).toEqual("an other string");
+      done();
+    });
+    it("should call onSetModified() if value is changed", function (done) {
+      const annotationStorage = new _annotation_storage.AnnotationStorage();
+      let called = false;
+
+      const callback = function () {
+        called = true;
+      };
+
+      annotationStorage.onSetModified = callback;
+      annotationStorage.getOrCreateValue("asdf", "original");
+      expect(called).toBe(false);
+      annotationStorage.setValue("asdf", "original");
+      expect(called).toBe(false);
+      annotationStorage.setValue("asdf", "modified");
+      expect(called).toBe(true);
+      done();
+    });
+  });
+  describe("ResetModified", function () {
+    it("should call onResetModified() if set", function (done) {
+      const annotationStorage = new _annotation_storage.AnnotationStorage();
+      let called = false;
+
+      const callback = function () {
+        called = true;
+      };
+
+      annotationStorage.onResetModified = callback;
+      annotationStorage.getOrCreateValue("asdf", "original");
+      annotationStorage.setValue("asdf", "original");
+      annotationStorage.resetModified();
+      expect(called).toBe(false);
+      annotationStorage.setValue("asdf", "modified");
+      annotationStorage.resetModified();
+      expect(called).toBe(true);
+      done();
+    });
+  });
+});

+ 16 - 6
lib/test/unit/api_spec.js

@@ -39,6 +39,8 @@ var _is_node = require("../../shared/is_node.js");
 
 var _metadata = require("../../display/metadata.js");
 
+var _node_utils = require("../../display/node_utils.js");
+
 describe("api", function () {
   const basicApiFileName = "basicapi.pdf";
   const basicApiFileLength = 105779;
@@ -46,7 +48,7 @@ describe("api", function () {
   let CanvasFactory;
   beforeAll(function (done) {
     if (_is_node.isNodeJS) {
-      CanvasFactory = new _test_utils.NodeCanvasFactory();
+      CanvasFactory = new _node_utils.NodeCanvasFactory();
     } else {
       CanvasFactory = new _display_utils.DOMCanvasFactory();
     }
@@ -1240,8 +1242,8 @@ describe("api", function () {
       const result1 = loadingTask1.promise.then(pdfDoc => {
         return pdfDoc.getPage(1).then(pdfPage => {
           return pdfPage.getOperatorList().then(opList => {
-            expect(opList.fnArray.length).toEqual(722);
-            expect(opList.argsArray.length).toEqual(722);
+            expect(opList.fnArray.length).toBeGreaterThan(100);
+            expect(opList.argsArray.length).toBeGreaterThan(100);
             expect(opList.lastChunk).toEqual(true);
             return loadingTask1.destroy();
           });
@@ -1386,6 +1388,7 @@ describe("api", function () {
       }, done.fail);
     });
     it("multiple render() on the same canvas", function (done) {
+      const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
       var viewport = page.getViewport({
         scale: 1
       });
@@ -1393,12 +1396,14 @@ describe("api", function () {
       var renderTask1 = page.render({
         canvasContext: canvasAndCtx.context,
         canvasFactory: CanvasFactory,
-        viewport
+        viewport,
+        optionalContentConfigPromise
       });
       var renderTask2 = page.render({
         canvasContext: canvasAndCtx.context,
         canvasFactory: CanvasFactory,
-        viewport
+        viewport,
+        optionalContentConfigPromise
       });
       Promise.all([renderTask1.promise, renderTask2.promise.then(() => {
         done.fail("shall fail rendering");
@@ -1444,7 +1449,12 @@ describe("api", function () {
             canvasFactory: CanvasFactory,
             viewport
           });
-          pdfDoc.cleanup().then(() => {
+
+          renderTask.onContinue = function (cont) {
+            waitSome(cont);
+          };
+
+          return pdfDoc.cleanup().then(() => {
             throw new Error("shall fail cleanup");
           }, reason => {
             expect(reason instanceof Error).toEqual(true);

+ 4 - 4
lib/test/unit/cmap_spec.js

@@ -29,7 +29,7 @@ var _is_node = require("../../shared/is_node.js");
 
 var _primitives = require("../../core/primitives.js");
 
-var _test_utils = require("./test_utils.js");
+var _node_utils = require("../../display/node_utils.js");
 
 var _stream = require("../../core/stream.js");
 
@@ -44,7 +44,7 @@ describe("cmap", function () {
     var CMapReaderFactory;
 
     if (_is_node.isNodeJS) {
-      CMapReaderFactory = new _test_utils.NodeCMapReaderFactory({
+      CMapReaderFactory = new _node_utils.NodeCMapReaderFactory({
         baseUrl: cMapUrl.node,
         isCompressed: cMapPacked
       });
@@ -297,7 +297,7 @@ describe("cmap", function () {
   });
   it("attempts to load a built-in CMap without the necessary API parameters", function (done) {
     function tmpFetchBuiltInCMap(name) {
-      var CMapReaderFactory = _is_node.isNodeJS ? new _test_utils.NodeCMapReaderFactory({}) : new _display_utils.DOMCMapReaderFactory({});
+      var CMapReaderFactory = _is_node.isNodeJS ? new _node_utils.NodeCMapReaderFactory({}) : new _display_utils.DOMCMapReaderFactory({});
       return CMapReaderFactory.fetch({
         name
       });
@@ -322,7 +322,7 @@ describe("cmap", function () {
       let CMapReaderFactory;
 
       if (_is_node.isNodeJS) {
-        CMapReaderFactory = new _test_utils.NodeCMapReaderFactory({
+        CMapReaderFactory = new _node_utils.NodeCMapReaderFactory({
           baseUrl: cMapUrl.node,
           isCompressed: false
         });

+ 216 - 24
lib/test/unit/colorspace_spec.js

@@ -27,12 +27,14 @@ var _stream = require("../../core/stream.js");
 
 var _colorspace = require("../../core/colorspace.js");
 
+var _image_utils = require("../../core/image_utils.js");
+
 var _function = require("../../core/function.js");
 
 var _test_utils = require("./test_utils.js");
 
 describe("colorspace", function () {
-  describe("ColorSpace", function () {
+  describe("ColorSpace.isDefaultDecode", function () {
     it("should be true if decode is not an array", function () {
       expect(_colorspace.ColorSpace.isDefaultDecode("string", 0)).toBeTruthy();
     });
@@ -50,6 +52,130 @@ describe("colorspace", function () {
       expect(_colorspace.ColorSpace.isDefaultDecode([1, 0, 0, 1, 0, 1, 0, 1], 4)).toBeFalsy();
     });
   });
+  describe("ColorSpace caching", function () {
+    let localColorSpaceCache = null;
+    beforeAll(function (done) {
+      localColorSpaceCache = new _image_utils.LocalColorSpaceCache();
+      done();
+    });
+    afterAll(function (done) {
+      localColorSpaceCache = null;
+      done();
+    });
+    it("caching by Name", function () {
+      const xref = new _test_utils.XRefMock();
+      const pdfFunctionFactory = new _function.PDFFunctionFactory({
+        xref
+      });
+
+      const colorSpace1 = _colorspace.ColorSpace.parse({
+        cs: _primitives.Name.get("Pattern"),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache
+      });
+
+      expect(colorSpace1.name).toEqual("Pattern");
+
+      const colorSpace2 = _colorspace.ColorSpace.parse({
+        cs: _primitives.Name.get("Pattern"),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache
+      });
+
+      expect(colorSpace2.name).toEqual("Pattern");
+
+      const colorSpaceNonCached = _colorspace.ColorSpace.parse({
+        cs: _primitives.Name.get("Pattern"),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
+
+      expect(colorSpaceNonCached.name).toEqual("Pattern");
+
+      const colorSpaceOther = _colorspace.ColorSpace.parse({
+        cs: _primitives.Name.get("RGB"),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache
+      });
+
+      expect(colorSpaceOther.name).toEqual("DeviceRGB");
+      expect(colorSpace1).toBe(colorSpace2);
+      expect(colorSpace1).not.toBe(colorSpaceNonCached);
+      expect(colorSpace1).not.toBe(colorSpaceOther);
+    });
+    it("caching by Ref", function () {
+      const paramsCalGray = new _primitives.Dict();
+      paramsCalGray.set("WhitePoint", [1, 1, 1]);
+      paramsCalGray.set("BlackPoint", [0, 0, 0]);
+      paramsCalGray.set("Gamma", 2.0);
+      const paramsCalRGB = new _primitives.Dict();
+      paramsCalRGB.set("WhitePoint", [1, 1, 1]);
+      paramsCalRGB.set("BlackPoint", [0, 0, 0]);
+      paramsCalRGB.set("Gamma", [1, 1, 1]);
+      paramsCalRGB.set("Matrix", [1, 0, 0, 0, 1, 0, 0, 0, 1]);
+      const xref = new _test_utils.XRefMock([{
+        ref: _primitives.Ref.get(50, 0),
+        data: [_primitives.Name.get("CalGray"), paramsCalGray]
+      }, {
+        ref: _primitives.Ref.get(100, 0),
+        data: [_primitives.Name.get("CalRGB"), paramsCalRGB]
+      }]);
+      const pdfFunctionFactory = new _function.PDFFunctionFactory({
+        xref
+      });
+
+      const colorSpace1 = _colorspace.ColorSpace.parse({
+        cs: _primitives.Ref.get(50, 0),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache
+      });
+
+      expect(colorSpace1.name).toEqual("CalGray");
+
+      const colorSpace2 = _colorspace.ColorSpace.parse({
+        cs: _primitives.Ref.get(50, 0),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache
+      });
+
+      expect(colorSpace2.name).toEqual("CalGray");
+
+      const colorSpaceNonCached = _colorspace.ColorSpace.parse({
+        cs: _primitives.Ref.get(50, 0),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
+
+      expect(colorSpaceNonCached.name).toEqual("CalGray");
+
+      const colorSpaceOther = _colorspace.ColorSpace.parse({
+        cs: _primitives.Ref.get(100, 0),
+        xref,
+        resources: null,
+        pdfFunctionFactory,
+        localColorSpaceCache
+      });
+
+      expect(colorSpaceOther.name).toEqual("CalRGB");
+      expect(colorSpace1).toBe(colorSpace2);
+      expect(colorSpace1).not.toBe(colorSpaceNonCached);
+      expect(colorSpace1).not.toBe(colorSpaceOther);
+    });
+  });
   describe("DeviceGrayCS", function () {
     it("should handle the case when cs is a Name object", function () {
       const cs = _primitives.Name.get("DeviceGray");
@@ -58,12 +184,18 @@ describe("colorspace", function () {
         ref: _primitives.Ref.get(10, 0),
         data: new _primitives.Dict()
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 131]);
       const testDest = new Uint8ClampedArray(4 * 4 * 3);
@@ -81,12 +213,18 @@ describe("colorspace", function () {
         ref: cs,
         data: _primitives.Name.get("DeviceGray")
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 131]);
       const testDest = new Uint8ClampedArray(3 * 3 * 3);
@@ -106,12 +244,18 @@ describe("colorspace", function () {
         ref: _primitives.Ref.get(10, 0),
         data: new _primitives.Dict()
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 131, 139, 140, 111, 25, 198, 21, 147, 255]);
       const testDest = new Uint8ClampedArray(4 * 4 * 3);
@@ -129,12 +273,18 @@ describe("colorspace", function () {
         ref: cs,
         data: _primitives.Name.get("DeviceRGB")
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 131, 139, 140, 111, 25, 198, 21, 147, 255]);
       const testDest = new Uint8ClampedArray(3 * 3 * 3);
@@ -154,12 +304,18 @@ describe("colorspace", function () {
         ref: _primitives.Ref.get(10, 0),
         data: new _primitives.Dict()
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 128, 131, 139, 140, 45, 111, 25, 198, 78, 21, 147, 255, 69]);
       const testDest = new Uint8ClampedArray(4 * 4 * 3);
@@ -177,12 +333,18 @@ describe("colorspace", function () {
         ref: cs,
         data: _primitives.Name.get("DeviceCMYK")
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 128, 131, 139, 140, 45, 111, 25, 198, 78, 21, 147, 255, 69]);
       const testDest = new Uint8ClampedArray(3 * 3 * 3);
@@ -205,12 +367,18 @@ describe("colorspace", function () {
         ref: _primitives.Ref.get(10, 0),
         data: new _primitives.Dict()
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 131]);
       const testDest = new Uint8ClampedArray(4 * 4 * 3);
@@ -234,12 +402,18 @@ describe("colorspace", function () {
         ref: _primitives.Ref.get(10, 0),
         data: new _primitives.Dict()
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 125, 250, 131, 139, 140, 111, 25, 198, 21, 147, 255]);
       const testDest = new Uint8ClampedArray(3 * 3 * 3);
@@ -262,12 +436,18 @@ describe("colorspace", function () {
         ref: _primitives.Ref.get(10, 0),
         data: new _primitives.Dict()
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 25, 50, 31, 19, 40, 11, 25, 98, 21, 47, 55]);
       const testDest = new Uint8ClampedArray(3 * 3 * 3);
@@ -282,18 +462,24 @@ describe("colorspace", function () {
   });
   describe("IndexedCS", function () {
     it("should handle the case when cs is an array", function () {
-      const lookup = new Uint8Array([23, 155, 35, 147, 69, 93, 255, 109, 70]);
+      const lookup = new _stream.Stream(new Uint8Array([23, 155, 35, 147, 69, 93, 255, 109, 70]));
       const cs = [_primitives.Name.get("Indexed"), _primitives.Name.get("DeviceRGB"), 2, lookup];
       const xref = new _test_utils.XRefMock([{
         ref: _primitives.Ref.get(10, 0),
         data: new _primitives.Dict()
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([2, 2, 0, 1]);
       const testDest = new Uint8ClampedArray(3 * 3 * 3);
@@ -322,12 +508,18 @@ describe("colorspace", function () {
         ref: fnRef,
         data: fn
       }]);
-      const res = new _primitives.Dict();
+      const resources = new _primitives.Dict();
       const pdfFunctionFactory = new _function.PDFFunctionFactory({
         xref
       });
 
-      const colorSpace = _colorspace.ColorSpace.parse(cs, xref, res, pdfFunctionFactory);
+      const colorSpace = _colorspace.ColorSpace.parse({
+        cs,
+        xref,
+        resources,
+        pdfFunctionFactory,
+        localColorSpaceCache: new _image_utils.LocalColorSpaceCache()
+      });
 
       const testSrc = new Uint8Array([27, 25, 50, 31]);
       const testDest = new Uint8ClampedArray(3 * 3 * 3);

+ 51 - 4
lib/test/unit/crypto_spec.js

@@ -406,7 +406,15 @@ describe("CipherTransformFactory", function () {
     done.fail("Password should be rejected.");
   }
 
-  var fileId1, fileId2, dict1, dict2;
+  function ensureEncryptDecryptIsIdentity(dict, fileId, password, string) {
+    const factory = new _crypto.CipherTransformFactory(dict, fileId, password);
+    const cipher = factory.createCipherTransform(123, 0);
+    const encrypted = cipher.encryptString(string);
+    const decrypted = cipher.decryptString(encrypted);
+    expect(string).toEqual(decrypted);
+  }
+
+  var fileId1, fileId2, dict1, dict2, dict3;
   var aes256Dict, aes256IsoDict, aes256BlankDict, aes256IsoBlankDict;
   beforeAll(function (done) {
     fileId1 = unescape("%F6%C6%AF%17%F3rR%8DRM%9A%80%D1%EF%DF%18");
@@ -429,7 +437,7 @@ describe("CipherTransformFactory", function () {
       P: -1084,
       R: 4
     });
-    aes256Dict = buildDict({
+    dict3 = {
       Filter: _primitives.Name.get("Standard"),
       V: 5,
       Length: 256,
@@ -440,7 +448,8 @@ describe("CipherTransformFactory", function () {
       Perms: unescape("%D8%FC%844%E5e%0DB%5D%7Ff%FD%3COMM"),
       P: -1084,
       R: 5
-    });
+    };
+    aes256Dict = buildDict(dict3);
     aes256IsoDict = buildDict({
       Filter: _primitives.Name.get("Standard"),
       V: 5,
@@ -480,7 +489,7 @@ describe("CipherTransformFactory", function () {
     done();
   });
   afterAll(function () {
-    fileId1 = fileId2 = dict1 = dict2 = null;
+    fileId1 = fileId2 = dict1 = dict2 = dict3 = null;
     aes256Dict = aes256IsoDict = aes256BlankDict = aes256IsoBlankDict = null;
   });
   describe("#ctor", function () {
@@ -534,4 +543,42 @@ describe("CipherTransformFactory", function () {
       ensurePasswordCorrect(done, dict2, fileId2);
     });
   });
+  describe("Encrypt and decrypt", function () {
+    it("should encrypt and decrypt using ARCFour", function (done) {
+      dict3.CF = buildDict({
+        Identity: buildDict({
+          CFM: _primitives.Name.get("V2")
+        })
+      });
+      const dict = buildDict(dict3);
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "hello world");
+      done();
+    });
+    it("should encrypt and decrypt using AES128", function (done) {
+      dict3.CF = buildDict({
+        Identity: buildDict({
+          CFM: _primitives.Name.get("AESV2")
+        })
+      });
+      const dict = buildDict(dict3);
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "a");
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aa");
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaaaaaaaaaaaaa");
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaaaaaaaaaaaaaaaa");
+      done();
+    });
+    it("should encrypt and decrypt using AES256", function (done) {
+      dict3.CF = buildDict({
+        Identity: buildDict({
+          CFM: _primitives.Name.get("AESV3")
+        })
+      });
+      const dict = buildDict(dict3);
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaa");
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaa");
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaaaaaaaaaaaaa");
+      ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaaaaaaaaaaaaaaaaaaa");
+      done();
+    });
+  });
 });

+ 129 - 1
lib/test/unit/custom_spec.js

@@ -29,6 +29,8 @@ var _api = require("../../display/api.js");
 
 var _is_node = require("../../shared/is_node.js");
 
+var _node_utils = require("../../display/node_utils.js");
+
 function getTopLeftPixel(canvasContext) {
   const imgData = canvasContext.getImageData(0, 0, 1, 1);
   return {
@@ -46,7 +48,7 @@ describe("custom canvas rendering", function () {
   let page;
   beforeAll(function (done) {
     if (_is_node.isNodeJS) {
-      CanvasFactory = new _test_utils.NodeCanvasFactory();
+      CanvasFactory = new _node_utils.NodeCanvasFactory();
     } else {
       CanvasFactory = new _display_utils.DOMCanvasFactory();
     }
@@ -105,4 +107,130 @@ describe("custom canvas rendering", function () {
       done();
     }).catch(done.fail);
   });
+});
+describe("custom ownerDocument", function () {
+  const FontFace = globalThis.FontFace;
+
+  const checkFont = font => /g_d\d+_f1/.test(font.family);
+
+  const checkFontFaceRule = rule => /^@font-face {font-family:"g_d\d+_f1";src:/.test(rule);
+
+  beforeEach(() => {
+    globalThis.FontFace = function MockFontFace(name) {
+      this.family = name;
+    };
+  });
+  afterEach(() => {
+    globalThis.FontFace = FontFace;
+  });
+
+  function getMocks() {
+    const elements = [];
+
+    const createElement = name => {
+      let element = typeof document !== "undefined" && document.createElement(name);
+
+      if (name === "style") {
+        element = {
+          tagName: name,
+          sheet: {
+            cssRules: [],
+
+            insertRule(rule) {
+              this.cssRules.push(rule);
+            }
+
+          }
+        };
+        Object.assign(element, {
+          remove() {
+            this.remove.called = true;
+          }
+
+        });
+      }
+
+      elements.push(element);
+      return element;
+    };
+
+    const ownerDocument = {
+      fonts: new Set(),
+      createElement,
+      documentElement: {
+        getElementsByTagName: () => [{
+          appendChild: () => {}
+        }]
+      }
+    };
+    const CanvasFactory = _is_node.isNodeJS ? new _node_utils.NodeCanvasFactory() : new _display_utils.DOMCanvasFactory({
+      ownerDocument
+    });
+    return {
+      elements,
+      ownerDocument,
+      CanvasFactory
+    };
+  }
+
+  it("should use given document for loading fonts (with Font Loading API)", async function () {
+    const {
+      ownerDocument,
+      elements,
+      CanvasFactory
+    } = getMocks();
+    const getDocumentParams = (0, _test_utils.buildGetDocumentParams)("TrueType_without_cmap.pdf", {
+      disableFontFace: false,
+      ownerDocument
+    });
+    const loadingTask = (0, _api.getDocument)(getDocumentParams);
+    const doc = await loadingTask.promise;
+    const page = await doc.getPage(1);
+    const viewport = page.getViewport({
+      scale: 1
+    });
+    const canvasAndCtx = CanvasFactory.create(viewport.width, viewport.height);
+    await page.render({
+      canvasContext: canvasAndCtx.context,
+      viewport
+    }).promise;
+    const style = elements.find(element => element.tagName === "style");
+    expect(style).toBeFalsy();
+    expect(ownerDocument.fonts.size).toBeGreaterThanOrEqual(1);
+    expect(Array.from(ownerDocument.fonts).find(checkFont)).toBeTruthy();
+    await doc.destroy();
+    await loadingTask.destroy();
+    CanvasFactory.destroy(canvasAndCtx);
+    expect(ownerDocument.fonts.size).toBe(0);
+  });
+  it("should use given document for loading fonts (with CSS rules)", async function () {
+    const {
+      ownerDocument,
+      elements,
+      CanvasFactory
+    } = getMocks();
+    ownerDocument.fonts = null;
+    const getDocumentParams = (0, _test_utils.buildGetDocumentParams)("TrueType_without_cmap.pdf", {
+      disableFontFace: false,
+      ownerDocument
+    });
+    const loadingTask = (0, _api.getDocument)(getDocumentParams);
+    const doc = await loadingTask.promise;
+    const page = await doc.getPage(1);
+    const viewport = page.getViewport({
+      scale: 1
+    });
+    const canvasAndCtx = CanvasFactory.create(viewport.width, viewport.height);
+    await page.render({
+      canvasContext: canvasAndCtx.context,
+      viewport
+    }).promise;
+    const style = elements.find(element => element.tagName === "style");
+    expect(style.sheet.cssRules.length).toBeGreaterThanOrEqual(1);
+    expect(style.sheet.cssRules.find(checkFontFaceRule)).toBeTruthy();
+    await doc.destroy();
+    await loadingTask.destroy();
+    CanvasFactory.destroy(canvasAndCtx);
+    expect(style.remove.called).toBe(true);
+  });
 });

+ 112 - 1
lib/test/unit/document_spec.js

@@ -23,20 +23,131 @@
 
 var _test_utils = require("./test_utils.js");
 
+var _primitives = require("../../core/primitives.js");
+
+var _document = require("../../core/document.js");
+
+var _stream = require("../../core/stream.js");
+
 describe("document", function () {
   describe("Page", function () {
-    it("should create correct objId using the idFactory", function () {
+    it("should create correct objId/fontId using the idFactory", function () {
       const idFactory1 = (0, _test_utils.createIdFactory)(0);
       const idFactory2 = (0, _test_utils.createIdFactory)(1);
       expect(idFactory1.createObjId()).toEqual("p0_1");
       expect(idFactory1.createObjId()).toEqual("p0_2");
+      expect(idFactory1.createFontId()).toEqual("f1");
+      expect(idFactory1.createFontId()).toEqual("f2");
       expect(idFactory1.getDocId()).toEqual("g_d0");
       expect(idFactory2.createObjId()).toEqual("p1_1");
       expect(idFactory2.createObjId()).toEqual("p1_2");
+      expect(idFactory2.createFontId()).toEqual("f1");
+      expect(idFactory2.createFontId()).toEqual("f2");
       expect(idFactory2.getDocId()).toEqual("g_d0");
       expect(idFactory1.createObjId()).toEqual("p0_3");
       expect(idFactory1.createObjId()).toEqual("p0_4");
+      expect(idFactory1.createFontId()).toEqual("f3");
+      expect(idFactory1.createFontId()).toEqual("f4");
       expect(idFactory1.getDocId()).toEqual("g_d0");
     });
   });
+  describe("PDFDocument", function () {
+    const pdfManager = {
+      get docId() {
+        return "d0";
+      }
+
+    };
+    const stream = new _stream.StringStream("Dummy_PDF_data");
+
+    function getDocument(acroForm) {
+      const pdfDocument = new _document.PDFDocument(pdfManager, stream);
+      pdfDocument.catalog = {
+        acroForm
+      };
+      return pdfDocument;
+    }
+
+    it("should get form info when no form data is present", function () {
+      const pdfDocument = getDocument(null);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: false,
+        hasXfa: false
+      });
+    });
+    it("should get form info when XFA is present", function () {
+      const acroForm = new _primitives.Dict();
+      acroForm.set("XFA", []);
+      let pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: false,
+        hasXfa: false
+      });
+      acroForm.set("XFA", ["foo", "bar"]);
+      pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: false,
+        hasXfa: true
+      });
+      acroForm.set("XFA", new _stream.StringStream(""));
+      pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: false,
+        hasXfa: false
+      });
+      acroForm.set("XFA", new _stream.StringStream("non-empty"));
+      pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: false,
+        hasXfa: true
+      });
+    });
+    it("should get form info when AcroForm is present", function () {
+      const acroForm = new _primitives.Dict();
+      acroForm.set("Fields", []);
+      let pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: false,
+        hasXfa: false
+      });
+      acroForm.set("Fields", ["foo", "bar"]);
+      pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: true,
+        hasXfa: false
+      });
+      acroForm.set("Fields", ["foo", "bar"]);
+      acroForm.set("SigFlags", 2);
+      pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: true,
+        hasXfa: false
+      });
+      const annotationDict = new _primitives.Dict();
+      annotationDict.set("FT", _primitives.Name.get("Sig"));
+      annotationDict.set("Rect", [0, 0, 0, 0]);
+
+      const annotationRef = _primitives.Ref.get(11, 0);
+
+      const kidsDict = new _primitives.Dict();
+      kidsDict.set("Kids", [annotationRef]);
+
+      const kidsRef = _primitives.Ref.get(10, 0);
+
+      pdfDocument.xref = new _test_utils.XRefMock([{
+        ref: annotationRef,
+        data: annotationDict
+      }, {
+        ref: kidsRef,
+        data: kidsDict
+      }]);
+      acroForm.set("Fields", [kidsRef]);
+      acroForm.set("SigFlags", 3);
+      pdfDocument = getDocument(acroForm);
+      expect(pdfDocument.formInfo).toEqual({
+        hasAcroForm: false,
+        hasXfa: false
+      });
+    });
+  });
 });

+ 29 - 10
lib/test/unit/evaluator_spec.js

@@ -108,14 +108,26 @@ describe("evaluator", function () {
       });
     });
     it("should handle two glued operations", function (done) {
-      var resources = new ResourcesMock();
-      resources.Res1 = {};
+      const imgDict = new _primitives.Dict();
+      imgDict.set("Subtype", _primitives.Name.get("Image"));
+      imgDict.set("Width", 1);
+      imgDict.set("Height", 1);
+      const imgStream = new _stream.Stream([0]);
+      imgStream.dict = imgDict;
+      const xObject = new _primitives.Dict();
+      xObject.set("Res1", imgStream);
+      const resources = new ResourcesMock();
+      resources.XObject = xObject;
       var stream = new _stream.StringStream("/Res1 DoQ");
       runOperatorListCheck(partialEvaluator, stream, resources, function (result) {
-        expect(!!result.fnArray && !!result.argsArray).toEqual(true);
-        expect(result.fnArray.length).toEqual(2);
-        expect(result.fnArray[0]).toEqual(_util.OPS.paintXObject);
-        expect(result.fnArray[1]).toEqual(_util.OPS.restore);
+        expect(result.fnArray.length).toEqual(3);
+        expect(result.fnArray[0]).toEqual(_util.OPS.dependency);
+        expect(result.fnArray[1]).toEqual(_util.OPS.paintImageXObject);
+        expect(result.fnArray[2]).toEqual(_util.OPS.restore);
+        expect(result.argsArray.length).toEqual(3);
+        expect(result.argsArray[0]).toEqual(["img_p0_1"]);
+        expect(result.argsArray[1]).toEqual(["img_p0_1", 1, 1]);
+        expect(result.argsArray[2]).toEqual(null);
         done();
       });
     });
@@ -194,16 +206,23 @@ describe("evaluator", function () {
       });
     });
     it("should execute if nested commands", function (done) {
+      const gState = new _primitives.Dict();
+      gState.set("LW", 2);
+      gState.set("CA", 0.5);
+      const extGState = new _primitives.Dict();
+      extGState.set("GS2", gState);
+      const resources = new ResourcesMock();
+      resources.ExtGState = extGState;
       var stream = new _stream.StringStream("/F2 /GS2 gs 5.711 Tf");
-      runOperatorListCheck(partialEvaluator, stream, new ResourcesMock(), function (result) {
+      runOperatorListCheck(partialEvaluator, stream, resources, function (result) {
         expect(result.fnArray.length).toEqual(3);
         expect(result.fnArray[0]).toEqual(_util.OPS.setGState);
         expect(result.fnArray[1]).toEqual(_util.OPS.dependency);
         expect(result.fnArray[2]).toEqual(_util.OPS.setFont);
         expect(result.argsArray.length).toEqual(3);
-        expect(result.argsArray[0].length).toEqual(1);
-        expect(result.argsArray[1].length).toEqual(1);
-        expect(result.argsArray[2].length).toEqual(2);
+        expect(result.argsArray[0]).toEqual([[["LW", 2], ["CA", 0.5]]]);
+        expect(result.argsArray[1]).toEqual(["g_font_error"]);
+        expect(result.argsArray[2]).toEqual(["g_font_error", 5.711]);
         done();
       });
     });

+ 1 - 1
lib/test/unit/jasmine-boot.js

@@ -22,7 +22,7 @@
 "use strict";
 
 function initializePDFJS(callback) {
-  Promise.all(["pdfjs/display/api.js", "pdfjs/display/worker_options.js", "pdfjs/display/network.js", "pdfjs/display/fetch_stream.js", "pdfjs/shared/is_node.js", "pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/api_spec.js", "pdfjs-test/unit/bidi_spec.js", "pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cmap_spec.js", "pdfjs-test/unit/colorspace_spec.js", "pdfjs-test/unit/core_utils_spec.js", "pdfjs-test/unit/crypto_spec.js", "pdfjs-test/unit/custom_spec.js", "pdfjs-test/unit/display_svg_spec.js", "pdfjs-test/unit/display_utils_spec.js", "pdfjs-test/unit/document_spec.js", "pdfjs-test/unit/encodings_spec.js", "pdfjs-test/unit/evaluator_spec.js", "pdfjs-test/unit/function_spec.js", "pdfjs-test/unit/fetch_stream_spec.js", "pdfjs-test/unit/message_handler_spec.js", "pdfjs-test/unit/metadata_spec.js", "pdfjs-test/unit/murmurhash3_spec.js", "pdfjs-test/unit/network_spec.js", "pdfjs-test/unit/network_utils_spec.js", "pdfjs-test/unit/parser_spec.js", "pdfjs-test/unit/pdf_find_controller_spec.js", "pdfjs-test/unit/pdf_find_utils_spec.js", "pdfjs-test/unit/pdf_history_spec.js", "pdfjs-test/unit/primitives_spec.js", "pdfjs-test/unit/stream_spec.js", "pdfjs-test/unit/type1_parser_spec.js", "pdfjs-test/unit/ui_utils_spec.js", "pdfjs-test/unit/unicode_spec.js", "pdfjs-test/unit/util_spec.js"].map(function (moduleName) {
+  Promise.all(["pdfjs/display/api.js", "pdfjs/display/worker_options.js", "pdfjs/display/network.js", "pdfjs/display/fetch_stream.js", "pdfjs/shared/is_node.js", "pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/annotation_storage_spec.js", "pdfjs-test/unit/api_spec.js", "pdfjs-test/unit/bidi_spec.js", "pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cmap_spec.js", "pdfjs-test/unit/colorspace_spec.js", "pdfjs-test/unit/core_utils_spec.js", "pdfjs-test/unit/crypto_spec.js", "pdfjs-test/unit/custom_spec.js", "pdfjs-test/unit/display_svg_spec.js", "pdfjs-test/unit/display_utils_spec.js", "pdfjs-test/unit/document_spec.js", "pdfjs-test/unit/encodings_spec.js", "pdfjs-test/unit/evaluator_spec.js", "pdfjs-test/unit/function_spec.js", "pdfjs-test/unit/fetch_stream_spec.js", "pdfjs-test/unit/message_handler_spec.js", "pdfjs-test/unit/metadata_spec.js", "pdfjs-test/unit/murmurhash3_spec.js", "pdfjs-test/unit/network_spec.js", "pdfjs-test/unit/network_utils_spec.js", "pdfjs-test/unit/parser_spec.js", "pdfjs-test/unit/pdf_find_controller_spec.js", "pdfjs-test/unit/pdf_find_utils_spec.js", "pdfjs-test/unit/pdf_history_spec.js", "pdfjs-test/unit/primitives_spec.js", "pdfjs-test/unit/stream_spec.js", "pdfjs-test/unit/type1_parser_spec.js", "pdfjs-test/unit/ui_utils_spec.js", "pdfjs-test/unit/unicode_spec.js", "pdfjs-test/unit/util_spec.js", "pdfjs-test/unit/writer_spec.js"].map(function (moduleName) {
     return SystemJS.import(moduleName);
   })).then(function (modules) {
     const displayApi = modules[0];

+ 3 - 3
lib/test/unit/metadata_spec.js

@@ -21,7 +21,7 @@
  */
 "use strict";
 
-var _util = require("../../shared/util.js");
+var _test_utils = require("./test_utils.js");
 
 var _metadata = require("../../display/metadata.js");
 
@@ -64,7 +64,7 @@ describe("metadata", function () {
   it("should gracefully handle incomplete tags (issue 8884)", function () {
     const data = '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d' + '<x:xmpmeta xmlns:x="adobe:ns:meta/">' + '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' + '<rdf:Description rdf:about=""' + 'xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/">' + "</rdf:Description>" + '<rdf:Description rdf:about=""' + 'xmlns:xap="http://ns.adobe.com/xap/1.0/">' + "<xap:ModifyDate>2010-03-25T11:20:09-04:00</xap:ModifyDate>" + "<xap:CreateDate>2010-03-25T11:20:09-04:00</xap:CreateDate>" + "<xap:MetadataDate>2010-03-25T11:20:09-04:00</xap:MetadataDate>" + "</rdf:Description>" + '<rdf:Description rdf:about=""' + 'xmlns:dc="http://purl.org/dc/elements/1.1/">' + "<dc:format>application/pdf</dc:format>" + "</rdf:Description>" + '<rdf:Description rdf:about=""' + 'xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">' + "<pdfaid:part>1</pdfaid:part>" + "<pdfaid:conformance>A</pdfaid:conformance>" + "</rdf:Description>" + "</rdf:RDF>" + "</x:xmpmeta>" + '<?xpacket end="w"?>';
     const metadata = new _metadata.Metadata(data);
-    expect((0, _util.isEmptyObj)(metadata.getAll())).toEqual(true);
+    expect((0, _test_utils.isEmptyObj)(metadata.getAll())).toEqual(true);
   });
   it('should gracefully handle "junk" before the actual metadata (issue 10395)', function () {
     const data = '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' + '<x:xmpmeta x:xmptk="TallComponents PDFObjects 1.0" ' + 'xmlns:x="adobe:ns:meta/">' + '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' + '<rdf:Description rdf:about="" ' + 'xmlns:pdf="http://ns.adobe.com/pdf/1.3/">' + "<pdf:Producer>PDFKit.NET 4.0.102.0</pdf:Producer>" + "<pdf:Keywords></pdf:Keywords>" + "<pdf:PDFVersion>1.7</pdf:PDFVersion></rdf:Description>" + '<rdf:Description rdf:about="" ' + 'xmlns:xap="http://ns.adobe.com/xap/1.0/">' + "<xap:CreateDate>2018-12-27T13:50:36-08:00</xap:CreateDate>" + "<xap:ModifyDate>2018-12-27T13:50:38-08:00</xap:ModifyDate>" + "<xap:CreatorTool></xap:CreatorTool>" + "<xap:MetadataDate>2018-12-27T13:50:38-08:00</xap:MetadataDate>" + '</rdf:Description><rdf:Description rdf:about="" ' + 'xmlns:dc="http://purl.org/dc/elements/1.1/">' + "<dc:creator><rdf:Seq><rdf:li></rdf:li></rdf:Seq></dc:creator>" + "<dc:subject><rdf:Bag /></dc:subject>" + '<dc:description><rdf:Alt><rdf:li xml:lang="x-default">' + "</rdf:li></rdf:Alt></dc:description>" + '<dc:title><rdf:Alt><rdf:li xml:lang="x-default"></rdf:li>' + "</rdf:Alt></dc:title><dc:format>application/pdf</dc:format>" + '</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
@@ -102,7 +102,7 @@ describe("metadata", function () {
   it("should gracefully handle unbalanced end tags (issue 10410)", function () {
     const data = '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' + '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' + '<rdf:Description rdf:about="" ' + 'xmlns:pdf="http://ns.adobe.com/pdf/1.3/">' + "<pdf:Producer>Soda PDF 5</pdf:Producer></rdf:Description>" + '<rdf:Description rdf:about="" ' + 'xmlns:xap="http://ns.adobe.com/xap/1.0/">' + "<xap:CreateDate>2018-10-02T08:14:49-05:00</xap:CreateDate>" + "<xap:CreatorTool>Soda PDF 5</xap:CreatorTool>" + "<xap:MetadataDate>2018-10-02T08:14:49-05:00</xap:MetadataDate> " + "<xap:ModifyDate>2018-10-02T08:14:49-05:00</xap:ModifyDate>" + '</rdf:Description><rdf:Description rdf:about="" ' + 'xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/">' + "<xmpMM:DocumentID>uuid:00000000-1c84-3cf9-89ba-bef0e729c831" + "</xmpMM:DocumentID></rdf:Description>" + '</rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
     const metadata = new _metadata.Metadata(data);
-    expect((0, _util.isEmptyObj)(metadata.getAll())).toEqual(true);
+    expect((0, _test_utils.isEmptyObj)(metadata.getAll())).toEqual(true);
   });
   it("should not be vulnerable to the billion laughs attack", function () {
     const data = '<?xml version="1.0"?>' + "<!DOCTYPE lolz [" + '  <!ENTITY lol "lol">' + '  <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">' + '  <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">' + '  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">' + '  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">' + '  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">' + '  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">' + '  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">' + '  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">' + '  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">' + "]>" + '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' + '  <rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/">' + "    <dc:title>" + "      <rdf:Alt>" + '        <rdf:li xml:lang="x-default">a&lol9;b</rdf:li>' + "      </rdf:Alt>" + "    </dc:title>" + "  </rdf:Description>" + "</rdf:RDF>";

+ 242 - 63
lib/test/unit/primitives_spec.js

@@ -23,25 +23,27 @@
 
 var _primitives = require("../../core/primitives.js");
 
+var _stream = require("../../core/stream.js");
+
 var _test_utils = require("./test_utils.js");
 
 describe("primitives", function () {
   describe("Name", function () {
     it("should retain the given name", function () {
-      var givenName = "Font";
+      const givenName = "Font";
 
-      var name = _primitives.Name.get(givenName);
+      const name = _primitives.Name.get(givenName);
 
       expect(name.name).toEqual(givenName);
     });
     it("should create only one object for a name and cache it", function () {
-      var firstFont = _primitives.Name.get("Font");
+      const firstFont = _primitives.Name.get("Font");
 
-      var secondFont = _primitives.Name.get("Font");
+      const secondFont = _primitives.Name.get("Font");
 
-      var firstSubtype = _primitives.Name.get("Subtype");
+      const firstSubtype = _primitives.Name.get("Subtype");
 
-      var secondSubtype = _primitives.Name.get("Subtype");
+      const secondSubtype = _primitives.Name.get("Subtype");
 
       expect(firstFont).toBe(secondFont);
       expect(firstSubtype).toBe(secondSubtype);
@@ -50,20 +52,20 @@ describe("primitives", function () {
   });
   describe("Cmd", function () {
     it("should retain the given cmd name", function () {
-      var givenCmd = "BT";
+      const givenCmd = "BT";
 
-      var cmd = _primitives.Cmd.get(givenCmd);
+      const cmd = _primitives.Cmd.get(givenCmd);
 
       expect(cmd.cmd).toEqual(givenCmd);
     });
     it("should create only one object for a command and cache it", function () {
-      var firstBT = _primitives.Cmd.get("BT");
+      const firstBT = _primitives.Cmd.get("BT");
 
-      var secondBT = _primitives.Cmd.get("BT");
+      const secondBT = _primitives.Cmd.get("BT");
 
-      var firstET = _primitives.Cmd.get("ET");
+      const firstET = _primitives.Cmd.get("ET");
 
-      var secondET = _primitives.Cmd.get("ET");
+      const secondET = _primitives.Cmd.get("ET");
 
       expect(firstBT).toBe(secondBT);
       expect(firstET).toBe(secondET);
@@ -71,23 +73,23 @@ describe("primitives", function () {
     });
   });
   describe("Dict", function () {
-    var checkInvalidHasValues = function (dict) {
+    const checkInvalidHasValues = function (dict) {
       expect(dict.has()).toBeFalsy();
       expect(dict.has("Prev")).toBeFalsy();
     };
 
-    var checkInvalidKeyValues = function (dict) {
+    const checkInvalidKeyValues = function (dict) {
       expect(dict.get()).toBeUndefined();
       expect(dict.get("Prev")).toBeUndefined();
       expect(dict.get("Decode", "D")).toBeUndefined();
       expect(dict.get("FontFile", "FontFile2", "FontFile3")).toBeUndefined();
     };
 
-    var emptyDict, dictWithSizeKey, dictWithManyKeys;
-    var storedSize = 42;
-    var testFontFile = "file1";
-    var testFontFile2 = "file2";
-    var testFontFile3 = "file3";
+    let emptyDict, dictWithSizeKey, dictWithManyKeys;
+    const storedSize = 42;
+    const testFontFile = "file1";
+    const testFontFile2 = "file2";
+    const testFontFile3 = "file3";
     beforeAll(function (done) {
       emptyDict = new _primitives.Dict();
       dictWithSizeKey = new _primitives.Dict();
@@ -101,6 +103,21 @@ describe("primitives", function () {
     afterAll(function () {
       emptyDict = dictWithSizeKey = dictWithManyKeys = null;
     });
+    it("should allow assigning an XRef table after creation", function () {
+      const dict = new _primitives.Dict(null);
+      expect(dict.xref).toEqual(null);
+      const xref = new _test_utils.XRefMock([]);
+      dict.assignXref(xref);
+      expect(dict.xref).toEqual(xref);
+    });
+    it("should return correct size", function () {
+      const dict = new _primitives.Dict(null);
+      expect(dict.size).toEqual(0);
+      dict.set("Type", _primitives.Name.get("Page"));
+      expect(dict.size).toEqual(1);
+      dict.set("Contents", _primitives.Ref.get(10, 0));
+      expect(dict.size).toEqual(2);
+    });
     it("should return invalid values for unknown keys", function () {
       checkInvalidHasValues(emptyDict);
       checkInvalidKeyValues(emptyDict);
@@ -132,7 +149,7 @@ describe("primitives", function () {
       expect(dictWithManyKeys.get("FontFile", "FontFile2", "FontFile3")).toEqual(testFontFile);
     });
     it("should asynchronously fetch unknown keys", function (done) {
-      var keyPromises = [dictWithManyKeys.getAsync("Size"), dictWithSizeKey.getAsync("FontFile", "FontFile2", "FontFile3")];
+      const keyPromises = [dictWithManyKeys.getAsync("Size"), dictWithSizeKey.getAsync("FontFile", "FontFile2", "FontFile3")];
       Promise.all(keyPromises).then(function (values) {
         expect(values[0]).toBeUndefined();
         expect(values[1]).toBeUndefined();
@@ -142,7 +159,7 @@ describe("primitives", function () {
       });
     });
     it("should asynchronously fetch correct values for multiple stored keys", function (done) {
-      var keyPromises = [dictWithManyKeys.getAsync("FontFile3"), dictWithManyKeys.getAsync("FontFile2", "FontFile3"), dictWithManyKeys.getAsync("FontFile", "FontFile2", "FontFile3")];
+      const keyPromises = [dictWithManyKeys.getAsync("FontFile3"), dictWithManyKeys.getAsync("FontFile2", "FontFile3"), dictWithManyKeys.getAsync("FontFile", "FontFile2", "FontFile3")];
       Promise.all(keyPromises).then(function (values) {
         expect(values[0]).toEqual(testFontFile3);
         expect(values[1]).toEqual(testFontFile2);
@@ -153,23 +170,23 @@ describe("primitives", function () {
       });
     });
     it("should callback for each stored key", function () {
-      var callbackSpy = jasmine.createSpy("spy on callback in dictionary");
+      const callbackSpy = jasmine.createSpy("spy on callback in dictionary");
       dictWithManyKeys.forEach(callbackSpy);
       expect(callbackSpy).toHaveBeenCalled();
-      var callbackSpyCalls = callbackSpy.calls;
+      const callbackSpyCalls = callbackSpy.calls;
       expect(callbackSpyCalls.argsFor(0)).toEqual(["FontFile", testFontFile]);
       expect(callbackSpyCalls.argsFor(1)).toEqual(["FontFile2", testFontFile2]);
       expect(callbackSpyCalls.argsFor(2)).toEqual(["FontFile3", testFontFile3]);
       expect(callbackSpyCalls.count()).toEqual(3);
     });
     it("should handle keys pointing to indirect objects, both sync and async", function (done) {
-      var fontRef = _primitives.Ref.get(1, 0);
+      const fontRef = _primitives.Ref.get(1, 0);
 
-      var xref = new _test_utils.XRefMock([{
+      const xref = new _test_utils.XRefMock([{
         ref: fontRef,
         data: testFontFile
       }]);
-      var fontDict = new _primitives.Dict(xref);
+      const fontDict = new _primitives.Dict(xref);
       fontDict.set("FontFile", fontRef);
       expect(fontDict.getRaw("FontFile")).toEqual(fontRef);
       expect(fontDict.get("FontFile", "FontFile2", "FontFile3")).toEqual(testFontFile);
@@ -181,89 +198,241 @@ describe("primitives", function () {
       });
     });
     it("should handle arrays containing indirect objects", function () {
-      var minCoordRef = _primitives.Ref.get(1, 0),
-          maxCoordRef = _primitives.Ref.get(2, 0);
+      const minCoordRef = _primitives.Ref.get(1, 0);
+
+      const maxCoordRef = _primitives.Ref.get(2, 0);
 
-      var minCoord = 0,
-          maxCoord = 1;
-      var xref = new _test_utils.XRefMock([{
+      const minCoord = 0;
+      const maxCoord = 1;
+      const xref = new _test_utils.XRefMock([{
         ref: minCoordRef,
         data: minCoord
       }, {
         ref: maxCoordRef,
         data: maxCoord
       }]);
-      var xObjectDict = new _primitives.Dict(xref);
+      const xObjectDict = new _primitives.Dict(xref);
       xObjectDict.set("BBox", [minCoord, maxCoord, minCoordRef, maxCoordRef]);
       expect(xObjectDict.get("BBox")).toEqual([minCoord, maxCoord, minCoordRef, maxCoordRef]);
       expect(xObjectDict.getArray("BBox")).toEqual([minCoord, maxCoord, minCoord, maxCoord]);
     });
     it("should get all key names", function () {
-      var expectedKeys = ["FontFile", "FontFile2", "FontFile3"];
-      var keys = dictWithManyKeys.getKeys();
+      const expectedKeys = ["FontFile", "FontFile2", "FontFile3"];
+      const keys = dictWithManyKeys.getKeys();
       expect(keys.sort()).toEqual(expectedKeys);
     });
+    it("should get all raw values", function () {
+      const expectedRawValues1 = [testFontFile, testFontFile2, testFontFile3];
+      const rawValues1 = dictWithManyKeys.getRawValues();
+      expect(rawValues1.sort()).toEqual(expectedRawValues1);
+
+      const typeName = _primitives.Name.get("Page");
+
+      const resources = new _primitives.Dict(null),
+            resourcesRef = _primitives.Ref.get(5, 0);
+
+      const contents = new _stream.StringStream("data"),
+            contentsRef = _primitives.Ref.get(10, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: resourcesRef,
+        data: resources
+      }, {
+        ref: contentsRef,
+        data: contents
+      }]);
+      const dict = new _primitives.Dict(xref);
+      dict.set("Type", typeName);
+      dict.set("Resources", resourcesRef);
+      dict.set("Contents", contentsRef);
+      const expectedRawValues2 = [contentsRef, resourcesRef, typeName];
+      const rawValues2 = dict.getRawValues();
+      expect(rawValues2.sort()).toEqual(expectedRawValues2);
+    });
     it("should create only one object for Dict.empty", function () {
-      var firstDictEmpty = _primitives.Dict.empty;
-      var secondDictEmpty = _primitives.Dict.empty;
+      const firstDictEmpty = _primitives.Dict.empty;
+      const secondDictEmpty = _primitives.Dict.empty;
       expect(firstDictEmpty).toBe(secondDictEmpty);
       expect(firstDictEmpty).not.toBe(emptyDict);
     });
     it("should correctly merge dictionaries", function () {
-      var expectedKeys = ["FontFile", "FontFile2", "FontFile3", "Size"];
-      var fontFileDict = new _primitives.Dict();
+      const expectedKeys = ["FontFile", "FontFile2", "FontFile3", "Size"];
+      const fontFileDict = new _primitives.Dict();
       fontFileDict.set("FontFile", "Type1 font file");
 
-      var mergedDict = _primitives.Dict.merge(null, [dictWithManyKeys, dictWithSizeKey, fontFileDict]);
+      const mergedDict = _primitives.Dict.merge({
+        xref: null,
+        dictArray: [dictWithManyKeys, dictWithSizeKey, fontFileDict]
+      });
 
-      var mergedKeys = mergedDict.getKeys();
+      const mergedKeys = mergedDict.getKeys();
       expect(mergedKeys.sort()).toEqual(expectedKeys);
       expect(mergedDict.get("FontFile")).toEqual(testFontFile);
     });
+    it("should correctly merge sub-dictionaries", function () {
+      const localFontDict = new _primitives.Dict();
+      localFontDict.set("F1", "Local font one");
+      const globalFontDict = new _primitives.Dict();
+      globalFontDict.set("F1", "Global font one");
+      globalFontDict.set("F2", "Global font two");
+      globalFontDict.set("F3", "Global font three");
+      const localDict = new _primitives.Dict();
+      localDict.set("Font", localFontDict);
+      const globalDict = new _primitives.Dict();
+      globalDict.set("Font", globalFontDict);
+
+      const mergedDict = _primitives.Dict.merge({
+        xref: null,
+        dictArray: [localDict, globalDict]
+      });
+
+      const mergedSubDict = _primitives.Dict.merge({
+        xref: null,
+        dictArray: [localDict, globalDict],
+        mergeSubDicts: true
+      });
+
+      const mergedFontDict = mergedDict.get("Font");
+      const mergedSubFontDict = mergedSubDict.get("Font");
+      expect(mergedFontDict instanceof _primitives.Dict).toEqual(true);
+      expect(mergedSubFontDict instanceof _primitives.Dict).toEqual(true);
+      const mergedFontDictKeys = mergedFontDict.getKeys();
+      const mergedSubFontDictKeys = mergedSubFontDict.getKeys();
+      expect(mergedFontDictKeys).toEqual(["F1"]);
+      expect(mergedSubFontDictKeys).toEqual(["F1", "F2", "F3"]);
+      const mergedFontDictValues = mergedFontDict.getRawValues();
+      const mergedSubFontDictValues = mergedSubFontDict.getRawValues();
+      expect(mergedFontDictValues).toEqual(["Local font one"]);
+      expect(mergedSubFontDictValues).toEqual(["Local font one", "Global font two", "Global font three"]);
+    });
   });
   describe("Ref", function () {
+    it("should get a string representation", function () {
+      const nonZeroRef = _primitives.Ref.get(4, 2);
+
+      expect(nonZeroRef.toString()).toEqual("4R2");
+
+      const zeroRef = _primitives.Ref.get(4, 0);
+
+      expect(zeroRef.toString()).toEqual("4R");
+    });
     it("should retain the stored values", function () {
-      var storedNum = 4;
-      var storedGen = 2;
+      const storedNum = 4;
+      const storedGen = 2;
 
-      var ref = _primitives.Ref.get(storedNum, storedGen);
+      const ref = _primitives.Ref.get(storedNum, storedGen);
 
       expect(ref.num).toEqual(storedNum);
       expect(ref.gen).toEqual(storedGen);
     });
+    it("should create only one object for a reference and cache it", function () {
+      const firstRef = _primitives.Ref.get(4, 2);
+
+      const secondRef = _primitives.Ref.get(4, 2);
+
+      const firstOtherRef = _primitives.Ref.get(5, 2);
+
+      const secondOtherRef = _primitives.Ref.get(5, 2);
+
+      expect(firstRef).toBe(secondRef);
+      expect(firstOtherRef).toBe(secondOtherRef);
+      expect(firstRef).not.toBe(firstOtherRef);
+    });
   });
   describe("RefSet", function () {
     it("should have a stored value", function () {
-      var ref = _primitives.Ref.get(4, 2);
+      const ref = _primitives.Ref.get(4, 2);
 
-      var refset = new _primitives.RefSet();
+      const refset = new _primitives.RefSet();
       refset.put(ref);
       expect(refset.has(ref)).toBeTruthy();
     });
     it("should not have an unknown value", function () {
-      var ref = _primitives.Ref.get(4, 2);
+      const ref = _primitives.Ref.get(4, 2);
 
-      var refset = new _primitives.RefSet();
+      const refset = new _primitives.RefSet();
       expect(refset.has(ref)).toBeFalsy();
       refset.put(ref);
 
-      var anotherRef = _primitives.Ref.get(2, 4);
+      const anotherRef = _primitives.Ref.get(2, 4);
 
       expect(refset.has(anotherRef)).toBeFalsy();
     });
   });
+  describe("RefSetCache", function () {
+    const ref1 = _primitives.Ref.get(4, 2);
+
+    const ref2 = _primitives.Ref.get(5, 2);
+
+    const obj1 = _primitives.Name.get("foo");
+
+    const obj2 = _primitives.Name.get("bar");
+
+    let cache;
+    beforeEach(function (done) {
+      cache = new _primitives.RefSetCache();
+      done();
+    });
+    afterEach(function () {
+      cache = null;
+    });
+    it("should put, have and get a value", function () {
+      cache.put(ref1, obj1);
+      expect(cache.has(ref1)).toBeTruthy();
+      expect(cache.has(ref2)).toBeFalsy();
+      expect(cache.get(ref1)).toBe(obj1);
+    });
+    it("should put, have and get a value by alias", function () {
+      cache.put(ref1, obj1);
+      cache.putAlias(ref2, ref1);
+      expect(cache.has(ref1)).toBeTruthy();
+      expect(cache.has(ref2)).toBeTruthy();
+      expect(cache.get(ref1)).toBe(obj1);
+      expect(cache.get(ref2)).toBe(obj1);
+    });
+    it("should report the size of the cache", function () {
+      cache.put(ref1, obj1);
+      expect(cache.size).toEqual(1);
+      cache.put(ref2, obj2);
+      expect(cache.size).toEqual(2);
+    });
+    it("should clear the cache", function () {
+      cache.put(ref1, obj1);
+      expect(cache.size).toEqual(1);
+      cache.clear();
+      expect(cache.size).toEqual(0);
+    });
+    it("should support iteration", function () {
+      cache.put(ref1, obj1);
+      cache.put(ref2, obj2);
+      const values = [];
+      cache.forEach(function (value) {
+        values.push(value);
+      });
+      expect(values).toEqual([obj1, obj2]);
+    });
+  });
+  describe("isEOF", function () {
+    it("handles non-EOF", function () {
+      const nonEOF = "foo";
+      expect((0, _primitives.isEOF)(nonEOF)).toEqual(false);
+    });
+    it("handles EOF", function () {
+      expect((0, _primitives.isEOF)(_primitives.EOF)).toEqual(true);
+    });
+  });
   describe("isName", function () {
     it("handles non-names", function () {
-      var nonName = {};
+      const nonName = {};
       expect((0, _primitives.isName)(nonName)).toEqual(false);
     });
     it("handles names", function () {
-      var name = _primitives.Name.get("Font");
+      const name = _primitives.Name.get("Font");
 
       expect((0, _primitives.isName)(name)).toEqual(true);
     });
     it("handles names with name check", function () {
-      var name = _primitives.Name.get("Font");
+      const name = _primitives.Name.get("Font");
 
       expect((0, _primitives.isName)(name, "Font")).toEqual(true);
       expect((0, _primitives.isName)(name, "Subtype")).toEqual(false);
@@ -271,16 +440,16 @@ describe("primitives", function () {
   });
   describe("isCmd", function () {
     it("handles non-commands", function () {
-      var nonCmd = {};
+      const nonCmd = {};
       expect((0, _primitives.isCmd)(nonCmd)).toEqual(false);
     });
     it("handles commands", function () {
-      var cmd = _primitives.Cmd.get("BT");
+      const cmd = _primitives.Cmd.get("BT");
 
       expect((0, _primitives.isCmd)(cmd)).toEqual(true);
     });
     it("handles commands with cmd check", function () {
-      var cmd = _primitives.Cmd.get("BT");
+      const cmd = _primitives.Cmd.get("BT");
 
       expect((0, _primitives.isCmd)(cmd, "BT")).toEqual(true);
       expect((0, _primitives.isCmd)(cmd, "ET")).toEqual(false);
@@ -288,16 +457,16 @@ describe("primitives", function () {
   });
   describe("isDict", function () {
     it("handles non-dictionaries", function () {
-      var nonDict = {};
+      const nonDict = {};
       expect((0, _primitives.isDict)(nonDict)).toEqual(false);
     });
     it("handles empty dictionaries with type check", function () {
-      var dict = _primitives.Dict.empty;
+      const dict = _primitives.Dict.empty;
       expect((0, _primitives.isDict)(dict)).toEqual(true);
       expect((0, _primitives.isDict)(dict, "Page")).toEqual(false);
     });
     it("handles dictionaries with type check", function () {
-      var dict = new _primitives.Dict();
+      const dict = new _primitives.Dict();
       dict.set("Type", _primitives.Name.get("Page"));
       expect((0, _primitives.isDict)(dict, "Page")).toEqual(true);
       expect((0, _primitives.isDict)(dict, "Contents")).toEqual(false);
@@ -305,29 +474,39 @@ describe("primitives", function () {
   });
   describe("isRef", function () {
     it("handles non-refs", function () {
-      var nonRef = {};
+      const nonRef = {};
       expect((0, _primitives.isRef)(nonRef)).toEqual(false);
     });
     it("handles refs", function () {
-      var ref = _primitives.Ref.get(1, 0);
+      const ref = _primitives.Ref.get(1, 0);
 
       expect((0, _primitives.isRef)(ref)).toEqual(true);
     });
   });
   describe("isRefsEqual", function () {
     it("should handle Refs pointing to the same object", function () {
-      var ref1 = _primitives.Ref.get(1, 0);
+      const ref1 = _primitives.Ref.get(1, 0);
 
-      var ref2 = _primitives.Ref.get(1, 0);
+      const ref2 = _primitives.Ref.get(1, 0);
 
       expect((0, _primitives.isRefsEqual)(ref1, ref2)).toEqual(true);
     });
     it("should handle Refs pointing to different objects", function () {
-      var ref1 = _primitives.Ref.get(1, 0);
+      const ref1 = _primitives.Ref.get(1, 0);
 
-      var ref2 = _primitives.Ref.get(2, 0);
+      const ref2 = _primitives.Ref.get(2, 0);
 
       expect((0, _primitives.isRefsEqual)(ref1, ref2)).toEqual(false);
     });
   });
+  describe("isStream", function () {
+    it("handles non-streams", function () {
+      const nonStream = {};
+      expect((0, _primitives.isStream)(nonStream)).toEqual(false);
+    });
+    it("handles streams", function () {
+      const stream = new _stream.StringStream("foo");
+      expect((0, _primitives.isStream)(stream)).toEqual(true);
+    });
+  });
 });

+ 42 - 89
lib/test/unit/test_utils.js

@@ -26,15 +26,18 @@ Object.defineProperty(exports, "__esModule", {
 });
 exports.buildGetDocumentParams = buildGetDocumentParams;
 exports.createIdFactory = createIdFactory;
-exports.TEST_PDFS_PATH = exports.XRefMock = exports.NodeCMapReaderFactory = exports.NodeCanvasFactory = exports.NodeFileReaderFactory = exports.DOMFileReaderFactory = void 0;
+exports.isEmptyObj = isEmptyObj;
+exports.TEST_PDFS_PATH = exports.XRefMock = exports.NodeFileReaderFactory = exports.DOMFileReaderFactory = void 0;
+
+var _primitives = require("../../core/primitives.js");
+
+var _document = require("../../core/document.js");
 
 var _util = require("../../shared/util.js");
 
 var _is_node = require("../../shared/is_node.js");
 
-var _primitives = require("../../core/primitives.js");
-
-var _document = require("../../core/document.js");
+var _stream = require("../../core/stream.js");
 
 class DOMFileReaderFactory {
   static async fetch(params) {
@@ -92,86 +95,14 @@ function buildGetDocumentParams(filename, options) {
   return params;
 }
 
-class NodeCanvasFactory {
-  create(width, height) {
-    (0, _util.assert)(width > 0 && height > 0, "Invalid canvas size");
-
-    const Canvas = require("canvas");
-
-    const canvas = Canvas.createCanvas(width, height);
-    return {
-      canvas,
-      context: canvas.getContext("2d")
-    };
-  }
-
-  reset(canvasAndContext, width, height) {
-    (0, _util.assert)(canvasAndContext.canvas, "Canvas is not specified");
-    (0, _util.assert)(width > 0 && height > 0, "Invalid canvas size");
-    canvasAndContext.canvas.width = width;
-    canvasAndContext.canvas.height = height;
-  }
-
-  destroy(canvasAndContext) {
-    (0, _util.assert)(canvasAndContext.canvas, "Canvas is not specified");
-    canvasAndContext.canvas.width = 0;
-    canvasAndContext.canvas.height = 0;
-    canvasAndContext.canvas = null;
-    canvasAndContext.context = null;
-  }
-
-}
-
-exports.NodeCanvasFactory = NodeCanvasFactory;
-
-class NodeCMapReaderFactory {
-  constructor({
-    baseUrl = null,
-    isCompressed = false
-  }) {
-    this.baseUrl = baseUrl;
-    this.isCompressed = isCompressed;
-  }
-
-  async fetch({
-    name
-  }) {
-    if (!this.baseUrl) {
-      throw new Error('The CMap "baseUrl" parameter must be specified, ensure that ' + 'the "cMapUrl" and "cMapPacked" API parameters are provided.');
-    }
-
-    if (!name) {
-      throw new Error("CMap name must be specified.");
-    }
-
-    const url = this.baseUrl + name + (this.isCompressed ? ".bcmap" : "");
-    const compressionType = this.isCompressed ? _util.CMapCompressionType.BINARY : _util.CMapCompressionType.NONE;
-    return new Promise((resolve, reject) => {
-      const fs = require("fs");
-
-      fs.readFile(url, (error, data) => {
-        if (error || !data) {
-          reject(new Error(error));
-          return;
-        }
-
-        resolve({
-          cMapData: new Uint8Array(data),
-          compressionType
-        });
-      });
-    }).catch(reason => {
-      throw new Error(`Unable to load ${this.isCompressed ? "binary " : ""}` + `CMap at: ${url}`);
-    });
-  }
-
-}
-
-exports.NodeCMapReaderFactory = NodeCMapReaderFactory;
-
 class XRefMock {
   constructor(array) {
     this._map = Object.create(null);
+    this.stats = {
+      streamTypes: Object.create(null),
+      fontTypes: Object.create(null)
+    };
+    this._newRefNum = null;
 
     for (const key in array) {
       const obj = array[key];
@@ -179,6 +110,18 @@ class XRefMock {
     }
   }
 
+  getNewRef() {
+    if (this._newRefNum === null) {
+      this._newRefNum = Object.keys(this._map).length;
+    }
+
+    return _primitives.Ref.get(this._newRefNum++, 0);
+  }
+
+  resetNewRef() {
+    this.newRef = null;
+  }
+
   fetch(ref) {
     return this._map[ref.toString()];
   }
@@ -204,14 +147,24 @@ class XRefMock {
 exports.XRefMock = XRefMock;
 
 function createIdFactory(pageIndex) {
-  const page = new _document.Page({
-    pdfManager: {
-      get docId() {
-        return "d0";
-      }
+  const pdfManager = {
+    get docId() {
+      return "d0";
+    }
 
-    },
-    pageIndex
+  };
+  const stream = new _stream.StringStream("Dummy_PDF_data");
+  const pdfDocument = new _document.PDFDocument(pdfManager, stream);
+  const page = new _document.Page({
+    pdfManager: pdfDocument.pdfManager,
+    xref: pdfDocument.xref,
+    pageIndex,
+    globalIdFactory: pdfDocument._globalIdFactory
   });
-  return page.idFactory;
+  return page._localIdFactory;
+}
+
+function isEmptyObj(obj) {
+  (0, _util.assert)(typeof obj === "object" && obj !== null, "isEmptyObj - invalid argument.");
+  return Object.keys(obj).length === 0;
 }

+ 17 - 6
lib/test/unit/testreporter.js

@@ -72,10 +72,22 @@ var TestReporter = function (browser) {
 
   this.jasmineStarted = function (suiteInfo) {
     this.runnerStartTime = this.now();
-    sendInfo("Started tests for " + browser + ".");
+    const total = suiteInfo.totalSpecsDefined;
+    const seed = suiteInfo.order.seed;
+    sendInfo(`Started ${total} tests for ${browser} with seed ${seed}.`);
   };
 
-  this.suiteStarted = function (result) {};
+  this.suiteStarted = function (result) {
+    if (result.failedExpectations.length > 0) {
+      let failedMessages = "";
+
+      for (const item of result.failedExpectations) {
+        failedMessages += `${item.message} `;
+      }
+
+      sendResult("TEST-UNEXPECTED-FAIL", result.description, failedMessages);
+    }
+  };
 
   this.specStarted = function (result) {};
 
@@ -83,11 +95,10 @@ var TestReporter = function (browser) {
     if (result.failedExpectations.length === 0) {
       sendResult("TEST-PASSED", result.description);
     } else {
-      var failedMessages = "";
-      var items = result.failedExpectations;
+      let failedMessages = "";
 
-      for (var i = 0, ii = items.length; i < ii; i++) {
-        failedMessages += items[i].message + " ";
+      for (const item of result.failedExpectations) {
+        failedMessages += `${item.message} `;
       }
 
       sendResult("TEST-UNEXPECTED-FAIL", result.description, failedMessages);

+ 13 - 12
lib/test/unit/util_spec.js

@@ -72,16 +72,6 @@ describe("util", function () {
       expect((0, _util.isBool)(undefined)).toEqual(false);
     });
   });
-  describe("isEmptyObj", function () {
-    it("handles empty objects", function () {
-      expect((0, _util.isEmptyObj)({})).toEqual(true);
-    });
-    it("handles non-empty objects", function () {
-      expect((0, _util.isEmptyObj)({
-        foo: "bar"
-      })).toEqual(false);
-    });
-  });
   describe("isNum", function () {
     it("handles numeric values", function () {
       expect((0, _util.isNum)(1)).toEqual(true);
@@ -195,10 +185,10 @@ describe("util", function () {
       expect((0, _util.createValidAbsoluteUrl)(null, null)).toEqual(null);
       expect((0, _util.createValidAbsoluteUrl)("/foo", "/bar")).toEqual(null);
     });
-    it("handles URLs that do not use a whitelisted protocol", function () {
+    it("handles URLs that do not use an allowed protocol", function () {
       expect((0, _util.createValidAbsoluteUrl)("magnet:?foo", null)).toEqual(null);
     });
-    it("correctly creates a valid URL for whitelisted protocols", function () {
+    it("correctly creates a valid URL for allowed protocols", function () {
       expect((0, _util.createValidAbsoluteUrl)("http://www.mozilla.org/foo", null)).toEqual(new URL("http://www.mozilla.org/foo"));
       expect((0, _util.createValidAbsoluteUrl)("/foo", "http://www.mozilla.org")).toEqual(new URL("http://www.mozilla.org/foo"));
       expect((0, _util.createValidAbsoluteUrl)("https://www.mozilla.org/foo", null)).toEqual(new URL("https://www.mozilla.org/foo"));
@@ -238,4 +228,15 @@ describe("util", function () {
       });
     });
   });
+  describe("escapeString", function () {
+    it("should escape (, ) and \\", function () {
+      expect((0, _util.escapeString)("((a\\a))(b(b\\b)b)")).toEqual("\\(\\(a\\\\a\\)\\)\\(b\\(b\\\\b\\)b\\)");
+    });
+  });
+  describe("getModificationDate", function () {
+    it("should get a correctly formatted date", function () {
+      const date = new Date(Date.UTC(3141, 5, 9, 2, 6, 53));
+      expect((0, _util.getModificationDate)(date)).toEqual("31410610020653");
+    });
+  });
 });

+ 84 - 0
lib/test/unit/writer_spec.js

@@ -0,0 +1,84 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+var _primitives = require("../../core/primitives.js");
+
+var _writer = require("../../core/writer.js");
+
+var _util = require("../../shared/util.js");
+
+var _stream = require("../../core/stream.js");
+
+describe("Writer", function () {
+  describe("Incremental update", function () {
+    it("should update a file with new objects", function (done) {
+      const originalData = new Uint8Array();
+      const newRefs = [{
+        ref: _primitives.Ref.get(123, 0x2d),
+        data: "abc\n"
+      }, {
+        ref: _primitives.Ref.get(456, 0x4e),
+        data: "defg\n"
+      }];
+      const xrefInfo = {
+        newRef: _primitives.Ref.get(789, 0),
+        startXRef: 314,
+        fileIds: ["id", ""],
+        rootRef: null,
+        infoRef: null,
+        encrypt: null,
+        filename: "foo.pdf",
+        info: {}
+      };
+      let data = (0, _writer.incrementalUpdate)(originalData, xrefInfo, newRefs);
+      data = (0, _util.bytesToString)(data);
+      const expected = "\nabc\n" + "defg\n" + "789 0 obj\n" + "<< /Size 790 /Prev 314 /Type /XRef /Index [0 1 123 1 456 1 789 1] " + "/ID [(id) (\x01#Eg\x89\xab\xcd\xef\xfe\xdc\xba\x98vT2\x10)] " + "/W [1 1 2] /Length 16>> stream\n" + "\x00\x01\xff\xff" + "\x01\x01\x00\x2d" + "\x01\x05\x00\x4e" + "\x01\x0a\x00\x00\n" + "endstream\n" + "endobj\n" + "startxref\n" + "10\n" + "%%EOF\n";
+      expect(data).toEqual(expected);
+      done();
+    });
+  });
+  describe("writeDict", function () {
+    it("should write a Dict", function (done) {
+      const dict = new _primitives.Dict(null);
+      dict.set("A", _primitives.Name.get("B"));
+      dict.set("B", _primitives.Ref.get(123, 456));
+      dict.set("C", 789);
+      dict.set("D", "hello world");
+      dict.set("E", "(hello\\world)");
+      dict.set("F", [1.23001, 4.50001, 6]);
+      const gdict = new _primitives.Dict(null);
+      gdict.set("H", 123.00001);
+      const string = "a stream";
+      const stream = new _stream.StringStream(string);
+      stream.dict = new _primitives.Dict(null);
+      stream.dict.set("Length", string.length);
+      gdict.set("I", stream);
+      dict.set("G", gdict);
+      const buffer = [];
+      (0, _writer.writeDict)(dict, buffer, null);
+      const expected = "<< /A /B /B 123 456 R /C 789 /D (hello world) " + "/E (\\(hello\\\\world\\)) /F [1.23 4.5 6] " + "/G << /H 123 /I << /Length 8>> stream\n" + "a stream\n" + "endstream\n>>>>";
+      expect(buffer.join("")).toEqual(expected);
+      done();
+    });
+  });
+});

+ 13 - 9
lib/web/annotation_layer_builder.js

@@ -38,8 +38,9 @@ class AnnotationLayerBuilder {
     pdfPage,
     linkService,
     downloadManager,
+    annotationStorage = null,
     imageResourcesPath = "",
-    renderInteractiveForms = false,
+    renderInteractiveForms = true,
     l10n = _ui_utils.NullL10n
   }) {
     this.pageDiv = pageDiv;
@@ -49,18 +50,23 @@ class AnnotationLayerBuilder {
     this.imageResourcesPath = imageResourcesPath;
     this.renderInteractiveForms = renderInteractiveForms;
     this.l10n = l10n;
+    this.annotationStorage = annotationStorage;
     this.div = null;
     this._cancelled = false;
   }
 
   render(viewport, intent = "display") {
-    this.pdfPage.getAnnotations({
+    return this.pdfPage.getAnnotations({
       intent
     }).then(annotations => {
       if (this._cancelled) {
         return;
       }
 
+      if (annotations.length === 0) {
+        return;
+      }
+
       const parameters = {
         viewport: viewport.clone({
           dontFlip: true
@@ -71,16 +77,13 @@ class AnnotationLayerBuilder {
         imageResourcesPath: this.imageResourcesPath,
         renderInteractiveForms: this.renderInteractiveForms,
         linkService: this.linkService,
-        downloadManager: this.downloadManager
+        downloadManager: this.downloadManager,
+        annotationStorage: this.annotationStorage
       };
 
       if (this.div) {
         _pdf.AnnotationLayer.update(parameters);
       } else {
-        if (annotations.length === 0) {
-          return;
-        }
-
         this.div = document.createElement("div");
         this.div.className = "annotationLayer";
         this.pageDiv.appendChild(this.div);
@@ -110,14 +113,15 @@ class AnnotationLayerBuilder {
 exports.AnnotationLayerBuilder = AnnotationLayerBuilder;
 
 class DefaultAnnotationLayerFactory {
-  createAnnotationLayerBuilder(pageDiv, pdfPage, imageResourcesPath = "", renderInteractiveForms = false, l10n = _ui_utils.NullL10n) {
+  createAnnotationLayerBuilder(pageDiv, pdfPage, annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = true, l10n = _ui_utils.NullL10n) {
     return new AnnotationLayerBuilder({
       pageDiv,
       pdfPage,
       imageResourcesPath,
       renderInteractiveForms,
       linkService: new _pdf_link_service.SimpleLinkService(),
-      l10n
+      l10n,
+      annotationStorage
     });
   }
 

+ 231 - 26
lib/web/app.js

@@ -52,6 +52,8 @@ var _pdf_find_controller = require("./pdf_find_controller.js");
 
 var _pdf_history = require("./pdf_history.js");
 
+var _pdf_layer_viewer = require("./pdf_layer_viewer.js");
+
 var _pdf_link_service = require("./pdf_link_service.js");
 
 var _pdf_outline_viewer = require("./pdf_outline_viewer.js");
@@ -68,6 +70,8 @@ var _secondary_toolbar = require("./secondary_toolbar.js");
 
 var _toolbar = require("./toolbar.js");
 
+var _viewer_compatibility = require("./viewer_compatibility.js");
+
 var _view_history = require("./view_history.js");
 
 const DEFAULT_SCALE_DELTA = 1.1;
@@ -80,6 +84,8 @@ const ViewOnLoad = {
   PREVIOUS: 0,
   INITIAL: 1
 };
+const KNOWN_VERSIONS = ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9", "2.0", "2.1", "2.2", "2.3"];
+const KNOWN_GENERATORS = ["acrobat distiller", "acrobat pdfwriter", "adobe livecycle", "adobe pdf library", "adobe photoshop", "ghostscript", "tcpdf", "cairo", "dvipdfm", "dvips", "pdftex", "pdfkit", "itext", "prince", "quarkxpress", "mac os x", "microsoft", "openoffice", "oracle", "luradocument", "pdf-xchange", "antenna house", "aspose.cells", "fpdf"];
 
 class DefaultExternalServices {
   constructor() {
@@ -149,6 +155,7 @@ const PDFViewerApplication = {
   pdfSidebarResizer: null,
   pdfOutlineViewer: null,
   pdfAttachmentViewer: null,
+  pdfLayerViewer: null,
   pdfCursorTools: null,
   store: null,
   downloadManager: null,
@@ -166,6 +173,9 @@ const PDFViewerApplication = {
   externalServices: DefaultExternalServices,
   _boundEvents: {},
   contentDispositionFilename: null,
+  triggerDelayedFallback: null,
+  _saveInProgress: false,
+  _wheelUnusedTicks: 0,
 
   async initialize(appConfig) {
     this.preferences = this.externalServices.createPreferences();
@@ -312,9 +322,7 @@ const PDFViewerApplication = {
       ignoreDestinationZoom: _app_options.AppOptions.get("ignoreDestinationZoom")
     });
     this.pdfLinkService = pdfLinkService;
-    const downloadManager = this.externalServices.createDownloadManager({
-      disableCreateObjectURL: _app_options.AppOptions.get("disableCreateObjectURL")
-    });
+    const downloadManager = this.externalServices.createDownloadManager();
     this.downloadManager = downloadManager;
     const findController = new _pdf_find_controller.PDFFindController({
       linkService: pdfLinkService,
@@ -345,6 +353,7 @@ const PDFViewerApplication = {
     pdfLinkService.setViewer(this.pdfViewer);
     this.pdfThumbnailViewer = new _pdf_thumbnail_viewer.PDFThumbnailViewer({
       container: appConfig.sidebar.thumbnailView,
+      eventBus,
       renderingQueue: pdfRenderingQueue,
       linkService: pdfLinkService,
       l10n: this.l10n
@@ -389,6 +398,11 @@ const PDFViewerApplication = {
       eventBus,
       downloadManager
     });
+    this.pdfLayerViewer = new _pdf_layer_viewer.PDFLayerViewer({
+      container: appConfig.sidebar.layersView,
+      eventBus,
+      l10n: this.l10n
+    });
     this.pdfSidebar = new _pdf_sidebar.PDFSidebar({
       elements: appConfig.sidebar,
       pdfViewer: this.pdfViewer,
@@ -555,9 +569,12 @@ const PDFViewerApplication = {
     this.url = "";
     this.baseUrl = "";
     this.contentDispositionFilename = null;
+    this.triggerDelayedFallback = null;
+    this._saveInProgress = false;
     this.pdfSidebar.reset();
     this.pdfOutlineViewer.reset();
     this.pdfAttachmentViewer.reset();
+    this.pdfLayerViewer.reset();
 
     if (this.pdfHistory) {
       this.pdfHistory.reset();
@@ -668,7 +685,9 @@ const PDFViewerApplication = {
     });
   },
 
-  download() {
+  download({
+    sourceEventType = "download"
+  } = {}) {
     function downloadByUrl() {
       downloadManager.downloadUrl(url, filename);
     }
@@ -690,11 +709,67 @@ const PDFViewerApplication = {
       const blob = new Blob([data], {
         type: "application/pdf"
       });
-      downloadManager.download(blob, url, filename);
+      downloadManager.download(blob, url, filename, sourceEventType);
     }).catch(downloadByUrl);
   },
 
+  save({
+    sourceEventType = "download"
+  } = {}) {
+    if (this._saveInProgress) {
+      return;
+    }
+
+    const url = this.baseUrl;
+    const filename = this.contentDispositionFilename || (0, _ui_utils.getPDFFileNameFromURL)(this.url);
+    const downloadManager = this.downloadManager;
+
+    downloadManager.onerror = err => {
+      this.error(`PDF failed to be saved: ${err}`);
+    };
+
+    if (!this.pdfDocument || !this.downloadComplete) {
+      this.download({
+        sourceEventType
+      });
+      return;
+    }
+
+    this._saveInProgress = true;
+    this.pdfDocument.saveDocument(this.pdfDocument.annotationStorage).then(data => {
+      const blob = new Blob([data], {
+        type: "application/pdf"
+      });
+      downloadManager.download(blob, url, filename, sourceEventType);
+    }).catch(() => {
+      this.download({
+        sourceEventType
+      });
+    }).finally(() => {
+      this._saveInProgress = false;
+    });
+  },
+
+  _delayedFallback(featureId) {
+    this.externalServices.reportTelemetry({
+      type: "unsupportedFeature",
+      featureId
+    });
+
+    if (!this.triggerDelayedFallback) {
+      this.triggerDelayedFallback = () => {
+        this.fallback(featureId);
+        this.triggerDelayedFallback = null;
+      };
+    }
+  },
+
   fallback(featureId) {
+    this.externalServices.reportTelemetry({
+      type: "unsupportedFeature",
+      featureId
+    });
+
     if (this.fellback) {
       return;
     }
@@ -708,7 +783,9 @@ const PDFViewerApplication = {
         return;
       }
 
-      PDFViewerApplication.download();
+      PDFViewerApplication.download({
+        sourceEventType: "download"
+      });
     });
   },
 
@@ -826,6 +903,16 @@ const PDFViewerApplication = {
     baseDocumentUrl = null;
     this.pdfLinkService.setDocument(pdfDocument, baseDocumentUrl);
     this.pdfDocumentProperties.setDocument(pdfDocument, this.url);
+    const annotationStorage = pdfDocument.annotationStorage;
+
+    annotationStorage.onSetModified = function () {
+      window.addEventListener("beforeunload", beforeUnload);
+    };
+
+    annotationStorage.onResetModified = function () {
+      window.removeEventListener("beforeunload", beforeUnload);
+    };
+
     const pdfViewer = this.pdfViewer;
     pdfViewer.setDocument(pdfDocument);
     const {
@@ -947,6 +1034,12 @@ const PDFViewerApplication = {
           attachments
         });
       });
+      pdfViewer.optionalContentConfigPromise.then(optionalContentConfig => {
+        this.pdfLayerViewer.render({
+          optionalContentConfig,
+          pdfDocument
+        });
+      });
     });
 
     this._initializePageLabels(pdfDocument);
@@ -974,7 +1067,9 @@ const PDFViewerApplication = {
         }
 
         console.warn("Warning: JavaScript is not supported");
-        this.fallback(_pdf.UNSUPPORTED_FEATURES.javaScript);
+
+        this._delayedFallback(_pdf.UNSUPPORTED_FEATURES.javaScript);
+
         return true;
       });
 
@@ -1035,20 +1130,23 @@ const PDFViewerApplication = {
       this.setTitle(contentDispositionFilename);
     }
 
-    if (info.IsAcroFormPresent) {
-      console.warn("Warning: AcroForm/XFA is not supported");
-      this.fallback(_pdf.UNSUPPORTED_FEATURES.forms);
+    if (info.IsXFAPresent && !info.IsAcroFormPresent) {
+      console.warn("Warning: XFA is not supported");
+
+      this._delayedFallback(_pdf.UNSUPPORTED_FEATURES.forms);
+    } else if ((info.IsAcroFormPresent || info.IsXFAPresent) && !this.pdfViewer.renderInteractiveForms) {
+      console.warn("Warning: Interactive form support is not enabled");
+
+      this._delayedFallback(_pdf.UNSUPPORTED_FEATURES.forms);
     }
 
     let versionId = "other";
-    const KNOWN_VERSIONS = ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9", "2.0", "2.1", "2.2", "2.3"];
 
     if (KNOWN_VERSIONS.includes(info.PDFFormatVersion)) {
       versionId = `v${info.PDFFormatVersion.replace(".", "_")}`;
     }
 
     let generatorId = "other";
-    const KNOWN_GENERATORS = ["acrobat distiller", "acrobat pdfwriter", "adobe livecycle", "adobe pdf library", "adobe photoshop", "ghostscript", "tcpdf", "cairo", "dvipdfm", "dvips", "pdftex", "pdfkit", "itext", "prince", "quarkxpress", "mac os x", "microsoft", "openoffice", "oracle", "luradocument", "pdf-xchange", "antenna house", "aspose.cells", "fpdf"];
 
     if (info.Producer) {
       const producer = info.Producer.toLowerCase();
@@ -1064,8 +1162,10 @@ const PDFViewerApplication = {
 
     let formType = null;
 
-    if (info.IsAcroFormPresent) {
-      formType = info.IsXFAPresent ? "xfa" : "acroform";
+    if (info.IsXFAPresent) {
+      formType = "xfa";
+    } else if (info.IsAcroFormPresent) {
+      formType = "acroform";
     }
 
     this.externalServices.reportTelemetry({
@@ -1244,7 +1344,11 @@ const PDFViewerApplication = {
 
     const pagesOverview = this.pdfViewer.getPagesOverview();
     const printContainer = this.appConfig.printContainer;
-    const printService = PDFPrintServiceFactory.instance.createPrintService(this.pdfDocument, pagesOverview, printContainer, this.l10n);
+
+    const printResolution = _app_options.AppOptions.get("printResolution");
+
+    const optionalContentConfigPromise = this.pdfViewer.optionalContentConfigPromise;
+    const printService = PDFPrintServiceFactory.instance.createPrintService(this.pdfDocument, pagesOverview, printContainer, printResolution, optionalContentConfigPromise, this.l10n);
     this.printService = printService;
     this.forceRendering();
     printService.layout();
@@ -1257,6 +1361,10 @@ const PDFViewerApplication = {
     if (this.printService) {
       this.printService.destroy();
       this.printService = null;
+
+      if (this.pdfDocument) {
+        this.pdfDocument.annotationStorage.resetModified();
+      }
     }
 
     this.forceRendering();
@@ -1319,6 +1427,8 @@ const PDFViewerApplication = {
 
     eventBus._on("download", webViewerDownload);
 
+    eventBus._on("save", webViewerSave);
+
     eventBus._on("firstpage", webViewerFirstPage);
 
     eventBus._on("lastpage", webViewerLastPage);
@@ -1341,6 +1451,8 @@ const PDFViewerApplication = {
 
     eventBus._on("rotateccw", webViewerRotateCcw);
 
+    eventBus._on("optionalcontentconfig", webViewerOptionalContentConfig);
+
     eventBus._on("switchscrollmode", webViewerSwitchScrollMode);
 
     eventBus._on("scrollmodechanged", webViewerScrollModeChanged);
@@ -1399,8 +1511,12 @@ const PDFViewerApplication = {
     window.addEventListener("wheel", webViewerWheel, {
       passive: false
     });
+    window.addEventListener("touchstart", webViewerTouchStart, {
+      passive: false
+    });
     window.addEventListener("click", webViewerClick);
     window.addEventListener("keydown", webViewerKeyDown);
+    window.addEventListener("keyup", webViewerKeyUp);
     window.addEventListener("resize", _boundEvents.windowResize);
     window.addEventListener("hashchange", _boundEvents.windowHashChange);
     window.addEventListener("beforeprint", _boundEvents.windowBeforePrint);
@@ -1445,6 +1561,8 @@ const PDFViewerApplication = {
 
     eventBus._off("download", webViewerDownload);
 
+    eventBus._off("save", webViewerSave);
+
     eventBus._off("firstpage", webViewerFirstPage);
 
     eventBus._off("lastpage", webViewerLastPage);
@@ -1467,6 +1585,8 @@ const PDFViewerApplication = {
 
     eventBus._off("rotateccw", webViewerRotateCcw);
 
+    eventBus._off("optionalcontentconfig", webViewerOptionalContentConfig);
+
     eventBus._off("switchscrollmode", webViewerSwitchScrollMode);
 
     eventBus._off("scrollmodechanged", webViewerScrollModeChanged);
@@ -1501,8 +1621,12 @@ const PDFViewerApplication = {
     window.removeEventListener("wheel", webViewerWheel, {
       passive: false
     });
+    window.removeEventListener("touchstart", webViewerTouchStart, {
+      passive: false
+    });
     window.removeEventListener("click", webViewerClick);
     window.removeEventListener("keydown", webViewerKeyDown);
+    window.removeEventListener("keyup", webViewerKeyUp);
     window.removeEventListener("resize", _boundEvents.windowResize);
     window.removeEventListener("hashchange", _boundEvents.windowHashChange);
     window.removeEventListener("beforeprint", _boundEvents.windowBeforePrint);
@@ -1511,6 +1635,17 @@ const PDFViewerApplication = {
     _boundEvents.windowHashChange = null;
     _boundEvents.windowBeforePrint = null;
     _boundEvents.windowAfterPrint = null;
+  },
+
+  accumulateWheelTicks(ticks) {
+    if (this._wheelUnusedTicks > 0 && ticks < 0 || this._wheelUnusedTicks < 0 && ticks > 0) {
+      this._wheelUnusedTicks = 0;
+    }
+
+    this._wheelUnusedTicks += ticks;
+    const wholeTicks = Math.sign(this._wheelUnusedTicks) * Math.floor(Math.abs(this._wheelUnusedTicks));
+    this._wheelUnusedTicks -= wholeTicks;
+    return wholeTicks;
   }
 
 };
@@ -1753,6 +1888,10 @@ function webViewerPageMode({
       view = _pdf_sidebar.SidebarView.ATTACHMENTS;
       break;
 
+    case "layers":
+      view = _pdf_sidebar.SidebarView.LAYERS;
+      break;
+
     case "none":
       view = _pdf_sidebar.SidebarView.NONE;
       break;
@@ -1766,9 +1905,7 @@ function webViewerPageMode({
 }
 
 function webViewerNamedAction(evt) {
-  const action = evt.action;
-
-  switch (action) {
+  switch (evt.action) {
     case "GoToPage":
       PDFViewerApplication.appConfig.toolbar.pageNumber.select();
       break;
@@ -1779,6 +1916,17 @@ function webViewerNamedAction(evt) {
       }
 
       break;
+
+    case "Print":
+      if (PDFViewerApplication.supportsPrinting) {
+        webViewerPrint();
+      }
+
+      break;
+
+    case "SaveAs":
+      webViewerSave();
+      break;
   }
 }
 
@@ -1886,7 +2034,7 @@ let webViewerFileInputChange, webViewerOpenFile;
 
     const file = evt.fileInput.files[0];
 
-    if (!_app_options.AppOptions.get("disableCreateObjectURL")) {
+    if (!_viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL) {
       let url = URL.createObjectURL(file);
 
       if (file.name) {
@@ -1930,8 +2078,24 @@ function webViewerPrint() {
   window.print();
 }
 
+function webViewerDownloadOrSave(sourceEventType) {
+  if (PDFViewerApplication.pdfDocument && PDFViewerApplication.pdfDocument.annotationStorage.size > 0) {
+    PDFViewerApplication.save({
+      sourceEventType
+    });
+  } else {
+    PDFViewerApplication.download({
+      sourceEventType
+    });
+  }
+}
+
 function webViewerDownload() {
-  PDFViewerApplication.download();
+  webViewerDownloadOrSave("download");
+}
+
+function webViewerSave() {
+  webViewerDownloadOrSave("save");
 }
 
 function webViewerFirstPage() {
@@ -1990,6 +2154,10 @@ function webViewerRotateCcw() {
   PDFViewerApplication.rotatePages(-90);
 }
 
+function webViewerOptionalContentConfig(evt) {
+  PDFViewerApplication.pdfViewer.optionalContentConfigPromise = evt.promise;
+}
+
 function webViewerSwitchScrollMode(evt) {
   PDFViewerApplication.pdfViewer.scrollMode = evt.mode;
 }
@@ -2037,13 +2205,15 @@ function webViewerUpdateFindMatchesCount({
 function webViewerUpdateFindControlState({
   state,
   previous,
-  matchesCount
+  matchesCount,
+  rawQuery
 }) {
   if (PDFViewerApplication.supportsIntegratedFind) {
     PDFViewerApplication.externalServices.updateFindControlState({
       result: state,
       findPrevious: previous,
-      matchesCount
+      matchesCount,
+      rawQuery
     });
   } else {
     PDFViewerApplication.findBar.updateUIState(state, previous, matchesCount);
@@ -2115,13 +2285,23 @@ function webViewerWheel(evt) {
     }
 
     const previousScale = pdfViewer.currentScale;
-    const delta = (0, _ui_utils.normalizeWheelEventDelta)(evt);
-    const MOUSE_WHEEL_DELTA_PER_PAGE_SCALE = 3.0;
-    const ticks = delta * MOUSE_WHEEL_DELTA_PER_PAGE_SCALE;
+    const delta = (0, _ui_utils.normalizeWheelEventDirection)(evt);
+    let ticks = 0;
+
+    if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE || evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
+      if (Math.abs(delta) >= 1) {
+        ticks = Math.sign(delta);
+      } else {
+        ticks = PDFViewerApplication.accumulateWheelTicks(delta);
+      }
+    } else {
+      const PIXELS_PER_LINE_SCALE = 30;
+      ticks = PDFViewerApplication.accumulateWheelTicks(delta / PIXELS_PER_LINE_SCALE);
+    }
 
     if (ticks < 0) {
       PDFViewerApplication.zoomOut(-ticks);
-    } else {
+    } else if (ticks > 0) {
       PDFViewerApplication.zoomIn(ticks);
     }
 
@@ -2140,7 +2320,17 @@ function webViewerWheel(evt) {
   }
 }
 
+function webViewerTouchStart(evt) {
+  if (evt.touches.length > 1) {
+    evt.preventDefault();
+  }
+}
+
 function webViewerClick(evt) {
+  if (PDFViewerApplication.triggerDelayedFallback && PDFViewerApplication.pdfViewer.containsElement(evt.target)) {
+    PDFViewerApplication.triggerDelayedFallback();
+  }
+
   if (!PDFViewerApplication.secondaryToolbar.isOpen) {
     return;
   }
@@ -2152,6 +2342,14 @@ function webViewerClick(evt) {
   }
 }
 
+function webViewerKeyUp(evt) {
+  if (evt.keyCode === 9) {
+    if (PDFViewerApplication.triggerDelayedFallback) {
+      PDFViewerApplication.triggerDelayedFallback();
+    }
+  }
+}
+
 function webViewerKeyDown(evt) {
   if (PDFViewerApplication.overlayManager.active) {
     return;
@@ -2460,6 +2658,12 @@ function webViewerKeyDown(evt) {
   }
 }
 
+function beforeUnload(evt) {
+  evt.preventDefault();
+  evt.returnValue = "";
+  return false;
+}
+
 function apiPageLayoutToSpreadMode(layout) {
   switch (layout) {
     case "SinglePage":
@@ -2493,6 +2697,7 @@ function apiPageModeToSidebarView(mode) {
       return _pdf_sidebar.SidebarView.ATTACHMENTS;
 
     case "UseOC":
+      return _pdf_sidebar.SidebarView.LAYERS;
   }
 
   return _pdf_sidebar.SidebarView.NONE;

+ 1 - 6
lib/web/app_options.js

@@ -48,11 +48,6 @@ const defaultOptions = {
     value: "",
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE
   },
-  disableCreateObjectURL: {
-    value: false,
-    compatibility: _viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL,
-    kind: OptionKind.VIEWER
-  },
   disableHistory: {
     value: false,
     kind: OptionKind.VIEWER
@@ -111,7 +106,7 @@ const defaultOptions = {
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE
   },
   renderInteractiveForms: {
-    value: false,
+    value: true,
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE
   },
   sidebarViewOnLoad: {

+ 100 - 0
lib/web/base_tree_viewer.js

@@ -0,0 +1,100 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.BaseTreeViewer = void 0;
+
+var _pdf = require("../pdf");
+
+class BaseTreeViewer {
+  constructor(options) {
+    if (this.constructor === BaseTreeViewer) {
+      throw new Error("Cannot initialize BaseTreeViewer.");
+    }
+
+    this.container = options.container;
+    this.eventBus = options.eventBus;
+    this.reset();
+  }
+
+  reset() {
+    this._lastToggleIsShow = true;
+    this.container.textContent = "";
+    this.container.classList.remove("treeWithDeepNesting");
+  }
+
+  _dispatchEvent(count) {
+    throw new Error("Not implemented: _dispatchEvent");
+  }
+
+  _bindLink(element, params) {
+    throw new Error("Not implemented: _bindLink");
+  }
+
+  _normalizeTextContent(str) {
+    return (0, _pdf.removeNullCharacters)(str) || "\u2013";
+  }
+
+  _addToggleButton(div, hidden = false) {
+    const toggler = document.createElement("div");
+    toggler.className = "treeItemToggler";
+
+    if (hidden) {
+      toggler.classList.add("treeItemsHidden");
+    }
+
+    toggler.onclick = evt => {
+      evt.stopPropagation();
+      toggler.classList.toggle("treeItemsHidden");
+
+      if (evt.shiftKey) {
+        const shouldShowAll = !toggler.classList.contains("treeItemsHidden");
+
+        this._toggleTreeItem(div, shouldShowAll);
+      }
+    };
+
+    div.insertBefore(toggler, div.firstChild);
+  }
+
+  _toggleTreeItem(root, show = false) {
+    this._lastToggleIsShow = show;
+
+    for (const toggler of root.querySelectorAll(".treeItemToggler")) {
+      toggler.classList.toggle("treeItemsHidden", !show);
+    }
+  }
+
+  _toggleAllTreeItems() {
+    this._toggleTreeItem(this.container, !this._lastToggleIsShow);
+  }
+
+  render(params) {
+    throw new Error("Not implemented: render");
+  }
+
+}
+
+exports.BaseTreeViewer = BaseTreeViewer;

+ 53 - 4
lib/web/base_viewer.js

@@ -101,6 +101,11 @@ class BaseViewer {
     this._name = this.constructor.name;
     this.container = options.container;
     this.viewer = options.viewer || options.container.firstElementChild;
+
+    if (!(this.container instanceof HTMLDivElement && this.viewer instanceof HTMLDivElement)) {
+      throw new Error("Invalid `container` and/or `viewer` option.");
+    }
+
     this.eventBus = options.eventBus;
     this.linkService = options.linkService || new _pdf_link_service.SimpleLinkService();
     this.downloadManager = options.downloadManager || null;
@@ -108,7 +113,7 @@ class BaseViewer {
     this.removePageBorders = options.removePageBorders || false;
     this.textLayerMode = Number.isInteger(options.textLayerMode) ? options.textLayerMode : _ui_utils.TextLayerMode.ENABLE;
     this.imageResourcesPath = options.imageResourcesPath || "";
-    this.renderInteractiveForms = options.renderInteractiveForms || false;
+    this.renderInteractiveForms = typeof options.renderInteractiveForms === "boolean" ? options.renderInteractiveForms : true;
     this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
     this.renderer = options.renderer || _ui_utils.RendererType.CANVAS;
     this.enableWebGL = options.enableWebGL || false;
@@ -339,6 +344,8 @@ class BaseViewer {
 
     const pagesCount = pdfDocument.numPages;
     const firstPagePromise = pdfDocument.getPage(1);
+    const annotationStorage = pdfDocument.annotationStorage;
+    const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
 
     this._pagesCapability.promise.then(() => {
       this.eventBus.dispatch("pagesloaded", {
@@ -376,6 +383,7 @@ class BaseViewer {
     firstPagePromise.then(firstPdfPage => {
       this._firstPageCapability.resolve(firstPdfPage);
 
+      this._optionalContentConfigPromise = optionalContentConfigPromise;
       const scale = this.currentScale;
       const viewport = firstPdfPage.getViewport({
         scale: scale * _ui_utils.CSS_UNITS
@@ -389,6 +397,8 @@ class BaseViewer {
           id: pageNum,
           scale,
           defaultViewport: viewport.clone(),
+          annotationStorage,
+          optionalContentConfigPromise,
           renderingQueue: this.renderingQueue,
           textLayerFactory,
           textLayerMode: this.textLayerMode,
@@ -500,6 +510,7 @@ class BaseViewer {
     this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE);
     this._location = null;
     this._pagesRotation = 0;
+    this._optionalContentConfigPromise = null;
     this._pagesRequests = new WeakMap();
     this._firstPageCapability = (0, _pdf.createPromiseCapability)();
     this._onePageRenderedCapability = (0, _pdf.createPromiseCapability)();
@@ -971,10 +982,11 @@ class BaseViewer {
     });
   }
 
-  createAnnotationLayerBuilder(pageDiv, pdfPage, imageResourcesPath = "", renderInteractiveForms = false, l10n = _ui_utils.NullL10n) {
+  createAnnotationLayerBuilder(pageDiv, pdfPage, annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = false, l10n = _ui_utils.NullL10n) {
     return new _annotation_layer_builder.AnnotationLayerBuilder({
       pageDiv,
       pdfPage,
+      annotationStorage,
       imageResourcesPath,
       renderInteractiveForms,
       linkService: this.linkService,
@@ -1013,9 +1025,8 @@ class BaseViewer {
       return pagesOverview;
     }
 
-    const isFirstPagePortrait = (0, _ui_utils.isPortraitOrientation)(pagesOverview[0]);
     return pagesOverview.map(function (size) {
-      if (isFirstPagePortrait === (0, _ui_utils.isPortraitOrientation)(size)) {
+      if ((0, _ui_utils.isPortraitOrientation)(size)) {
         return size;
       }
 
@@ -1027,6 +1038,44 @@ class BaseViewer {
     });
   }
 
+  get optionalContentConfigPromise() {
+    if (!this.pdfDocument) {
+      return Promise.resolve(null);
+    }
+
+    if (!this._optionalContentConfigPromise) {
+      return this.pdfDocument.getOptionalContentConfig();
+    }
+
+    return this._optionalContentConfigPromise;
+  }
+
+  set optionalContentConfigPromise(promise) {
+    if (!(promise instanceof Promise)) {
+      throw new Error(`Invalid optionalContentConfigPromise: ${promise}`);
+    }
+
+    if (!this.pdfDocument) {
+      return;
+    }
+
+    if (!this._optionalContentConfigPromise) {
+      return;
+    }
+
+    this._optionalContentConfigPromise = promise;
+
+    for (const pageView of this._pages) {
+      pageView.update(pageView.scale, pageView.rotation, promise);
+    }
+
+    this.update();
+    this.eventBus.dispatch("optionalcontentconfigchanged", {
+      source: this,
+      promise
+    });
+  }
+
   get scrollMode() {
     return this._scrollMode;
   }

+ 3 - 10
lib/web/download_manager.js

@@ -31,7 +31,6 @@ var _pdf = require("../pdf");
 var _viewer_compatibility = require("./viewer_compatibility.js");
 
 ;
-const DISABLE_CREATE_OBJECT_URL = _viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL || false;
 
 function download(blobUrl, filename) {
   const a = document.createElement("a");
@@ -53,12 +52,6 @@ function download(blobUrl, filename) {
 }
 
 class DownloadManager {
-  constructor({
-    disableCreateObjectURL = DISABLE_CREATE_OBJECT_URL
-  }) {
-    this.disableCreateObjectURL = disableCreateObjectURL;
-  }
-
   downloadUrl(url, filename) {
     if (!(0, _pdf.createValidAbsoluteUrl)(url, "http://example.com")) {
       return;
@@ -75,11 +68,11 @@ class DownloadManager {
       return;
     }
 
-    const blobUrl = (0, _pdf.createObjectURL)(data, contentType, this.disableCreateObjectURL);
+    const blobUrl = (0, _pdf.createObjectURL)(data, contentType, _viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL);
     download(blobUrl, filename);
   }
 
-  download(blob, url, filename) {
+  download(blob, url, filename, sourceEventType = "download") {
     if (navigator.msSaveBlob) {
       if (!navigator.msSaveBlob(blob, filename)) {
         this.downloadUrl(url, filename);
@@ -88,7 +81,7 @@ class DownloadManager {
       return;
     }
 
-    if (this.disableCreateObjectURL) {
+    if (_viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL) {
       this.downloadUrl(url, filename);
       return;
     }

+ 32 - 12
lib/web/firefox_print_service.js

@@ -26,18 +26,15 @@ Object.defineProperty(exports, "__esModule", {
 });
 exports.FirefoxPrintService = FirefoxPrintService;
 
-var _app_options = require("./app_options.js");
-
 var _ui_utils = require("./ui_utils.js");
 
 var _app = require("./app.js");
 
 var _pdf = require("../pdf");
 
-function composePage(pdfDocument, pageNumber, size, printContainer) {
+function composePage(pdfDocument, pageNumber, size, printContainer, printResolution, optionalContentConfigPromise) {
   const canvas = document.createElement("canvas");
-  const PRINT_RESOLUTION = _app_options.AppOptions.get("printResolution") || 150;
-  const PRINT_UNITS = PRINT_RESOLUTION / 72.0;
+  const PRINT_UNITS = printResolution / 72.0;
   canvas.width = Math.floor(size.width * PRINT_UNITS);
   canvas.height = Math.floor(size.height * PRINT_UNITS);
   canvas.style.width = Math.floor(size.width * _ui_utils.CSS_UNITS) + "px";
@@ -45,6 +42,7 @@ function composePage(pdfDocument, pageNumber, size, printContainer) {
   const canvasWrapper = document.createElement("div");
   canvasWrapper.appendChild(canvas);
   printContainer.appendChild(canvasWrapper);
+  let currentRenderTask = null;
 
   canvas.mozPrintCallback = function (obj) {
     const ctx = obj.context;
@@ -52,7 +50,13 @@ function composePage(pdfDocument, pageNumber, size, printContainer) {
     ctx.fillStyle = "rgb(255, 255, 255)";
     ctx.fillRect(0, 0, canvas.width, canvas.height);
     ctx.restore();
+    let thisRenderTask = null;
     pdfDocument.getPage(pageNumber).then(function (pdfPage) {
+      if (currentRenderTask) {
+        currentRenderTask.cancel();
+        currentRenderTask = null;
+      }
+
       const renderContext = {
         canvasContext: ctx,
         transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
@@ -60,14 +64,26 @@ function composePage(pdfDocument, pageNumber, size, printContainer) {
           scale: 1,
           rotation: size.rotation
         }),
-        intent: "print"
+        intent: "print",
+        annotationStorage: pdfDocument.annotationStorage,
+        optionalContentConfigPromise
       };
-      return pdfPage.render(renderContext).promise;
+      currentRenderTask = thisRenderTask = pdfPage.render(renderContext);
+      return thisRenderTask.promise;
     }).then(function () {
+      if (currentRenderTask === thisRenderTask) {
+        currentRenderTask = null;
+      }
+
       obj.done();
     }, function (error) {
       console.error(error);
 
+      if (currentRenderTask === thisRenderTask) {
+        currentRenderTask.cancel();
+        currentRenderTask = null;
+      }
+
       if ("abort" in obj) {
         obj.abort();
       } else {
@@ -77,10 +93,12 @@ function composePage(pdfDocument, pageNumber, size, printContainer) {
   };
 }
 
-function FirefoxPrintService(pdfDocument, pagesOverview, printContainer) {
+function FirefoxPrintService(pdfDocument, pagesOverview, printContainer, printResolution, optionalContentConfigPromise = null) {
   this.pdfDocument = pdfDocument;
   this.pagesOverview = pagesOverview;
   this.printContainer = printContainer;
+  this._printResolution = printResolution || 150;
+  this._optionalContentConfigPromise = optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
 }
 
 FirefoxPrintService.prototype = {
@@ -88,13 +106,15 @@ FirefoxPrintService.prototype = {
     const {
       pdfDocument,
       pagesOverview,
-      printContainer
+      printContainer,
+      _printResolution,
+      _optionalContentConfigPromise
     } = this;
     const body = document.querySelector("body");
     body.setAttribute("data-pdfjsprinting", true);
 
     for (let i = 0, ii = pagesOverview.length; i < ii; ++i) {
-      composePage(pdfDocument, i + 1, pagesOverview[i], printContainer);
+      composePage(pdfDocument, i + 1, pagesOverview[i], printContainer, _printResolution, _optionalContentConfigPromise);
     }
   },
 
@@ -112,8 +132,8 @@ _app.PDFPrintServiceFactory.instance = {
     return (0, _pdf.shadow)(this, "supportsPrinting", value);
   },
 
-  createPrintService(pdfDocument, pagesOverview, printContainer) {
-    return new FirefoxPrintService(pdfDocument, pagesOverview, printContainer);
+  createPrintService(pdfDocument, pagesOverview, printContainer, printResolution, optionalContentConfigPromise) {
+    return new FirefoxPrintService(pdfDocument, pagesOverview, printContainer, printResolution, optionalContentConfigPromise);
   }
 
 };

+ 20 - 6
lib/web/firefoxcom.js

@@ -87,10 +87,6 @@ const FirefoxCom = function FirefoxComClosure() {
 exports.FirefoxCom = FirefoxCom;
 
 class DownloadManager {
-  constructor(options) {
-    this.disableCreateObjectURL = false;
-  }
-
   downloadUrl(url, filename) {
     FirefoxCom.request("download", {
       originalUrl: url,
@@ -115,7 +111,7 @@ class DownloadManager {
     }, onResponse);
   }
 
-  download(blob, url, filename) {
+  download(blob, url, filename, sourceEventType = "download") {
     const blobUrl = URL.createObjectURL(blob);
 
     const onResponse = err => {
@@ -129,7 +125,8 @@ class DownloadManager {
     FirefoxCom.request("download", {
       blobUrl,
       originalUrl: url,
-      filename
+      filename,
+      sourceEventType
     }, onResponse);
   }
 
@@ -239,6 +236,23 @@ class MozL10n {
   }
 })();
 
+(function listenSaveEvent() {
+  const handleEvent = function ({
+    type,
+    detail
+  }) {
+    if (!_app.PDFViewerApplication.initialized) {
+      return;
+    }
+
+    _app.PDFViewerApplication.eventBus.dispatch(type, {
+      source: window
+    });
+  };
+
+  window.addEventListener("save", handleEvent);
+})();
+
 class FirefoxComDataRangeTransport extends _pdf.PDFDataRangeTransport {
   requestDataRange(begin, end) {
     FirefoxCom.request("requestDataRange", {

+ 1 - 1
lib/web/interfaces.js

@@ -105,7 +105,7 @@ class IPDFTextLayerFactory {
 exports.IPDFTextLayerFactory = IPDFTextLayerFactory;
 
 class IPDFAnnotationLayerFactory {
-  createAnnotationLayerBuilder(pageDiv, pdfPage, imageResourcesPath = "", renderInteractiveForms = false, l10n = undefined) {}
+  createAnnotationLayerBuilder(pageDiv, pdfPage, annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = true, l10n = undefined) {}
 
 }
 

+ 80 - 38
lib/web/pdf_attachment_viewer.js

@@ -28,42 +28,67 @@ exports.PDFAttachmentViewer = void 0;
 
 var _pdf = require("../pdf");
 
-class PDFAttachmentViewer {
-  constructor({
-    container,
-    eventBus,
-    downloadManager
-  }) {
-    this.container = container;
-    this.eventBus = eventBus;
-    this.downloadManager = downloadManager;
-    this.reset();
+var _base_tree_viewer = require("./base_tree_viewer.js");
+
+var _viewer_compatibility = require("./viewer_compatibility.js");
+
+const PdfFileRegExp = /\.pdf$/i;
+
+class PDFAttachmentViewer extends _base_tree_viewer.BaseTreeViewer {
+  constructor(options) {
+    super(options);
+    this.downloadManager = options.downloadManager;
 
     this.eventBus._on("fileattachmentannotation", this._appendAttachment.bind(this));
   }
 
   reset(keepRenderedCapability = false) {
-    this.attachments = null;
-    this.container.textContent = "";
+    super.reset();
+    this._attachments = null;
 
     if (!keepRenderedCapability) {
       this._renderedCapability = (0, _pdf.createPromiseCapability)();
     }
+
+    if (this._pendingDispatchEvent) {
+      clearTimeout(this._pendingDispatchEvent);
+    }
+
+    this._pendingDispatchEvent = null;
   }
 
   _dispatchEvent(attachmentsCount) {
     this._renderedCapability.resolve();
 
+    if (this._pendingDispatchEvent) {
+      clearTimeout(this._pendingDispatchEvent);
+      this._pendingDispatchEvent = null;
+    }
+
+    if (attachmentsCount === 0) {
+      this._pendingDispatchEvent = setTimeout(() => {
+        this.eventBus.dispatch("attachmentsloaded", {
+          source: this,
+          attachmentsCount: 0
+        });
+        this._pendingDispatchEvent = null;
+      });
+      return;
+    }
+
     this.eventBus.dispatch("attachmentsloaded", {
       source: this,
       attachmentsCount
     });
   }
 
-  _bindPdfLink(button, content, filename) {
+  _bindPdfLink(element, {
+    content,
+    filename
+  }) {
     let blobUrl;
 
-    button.onclick = () => {
+    element.onclick = () => {
       if (!blobUrl) {
         blobUrl = URL.createObjectURL(new Blob([content], {
           type: "application/pdf"
@@ -86,9 +111,13 @@ class PDFAttachmentViewer {
     };
   }
 
-  _bindLink(button, content, filename) {
-    button.onclick = () => {
-      this.downloadManager.downloadData(content, filename, "");
+  _bindLink(element, {
+    content,
+    filename
+  }) {
+    element.onclick = () => {
+      const contentType = PdfFileRegExp.test(filename) ? "application/pdf" : "";
+      this.downloadManager.downloadData(content, filename, contentType);
       return false;
     };
   }
@@ -97,16 +126,14 @@ class PDFAttachmentViewer {
     attachments,
     keepRenderedCapability = false
   }) {
-    let attachmentsCount = 0;
-
-    if (this.attachments) {
-      this.reset(keepRenderedCapability === true);
+    if (this._attachments) {
+      this.reset(keepRenderedCapability);
     }
 
-    this.attachments = attachments || null;
+    this._attachments = attachments || null;
 
     if (!attachments) {
-      this._dispatchEvent(attachmentsCount);
+      this._dispatchEvent(0);
 
       return;
     }
@@ -114,26 +141,36 @@ class PDFAttachmentViewer {
     const names = Object.keys(attachments).sort(function (a, b) {
       return a.toLowerCase().localeCompare(b.toLowerCase());
     });
-    attachmentsCount = names.length;
+    const fragment = document.createDocumentFragment();
+    let attachmentsCount = 0;
 
-    for (let i = 0; i < attachmentsCount; i++) {
-      const item = attachments[names[i]];
-      const filename = (0, _pdf.removeNullCharacters)((0, _pdf.getFilenameFromUrl)(item.filename));
+    for (const name of names) {
+      const item = attachments[name];
+      const filename = (0, _pdf.getFilenameFromUrl)(item.filename);
       const div = document.createElement("div");
-      div.className = "attachmentsItem";
-      const button = document.createElement("button");
-      button.textContent = filename;
-
-      if (/\.pdf$/i.test(filename) && !this.downloadManager.disableCreateObjectURL) {
-        this._bindPdfLink(button, item.content, filename);
+      div.className = "treeItem";
+      const element = document.createElement("a");
+
+      if (PdfFileRegExp.test(filename) && !_viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL) {
+        this._bindPdfLink(element, {
+          content: item.content,
+          filename
+        });
       } else {
-        this._bindLink(button, item.content, filename);
+        this._bindLink(element, {
+          content: item.content,
+          filename
+        });
       }
 
-      div.appendChild(button);
-      this.container.appendChild(div);
+      element.textContent = this._normalizeTextContent(filename);
+      div.appendChild(element);
+      fragment.appendChild(div);
+      attachmentsCount++;
     }
 
+    this.container.appendChild(fragment);
+
     this._dispatchEvent(attachmentsCount);
   }
 
@@ -142,8 +179,13 @@ class PDFAttachmentViewer {
     filename,
     content
   }) {
-    this._renderedCapability.promise.then(() => {
-      let attachments = this.attachments;
+    const renderedPromise = this._renderedCapability.promise;
+    renderedPromise.then(() => {
+      if (renderedPromise !== this._renderedCapability.promise) {
+        return;
+      }
+
+      let attachments = this._attachments;
 
       if (!attachments) {
         attachments = Object.create(null);

+ 2 - 1
lib/web/pdf_find_controller.js

@@ -687,7 +687,8 @@ class PDFFindController {
       source: this,
       state,
       previous,
-      matchesCount: this._requestMatchesCount()
+      matchesCount: this._requestMatchesCount(),
+      rawQuery: this._state ? this._state.query : null
     });
   }
 

+ 208 - 0
lib/web/pdf_layer_viewer.js

@@ -0,0 +1,208 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * Javascript code in this page
+ *
+ * Copyright 2020 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * Javascript code in this page
+ */
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.PDFLayerViewer = void 0;
+
+var _base_tree_viewer = require("./base_tree_viewer.js");
+
+class PDFLayerViewer extends _base_tree_viewer.BaseTreeViewer {
+  constructor(options) {
+    super(options);
+    this.l10n = options.l10n;
+
+    this.eventBus._on("resetlayers", this._resetLayers.bind(this));
+
+    this.eventBus._on("togglelayerstree", this._toggleAllTreeItems.bind(this));
+  }
+
+  reset() {
+    super.reset();
+    this._optionalContentConfig = null;
+    this._pdfDocument = null;
+  }
+
+  _dispatchEvent(layersCount) {
+    this.eventBus.dispatch("layersloaded", {
+      source: this,
+      layersCount
+    });
+  }
+
+  _bindLink(element, {
+    groupId,
+    input
+  }) {
+    const setVisibility = () => {
+      this._optionalContentConfig.setVisibility(groupId, input.checked);
+
+      this.eventBus.dispatch("optionalcontentconfig", {
+        source: this,
+        promise: Promise.resolve(this._optionalContentConfig)
+      });
+    };
+
+    element.onclick = evt => {
+      if (evt.target === input) {
+        setVisibility();
+        return true;
+      } else if (evt.target !== element) {
+        return true;
+      }
+
+      input.checked = !input.checked;
+      setVisibility();
+      return false;
+    };
+  }
+
+  async _setNestedName(element, {
+    name = null
+  }) {
+    if (typeof name === "string") {
+      element.textContent = this._normalizeTextContent(name);
+      return;
+    }
+
+    element.textContent = await this.l10n.get("additional_layers", null, "Additional Layers");
+    element.style.fontStyle = "italic";
+  }
+
+  _addToggleButton(div, {
+    name = null
+  }) {
+    super._addToggleButton(div, name === null);
+  }
+
+  _toggleAllTreeItems() {
+    if (!this._optionalContentConfig) {
+      return;
+    }
+
+    super._toggleAllTreeItems();
+  }
+
+  render({
+    optionalContentConfig,
+    pdfDocument
+  }) {
+    if (this._optionalContentConfig) {
+      this.reset();
+    }
+
+    this._optionalContentConfig = optionalContentConfig || null;
+    this._pdfDocument = pdfDocument || null;
+    const groups = optionalContentConfig && optionalContentConfig.getOrder();
+
+    if (!groups) {
+      this._dispatchEvent(0);
+
+      return;
+    }
+
+    const fragment = document.createDocumentFragment(),
+          queue = [{
+      parent: fragment,
+      groups
+    }];
+    let layersCount = 0,
+        hasAnyNesting = false;
+
+    while (queue.length > 0) {
+      const levelData = queue.shift();
+
+      for (const groupId of levelData.groups) {
+        const div = document.createElement("div");
+        div.className = "treeItem";
+        const element = document.createElement("a");
+        div.appendChild(element);
+
+        if (typeof groupId === "object") {
+          hasAnyNesting = true;
+
+          this._addToggleButton(div, groupId);
+
+          this._setNestedName(element, groupId);
+
+          const itemsDiv = document.createElement("div");
+          itemsDiv.className = "treeItems";
+          div.appendChild(itemsDiv);
+          queue.push({
+            parent: itemsDiv,
+            groups: groupId.order
+          });
+        } else {
+          const group = optionalContentConfig.getGroup(groupId);
+          const input = document.createElement("input");
+
+          this._bindLink(element, {
+            groupId,
+            input
+          });
+
+          input.type = "checkbox";
+          input.id = groupId;
+          input.checked = group.visible;
+          const label = document.createElement("label");
+          label.setAttribute("for", groupId);
+          label.textContent = this._normalizeTextContent(group.name);
+          element.appendChild(input);
+          element.appendChild(label);
+          layersCount++;
+        }
+
+        levelData.parent.appendChild(div);
+      }
+    }
+
+    if (hasAnyNesting) {
+      this.container.classList.add("treeWithDeepNesting");
+      this._lastToggleIsShow = fragment.querySelectorAll(".treeItemsHidden").length === 0;
+    }
+
+    this.container.appendChild(fragment);
+
+    this._dispatchEvent(layersCount);
+  }
+
+  async _resetLayers() {
+    if (!this._optionalContentConfig) {
+      return;
+    }
+
+    const optionalContentConfig = await this._pdfDocument.getOptionalContentConfig();
+    this.eventBus.dispatch("optionalcontentconfig", {
+      source: this,
+      promise: Promise.resolve(optionalContentConfig)
+    });
+    this.render({
+      optionalContentConfig,
+      pdfDocument: this._pdfDocument
+    });
+  }
+
+}
+
+exports.PDFLayerViewer = PDFLayerViewer;

+ 24 - 57
lib/web/pdf_outline_viewer.js

@@ -28,27 +28,19 @@ exports.PDFOutlineViewer = void 0;
 
 var _pdf = require("../pdf");
 
-const DEFAULT_TITLE = "\u2013";
+var _base_tree_viewer = require("./base_tree_viewer.js");
 
-class PDFOutlineViewer {
-  constructor({
-    container,
-    linkService,
-    eventBus
-  }) {
-    this.container = container;
-    this.linkService = linkService;
-    this.eventBus = eventBus;
-    this.reset();
+class PDFOutlineViewer extends _base_tree_viewer.BaseTreeViewer {
+  constructor(options) {
+    super(options);
+    this.linkService = options.linkService;
 
-    eventBus._on("toggleoutlinetree", this.toggleOutlineTree.bind(this));
+    this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this));
   }
 
   reset() {
-    this.outline = null;
-    this.lastToggleIsShow = true;
-    this.container.textContent = "";
-    this.container.classList.remove("outlineWithDeepNesting");
+    super.reset();
+    this._outline = null;
   }
 
   _dispatchEvent(outlineCount) {
@@ -105,56 +97,30 @@ class PDFOutlineViewer {
     count,
     items
   }) {
-    const toggler = document.createElement("div");
-    toggler.className = "outlineItemToggler";
-
-    if (count < 0 && Math.abs(count) === items.length) {
-      toggler.classList.add("outlineItemsHidden");
-    }
-
-    toggler.onclick = evt => {
-      evt.stopPropagation();
-      toggler.classList.toggle("outlineItemsHidden");
+    const hidden = count < 0 && Math.abs(count) === items.length;
 
-      if (evt.shiftKey) {
-        const shouldShowAll = !toggler.classList.contains("outlineItemsHidden");
-
-        this._toggleOutlineItem(div, shouldShowAll);
-      }
-    };
-
-    div.insertBefore(toggler, div.firstChild);
-  }
-
-  _toggleOutlineItem(root, show = false) {
-    this.lastToggleIsShow = show;
-
-    for (const toggler of root.querySelectorAll(".outlineItemToggler")) {
-      toggler.classList.toggle("outlineItemsHidden", !show);
-    }
+    super._addToggleButton(div, hidden);
   }
 
-  toggleOutlineTree() {
-    if (!this.outline) {
+  _toggleAllTreeItems() {
+    if (!this._outline) {
       return;
     }
 
-    this._toggleOutlineItem(this.container, !this.lastToggleIsShow);
+    super._toggleAllTreeItems();
   }
 
   render({
     outline
   }) {
-    let outlineCount = 0;
-
-    if (this.outline) {
+    if (this._outline) {
       this.reset();
     }
 
-    this.outline = outline || null;
+    this._outline = outline || null;
 
     if (!outline) {
-      this._dispatchEvent(outlineCount);
+      this._dispatchEvent(0);
 
       return;
     }
@@ -162,23 +128,24 @@ class PDFOutlineViewer {
     const fragment = document.createDocumentFragment();
     const queue = [{
       parent: fragment,
-      items: this.outline
+      items: outline
     }];
-    let hasAnyNesting = false;
+    let outlineCount = 0,
+        hasAnyNesting = false;
 
     while (queue.length > 0) {
       const levelData = queue.shift();
 
       for (const item of levelData.items) {
         const div = document.createElement("div");
-        div.className = "outlineItem";
+        div.className = "treeItem";
         const element = document.createElement("a");
 
         this._bindLink(element, item);
 
         this._setStyles(element, item);
 
-        element.textContent = (0, _pdf.removeNullCharacters)(item.title) || DEFAULT_TITLE;
+        element.textContent = this._normalizeTextContent(item.title);
         div.appendChild(element);
 
         if (item.items.length > 0) {
@@ -187,7 +154,7 @@ class PDFOutlineViewer {
           this._addToggleButton(div, item);
 
           const itemsDiv = document.createElement("div");
-          itemsDiv.className = "outlineItems";
+          itemsDiv.className = "treeItems";
           div.appendChild(itemsDiv);
           queue.push({
             parent: itemsDiv,
@@ -201,8 +168,8 @@ class PDFOutlineViewer {
     }
 
     if (hasAnyNesting) {
-      this.container.classList.add("outlineWithDeepNesting");
-      this.lastToggleIsShow = fragment.querySelectorAll(".outlineItemsHidden").length === 0;
+      this.container.classList.add("treeWithDeepNesting");
+      this._lastToggleIsShow = fragment.querySelectorAll(".treeItemsHidden").length === 0;
     }
 
     this.container.appendChild(fragment);

+ 29 - 6
lib/web/pdf_page_view.js

@@ -48,10 +48,12 @@ class PDFPageView {
     this.scale = options.scale || _ui_utils.DEFAULT_SCALE;
     this.viewport = defaultViewport;
     this.pdfPageRotate = defaultViewport.rotation;
+    this._annotationStorage = options.annotationStorage || null;
+    this._optionalContentConfigPromise = options.optionalContentConfigPromise || null;
     this.hasRestrictedScaling = false;
     this.textLayerMode = Number.isInteger(options.textLayerMode) ? options.textLayerMode : _ui_utils.TextLayerMode.ENABLE;
     this.imageResourcesPath = options.imageResourcesPath || "";
-    this.renderInteractiveForms = options.renderInteractiveForms || false;
+    this.renderInteractiveForms = typeof options.renderInteractiveForms === "boolean" ? options.renderInteractiveForms : true;
     this.useOnlyCssZoom = options.useOnlyCssZoom || false;
     this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS;
     this.eventBus = options.eventBus;
@@ -98,6 +100,22 @@ class PDFPageView {
     }
   }
 
+  async _renderAnnotationLayer() {
+    let error = null;
+
+    try {
+      await this.annotationLayer.render(this.viewport, "display");
+    } catch (ex) {
+      error = ex;
+    } finally {
+      this.eventBus.dispatch("annotationlayerrendered", {
+        source: this,
+        pageNumber: this.id,
+        error
+      });
+    }
+  }
+
   _resetZoomLayer(removeFromDOM = false) {
     if (!this.zoomLayer) {
       return;
@@ -165,13 +183,17 @@ class PDFPageView {
     div.appendChild(this.loadingIconDiv);
   }
 
-  update(scale, rotation) {
+  update(scale, rotation, optionalContentConfigPromise = null) {
     this.scale = scale || this.scale;
 
     if (typeof rotation !== "undefined") {
       this.rotation = rotation;
     }
 
+    if (optionalContentConfigPromise instanceof Promise) {
+      this._optionalContentConfigPromise = optionalContentConfigPromise;
+    }
+
     const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
     this.viewport = this.viewport.clone({
       scale: this.scale * _ui_utils.CSS_UNITS,
@@ -305,7 +327,7 @@ class PDFPageView {
     }
 
     if (redrawAnnotations && this.annotationLayer) {
-      this.annotationLayer.render(this.viewport, "display");
+      this._renderAnnotationLayer();
     }
   }
 
@@ -444,10 +466,10 @@ class PDFPageView {
 
     if (this.annotationLayerFactory) {
       if (!this.annotationLayer) {
-        this.annotationLayer = this.annotationLayerFactory.createAnnotationLayerBuilder(div, pdfPage, this.imageResourcesPath, this.renderInteractiveForms, this.l10n);
+        this.annotationLayer = this.annotationLayerFactory.createAnnotationLayerBuilder(div, pdfPage, this._annotationStorage, this.imageResourcesPath, this.renderInteractiveForms, this.l10n);
       }
 
-      this.annotationLayer.render(this.viewport, "display");
+      this._renderAnnotationLayer();
     }
 
     div.setAttribute("data-loaded", true);
@@ -534,7 +556,8 @@ class PDFPageView {
       transform,
       viewport: this.viewport,
       enableWebGL: this.enableWebGL,
-      renderInteractiveForms: this.renderInteractiveForms
+      renderInteractiveForms: this.renderInteractiveForms,
+      optionalContentConfigPromise: this._optionalContentConfigPromise
     };
     const renderTask = this.pdfPage.render(renderContext);
 

+ 13 - 11
lib/web/pdf_print_service.js

@@ -30,15 +30,14 @@ var _ui_utils = require("./ui_utils.js");
 
 var _app = require("./app.js");
 
-var _app_options = require("./app_options.js");
+var _viewer_compatibility = require("./viewer_compatibility.js");
 
 let activeService = null;
 let overlayManager = null;
 
-function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size) {
+function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size, printResolution, optionalContentConfigPromise) {
   const scratchCanvas = activeService.scratchCanvas;
-  const PRINT_RESOLUTION = _app_options.AppOptions.get("printResolution") || 150;
-  const PRINT_UNITS = PRINT_RESOLUTION / 72.0;
+  const PRINT_UNITS = printResolution / 72.0;
   scratchCanvas.width = Math.floor(size.width * PRINT_UNITS);
   scratchCanvas.height = Math.floor(size.height * PRINT_UNITS);
   const width = Math.floor(size.width * _ui_utils.CSS_UNITS) + "px";
@@ -56,7 +55,9 @@ function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size) {
         scale: 1,
         rotation: size.rotation
       }),
-      intent: "print"
+      intent: "print",
+      annotationStorage: pdfDocument.annotationStorage,
+      optionalContentConfigPromise
     };
     return pdfPage.render(renderContext).promise;
   }).then(function () {
@@ -67,12 +68,13 @@ function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size) {
   });
 }
 
-function PDFPrintService(pdfDocument, pagesOverview, printContainer, l10n) {
+function PDFPrintService(pdfDocument, pagesOverview, printContainer, printResolution, optionalContentConfigPromise = null, l10n) {
   this.pdfDocument = pdfDocument;
   this.pagesOverview = pagesOverview;
   this.printContainer = printContainer;
+  this._printResolution = printResolution || 150;
+  this._optionalContentConfigPromise = optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
   this.l10n = l10n || _ui_utils.NullL10n;
-  this.disableCreateObjectURL = _app_options.AppOptions.get("disableCreateObjectURL");
   this.currentPage = -1;
   this.scratchCanvas = document.createElement("canvas");
 }
@@ -136,7 +138,7 @@ PDFPrintService.prototype = {
 
       const index = this.currentPage;
       renderProgress(index, pageCount, this.l10n);
-      renderPage(this, this.pdfDocument, index + 1, this.pagesOverview[index]).then(this.useRenderedPage.bind(this)).then(function () {
+      renderPage(this, this.pdfDocument, index + 1, this.pagesOverview[index], this._printResolution, this._optionalContentConfigPromise).then(this.useRenderedPage.bind(this)).then(function () {
         renderNextPage(resolve, reject);
       }, reject);
     };
@@ -151,7 +153,7 @@ PDFPrintService.prototype = {
     img.style.height = printItem.height;
     const scratchCanvas = this.scratchCanvas;
 
-    if ("toBlob" in scratchCanvas && !this.disableCreateObjectURL) {
+    if ("toBlob" in scratchCanvas && !_viewer_compatibility.viewerCompatibilityParams.disableCreateObjectURL) {
       scratchCanvas.toBlob(function (blob) {
         img.src = URL.createObjectURL(blob);
       });
@@ -302,12 +304,12 @@ function ensureOverlay() {
 _app.PDFPrintServiceFactory.instance = {
   supportsPrinting: true,
 
-  createPrintService(pdfDocument, pagesOverview, printContainer, l10n) {
+  createPrintService(pdfDocument, pagesOverview, printContainer, printResolution, optionalContentConfigPromise, l10n) {
     if (activeService) {
       throw new Error("The print service is created and active.");
     }
 
-    activeService = new PDFPrintService(pdfDocument, pagesOverview, printContainer, l10n);
+    activeService = new PDFPrintService(pdfDocument, pagesOverview, printContainer, printResolution, optionalContentConfigPromise, l10n);
     return activeService;
   }
 

+ 46 - 25
lib/web/pdf_sidebar.js

@@ -62,9 +62,11 @@ class PDFSidebar {
     this.thumbnailButton = elements.thumbnailButton;
     this.outlineButton = elements.outlineButton;
     this.attachmentsButton = elements.attachmentsButton;
+    this.layersButton = elements.layersButton;
     this.thumbnailView = elements.thumbnailView;
     this.outlineView = elements.outlineView;
     this.attachmentsView = elements.attachmentsView;
+    this.layersView = elements.layersView;
     this.eventBus = eventBus;
     this.l10n = l10n;
     this._disableNotification = disableNotification;
@@ -80,6 +82,7 @@ class PDFSidebar {
     this.switchView(SidebarView.THUMBS);
     this.outlineButton.disabled = false;
     this.attachmentsButton.disabled = false;
+    this.layersButton.disabled = false;
   }
 
   get visibleView() {
@@ -98,6 +101,10 @@ class PDFSidebar {
     return this.isOpen && this.active === SidebarView.ATTACHMENTS;
   }
 
+  get isLayersViewVisible() {
+    return this.isOpen && this.active === SidebarView.LAYERS;
+  }
+
   setInitialView(view = SidebarView.NONE) {
     if (this.isInitialViewSet) {
       return;
@@ -154,6 +161,13 @@ class PDFSidebar {
 
         break;
 
+      case SidebarView.LAYERS:
+        if (this.layersButton.disabled) {
+          return false;
+        }
+
+        break;
+
       default:
         console.error(`PDFSidebar._switchView: "${view}" is not a valid view.`);
         return false;
@@ -163,9 +177,11 @@ class PDFSidebar {
     this.thumbnailButton.classList.toggle("toggled", view === SidebarView.THUMBS);
     this.outlineButton.classList.toggle("toggled", view === SidebarView.OUTLINE);
     this.attachmentsButton.classList.toggle("toggled", view === SidebarView.ATTACHMENTS);
+    this.layersButton.classList.toggle("toggled", view === SidebarView.LAYERS);
     this.thumbnailView.classList.toggle("hidden", view !== SidebarView.THUMBS);
     this.outlineView.classList.toggle("hidden", view !== SidebarView.OUTLINE);
     this.attachmentsView.classList.toggle("hidden", view !== SidebarView.ATTACHMENTS);
+    this.layersView.classList.toggle("hidden", view !== SidebarView.LAYERS);
 
     if (forceOpen && !this.isOpen) {
       this.open();
@@ -270,7 +286,7 @@ class PDFSidebar {
       return;
     }
 
-    this.l10n.get("toggle_sidebar_notification.title", null, "Toggle Sidebar (document contains outline/attachments)").then(msg => {
+    this.l10n.get("toggle_sidebar_notification2.title", null, "Toggle Sidebar (document contains outline/attachments/layers)").then(msg => {
       this.toggleButton.title = msg;
     });
 
@@ -288,6 +304,10 @@ class PDFSidebar {
       case SidebarView.ATTACHMENTS:
         this.attachmentsButton.classList.add(UI_NOTIFICATION_CLASS);
         break;
+
+      case SidebarView.LAYERS:
+        this.layersButton.classList.add(UI_NOTIFICATION_CLASS);
+        break;
     }
   }
 
@@ -305,6 +325,10 @@ class PDFSidebar {
         case SidebarView.ATTACHMENTS:
           this.attachmentsButton.classList.remove(UI_NOTIFICATION_CLASS);
           break;
+
+        case SidebarView.LAYERS:
+          this.layersButton.classList.remove(UI_NOTIFICATION_CLASS);
+          break;
       }
     };
 
@@ -351,38 +375,35 @@ class PDFSidebar {
     this.attachmentsButton.addEventListener("click", () => {
       this.switchView(SidebarView.ATTACHMENTS);
     });
+    this.layersButton.addEventListener("click", () => {
+      this.switchView(SidebarView.LAYERS);
+    });
+    this.layersButton.addEventListener("dblclick", () => {
+      this.eventBus.dispatch("resetlayers", {
+        source: this
+      });
+    });
 
-    this.eventBus._on("outlineloaded", evt => {
-      const outlineCount = evt.outlineCount;
-      this.outlineButton.disabled = !outlineCount;
+    const onTreeLoaded = (count, button, view) => {
+      button.disabled = !count;
 
-      if (outlineCount) {
-        this._showUINotification(SidebarView.OUTLINE);
-      } else if (this.active === SidebarView.OUTLINE) {
+      if (count) {
+        this._showUINotification(view);
+      } else if (this.active === view) {
         this.switchView(SidebarView.THUMBS);
       }
+    };
+
+    this.eventBus._on("outlineloaded", evt => {
+      onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);
     });
 
     this.eventBus._on("attachmentsloaded", evt => {
-      if (evt.attachmentsCount) {
-        this.attachmentsButton.disabled = false;
-
-        this._showUINotification(SidebarView.ATTACHMENTS);
-
-        return;
-      }
-
-      Promise.resolve().then(() => {
-        if (this.attachmentsView.hasChildNodes()) {
-          return;
-        }
-
-        this.attachmentsButton.disabled = true;
+      onTreeLoaded(evt.attachmentsCount, this.attachmentsButton, SidebarView.ATTACHMENTS);
+    });
 
-        if (this.active === SidebarView.ATTACHMENTS) {
-          this.switchView(SidebarView.THUMBS);
-        }
-      });
+    this.eventBus._on("layersloaded", evt => {
+      onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS);
     });
 
     this.eventBus._on("presentationmodechanged", evt => {

+ 16 - 3
lib/web/pdf_thumbnail_view.js

@@ -79,8 +79,10 @@ class PDFThumbnailView {
     container,
     id,
     defaultViewport,
+    optionalContentConfigPromise,
     linkService,
     renderingQueue,
+    checkSetImageDisabled,
     disableCanvasToImageConversion = false,
     l10n = _ui_utils.NullL10n
   }) {
@@ -91,11 +93,17 @@ class PDFThumbnailView {
     this.rotation = 0;
     this.viewport = defaultViewport;
     this.pdfPageRotate = defaultViewport.rotation;
+    this._optionalContentConfigPromise = optionalContentConfigPromise || null;
     this.linkService = linkService;
     this.renderingQueue = renderingQueue;
     this.renderTask = null;
     this.renderingState = _pdf_rendering_queue.RenderingStates.INITIAL;
     this.resume = null;
+
+    this._checkSetImageDisabled = checkSetImageDisabled || function () {
+      return false;
+    };
+
     this.disableCanvasToImageConversion = disableCanvasToImageConversion;
     this.pageWidth = this.viewport.width;
     this.pageHeight = this.viewport.height;
@@ -320,7 +328,8 @@ class PDFThumbnailView {
 
     const renderContext = {
       canvasContext: ctx,
-      viewport: drawViewport
+      viewport: drawViewport,
+      optionalContentConfigPromise: this._optionalContentConfigPromise
     };
     const renderTask = this.renderTask = pdfPage.render(renderContext);
     renderTask.onContinue = renderContinueCallback;
@@ -333,6 +342,10 @@ class PDFThumbnailView {
   }
 
   setImage(pageView) {
+    if (this._checkSetImageDisabled()) {
+      return;
+    }
+
     if (this.renderingState !== _pdf_rendering_queue.RenderingStates.INITIAL) {
       return;
     }
@@ -386,13 +399,13 @@ class PDFThumbnailView {
 
   get _thumbPageTitle() {
     return this.l10n.get("thumb_page_title", {
-      page: this.pageLabel !== null ? this.pageLabel : this.id
+      page: this.pageLabel ?? this.id
     }, "Page {{page}}");
   }
 
   get _thumbPageCanvas() {
     return this.l10n.get("thumb_page_canvas", {
-      page: this.pageLabel !== null ? this.pageLabel : this.id
+      page: this.pageLabel ?? this.id
     }, "Thumbnail of Page {{page}}");
   }
 

+ 17 - 1
lib/web/pdf_thumbnail_viewer.js

@@ -36,6 +36,7 @@ const THUMBNAIL_SELECTED_CLASS = "selected";
 class PDFThumbnailViewer {
   constructor({
     container,
+    eventBus,
     linkService,
     renderingQueue,
     l10n = _ui_utils.NullL10n
@@ -47,6 +48,10 @@ class PDFThumbnailViewer {
     this.scroll = (0, _ui_utils.watchScroll)(this.container, this._scrollUpdated.bind(this));
 
     this._resetView();
+
+    eventBus._on("optionalcontentconfigchanged", () => {
+      this._setImageDisabled = true;
+    });
   }
 
   _scrollUpdated() {
@@ -144,7 +149,9 @@ class PDFThumbnailViewer {
     this._currentPageNumber = 1;
     this._pageLabels = null;
     this._pagesRotation = 0;
+    this._optionalContentConfigPromise = null;
     this._pagesRequests = new WeakMap();
+    this._setImageDisabled = false;
     this.container.textContent = "";
   }
 
@@ -161,19 +168,28 @@ class PDFThumbnailViewer {
       return;
     }
 
-    pdfDocument.getPage(1).then(firstPdfPage => {
+    const firstPagePromise = pdfDocument.getPage(1);
+    const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
+    firstPagePromise.then(firstPdfPage => {
+      this._optionalContentConfigPromise = optionalContentConfigPromise;
       const pagesCount = pdfDocument.numPages;
       const viewport = firstPdfPage.getViewport({
         scale: 1
       });
 
+      const checkSetImageDisabled = () => {
+        return this._setImageDisabled;
+      };
+
       for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
         const thumbnail = new _pdf_thumbnail_view.PDFThumbnailView({
           container: this.container,
           id: pageNum,
           defaultViewport: viewport.clone(),
+          optionalContentConfigPromise,
           linkService: this.linkService,
           renderingQueue: this.renderingQueue,
+          checkSetImageDisabled,
           disableCanvasToImageConversion: false,
           l10n: this.l10n
         });

+ 2 - 2
lib/web/pdf_viewer.component.js

@@ -143,5 +143,5 @@ var _pdf_single_page_viewer = require("./pdf_single_page_viewer.js");
 
 var _pdf_viewer = require("./pdf_viewer.js");
 
-const pdfjsVersion = '2.5.207';
-const pdfjsBuild = '0974d605';
+const pdfjsVersion = '2.6.347';
+const pdfjsBuild = '3be9c65f';

+ 1 - 1
lib/web/preferences.js

@@ -44,7 +44,7 @@ function getDefaultPreferences() {
       "ignoreDestinationZoom": false,
       "pdfBugEnabled": false,
       "renderer": "canvas",
-      "renderInteractiveForms": false,
+      "renderInteractiveForms": true,
       "sidebarViewOnLoad": -1,
       "scrollModeOnLoad": -1,
       "spreadModeOnLoad": -1,

+ 1 - 1
lib/web/toolbar.js

@@ -268,7 +268,7 @@ class Toolbar {
     }
 
     const overflow = SCALE_SELECT_WIDTH - SCALE_SELECT_CONTAINER_WIDTH;
-    maxWidth += 1.5 * overflow;
+    maxWidth += 2 * overflow;
 
     if (maxWidth > SCALE_SELECT_CONTAINER_WIDTH) {
       items.scaleSelect.style.width = `${maxWidth + overflow}px`;

+ 7 - 1
lib/web/ui_utils.js

@@ -41,6 +41,7 @@ exports.getOutputScale = getOutputScale;
 exports.scrollIntoView = scrollIntoView;
 exports.watchScroll = watchScroll;
 exports.binarySearchFirstItem = binarySearchFirstItem;
+exports.normalizeWheelEventDirection = normalizeWheelEventDirection;
 exports.normalizeWheelEventDelta = normalizeWheelEventDelta;
 exports.waitOnEventOrTimeout = waitOnEventOrTimeout;
 exports.moveToEndOfArray = moveToEndOfArray;
@@ -478,7 +479,7 @@ function getPDFFileNameFromURL(url, defaultFilename = "document.pdf") {
   return suggestedFilename || defaultFilename;
 }
 
-function normalizeWheelEventDelta(evt) {
+function normalizeWheelEventDirection(evt) {
   let delta = Math.sqrt(evt.deltaX * evt.deltaX + evt.deltaY * evt.deltaY);
   const angle = Math.atan2(evt.deltaY, evt.deltaX);
 
@@ -486,6 +487,11 @@ function normalizeWheelEventDelta(evt) {
     delta = -delta;
   }
 
+  return delta;
+}
+
+function normalizeWheelEventDelta(evt) {
+  let delta = normalizeWheelEventDirection(evt);
   const MOUSE_DOM_DELTA_PIXEL_MODE = 0;
   const MOUSE_DOM_DELTA_LINE_MODE = 1;
   const MOUSE_PIXELS_PER_LINE = 30;

+ 3 - 1
package.json

@@ -1,7 +1,8 @@
 {
   "name": "pdfjs-dist",
-  "version": "2.5.207",
+  "version": "2.6.347",
   "main": "build/pdf.js",
+  "types": "types/pdf.d.ts",
   "description": "Generic build of Mozilla's PDF.js library.",
   "keywords": [
     "Mozilla",
@@ -12,6 +13,7 @@
   "bugs": "https://github.com/mozilla/pdf.js/issues",
   "license": "Apache-2.0",
   "browser": {
+    "canvas": false,
     "fs": false,
     "http": false,
     "https": false,

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.