From 41efa3c071f92e9a191c5cb5f8be737ca5219565 Mon Sep 17 00:00:00 2001
From: Jonas Jenwald <jonas.jenwald@gmail.com>
Date: Sun, 8 Aug 2021 14:36:28 +0200
Subject: [PATCH 1/2] [api-minor] Introduce a new `annotationMode`-option, in
 `PDFPageProxy.{render, getOperatorList}`

*This is a follow-up to PRs 13867 and 13899.*

This patch is tagged `api-minor` for the following reasons:
 - It replaces the `renderInteractiveForms`/`includeAnnotationStorage`-options, in the `PDFPageProxy.render`-method, with the single `annotationMode`-option that controls which annotations are being rendered and how. Note that the old options were mutually exclusive, and setting both to `true` would result in undefined behaviour.

 - For improved consistency in the API, the `annotationMode`-option will also work together with the `PDFPageProxy.getOperatorList`-method.

 - It's now also possible to disable *all* annotation rendering in both the API and the Viewer, since the other changes meant that this could now be supported with a single added line on the worker-thread[1]; fixes 7282.

---
[1] Please note that in order to simplify the overall implementation, we'll purposely only support disabling of *all* annotations and that the option is being shared between the API and the Viewer. For any more "specialized" use-cases, where e.g. only some annotation-types are being rendered and/or the API and Viewer render different sets of annotations, that'll have to be handled in third-party implementations/forks of the PDF.js code-base.
---
 extensions/chromium/preferences_schema.json | 12 ++-
 src/core/document.js                        |  5 +-
 src/display/annotation_layer.js             | 18 ++---
 src/display/api.js                          | 83 ++++++++++++++-------
 src/pdf.js                                  | 34 +++++----
 src/shared/util.js                          | 16 +++-
 test/driver.js                              | 33 +++++---
 test/unit/api_spec.js                       | 61 +++++++++++++--
 web/annotation_layer_builder.js             | 14 ++--
 web/app.js                                  |  4 +-
 web/app_options.js                          | 10 +--
 web/base_viewer.js                          | 35 ++++++---
 web/firefox_print_service.js                |  4 +-
 web/interfaces.js                           |  4 +-
 web/pdf_page_view.js                        | 70 +++++++++--------
 web/pdf_print_service.js                    |  3 +-
 16 files changed, 272 insertions(+), 134 deletions(-)

diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json
index 49190d6ed..9b30f007f 100644
--- a/extensions/chromium/preferences_schema.json
+++ b/extensions/chromium/preferences_schema.json
@@ -144,9 +144,15 @@
       "description": "Whether to prevent the extension from reporting the extension and browser version to the extension developers.",
       "default": false
     },
-    "renderInteractiveForms": {
-      "type": "boolean",
-      "default": true
+    "annotationMode": {
+      "type": "integer",
+      "enum": [
+        0,
+        1,
+        2,
+        3
+      ],
+      "default": 2
     },
     "enableScripting": {
       "type": "boolean",
diff --git a/src/core/document.js b/src/core/document.js
index 19f87d55d..9d35432a5 100644
--- a/src/core/document.js
+++ b/src/core/document.js
@@ -380,7 +380,10 @@ class Page {
     // page's operator list to render them.
     return Promise.all([pageListPromise, this._parsedAnnotations]).then(
       function ([pageOpList, annotations]) {
-        if (annotations.length === 0) {
+        if (
+          annotations.length === 0 ||
+          intent & RenderingIntentFlag.ANNOTATIONS_DISABLE
+        ) {
           pageOpList.flush(true);
           return { length: pageOpList.totalLength };
         }
diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js
index 2fdd7fcab..b69d84288 100644
--- a/src/display/annotation_layer.js
+++ b/src/display/annotation_layer.js
@@ -46,7 +46,7 @@ const DEFAULT_TAB_INDEX = 1000;
  * @property {AnnotationStorage} [annotationStorage]
  * @property {string} [imageResourcesPath] - Path for image resources, mainly
  *   for annotation icons. Include trailing slash.
- * @property {boolean} renderInteractiveForms
+ * @property {boolean} renderForms
  * @property {Object} svgFactory
  * @property {boolean} [enableScripting]
  * @property {boolean} [hasJSActions]
@@ -154,7 +154,7 @@ class AnnotationElement {
     this.linkService = parameters.linkService;
     this.downloadManager = parameters.downloadManager;
     this.imageResourcesPath = parameters.imageResourcesPath;
-    this.renderInteractiveForms = parameters.renderInteractiveForms;
+    this.renderForms = parameters.renderForms;
     this.svgFactory = parameters.svgFactory;
     this.annotationStorage = parameters.annotationStorage;
     this.enableScripting = parameters.enableScripting;
@@ -676,7 +676,7 @@ class WidgetAnnotationElement extends AnnotationElement {
 class TextWidgetAnnotationElement extends WidgetAnnotationElement {
   constructor(parameters) {
     const isRenderable =
-      parameters.renderInteractiveForms ||
+      parameters.renderForms ||
       (!parameters.data.hasAppearance && !!parameters.data.fieldValue);
     super(parameters, { isRenderable });
   }
@@ -700,7 +700,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
     this.container.className = "textWidgetAnnotation";
 
     let element = null;
-    if (this.renderInteractiveForms) {
+    if (this.renderForms) {
       // NOTE: We cannot set the values using `element.value` below, since it
       //       prevents the AnnotationLayer rasterizer in `test/driver.js`
       //       from parsing the elements correctly for the reference tests.
@@ -952,7 +952,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
 
 class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
   constructor(parameters) {
-    super(parameters, { isRenderable: parameters.renderInteractiveForms });
+    super(parameters, { isRenderable: parameters.renderForms });
   }
 
   render() {
@@ -1031,7 +1031,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
 
 class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
   constructor(parameters) {
-    super(parameters, { isRenderable: parameters.renderInteractiveForms });
+    super(parameters, { isRenderable: parameters.renderForms });
   }
 
   render() {
@@ -1123,7 +1123,7 @@ class PushButtonWidgetAnnotationElement extends LinkAnnotationElement {
 
 class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
   constructor(parameters) {
-    super(parameters, { isRenderable: parameters.renderInteractiveForms });
+    super(parameters, { isRenderable: parameters.renderForms });
   }
 
   render() {
@@ -2033,7 +2033,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
  * @property {DownloadManager} downloadManager
  * @property {string} [imageResourcesPath] - Path for image resources, mainly
  *   for annotation icons. Include trailing slash.
- * @property {boolean} renderInteractiveForms
+ * @property {boolean} renderForms
  * @property {boolean} [enableScripting] - Enable embedded script execution.
  * @property {boolean} [hasJSActions] - Some fields have JS actions.
  *   The default value is `false`.
@@ -2076,7 +2076,7 @@ class AnnotationLayer {
         linkService: parameters.linkService,
         downloadManager: parameters.downloadManager,
         imageResourcesPath: parameters.imageResourcesPath || "",
-        renderInteractiveForms: parameters.renderInteractiveForms !== false,
+        renderForms: parameters.renderForms !== false,
         svgFactory: new DOMSVGFactory(),
         annotationStorage:
           parameters.annotationStorage || new AnnotationStorage(),
diff --git a/src/display/api.js b/src/display/api.js
index 776e6b180..40fe7599f 100644
--- a/src/display/api.js
+++ b/src/display/api.js
@@ -19,6 +19,7 @@
 
 import {
   AbortException,
+  AnnotationMode,
   assert,
   createPromiseCapability,
   getVerbosityLevel,
@@ -1135,9 +1136,18 @@ class PDFDocumentProxy {
  *   the `PDFPageProxy.getViewport` method.
  * @property {string} [intent] - Rendering intent, can be 'display', 'print',
  *   or 'any'. The default value is 'display'.
- * @property {boolean} [renderInteractiveForms] - Whether or not interactive
- *   form elements are rendered in the display layer. If so, we do not render
- *   them on the canvas as well. The default value is `false`.
+ * @property {number} [annotationMode] Controls which annotations are rendered
+ *   onto the canvas, for annotations with appearance-data; the values from
+ *   {@link AnnotationMode} should be used. The following values are supported:
+ *    - `AnnotationMode.DISABLE`, which disables all annotations.
+ *    - `AnnotationMode.ENABLE`, which includes all possible annotations (thus
+ *      it also depends on the `intent`-option, see above).
+ *    - `AnnotationMode.ENABLE_FORMS`, which excludes annotations that contain
+ *      interactive form elements (those will be rendered in the display layer).
+ *    - `AnnotationMode.ENABLE_STORAGE`, which includes all possible annotations
+ *      (as above) but where interactive form elements are updated with data
+ *      from the {@link AnnotationStorage}-instance; useful e.g. for printing.
+ *   The default value is `AnnotationMode.ENABLE`.
  * @property {Array<any>} [transform] - Additional transform, applied just
  *   before viewport transform.
  * @property {Object} [imageLayer] - An object that has `beginLayout`,
@@ -1149,9 +1159,6 @@ class PDFDocumentProxy {
  *   <color> value, a `CanvasGradient` object (a linear or radial gradient) or
  *   a `CanvasPattern` object (a repetitive image). The default value is
  *   'rgb(255,255,255)'.
- * @property {boolean} [includeAnnotationStorage] - Render stored interactive
- *   form element data, from the {@link AnnotationStorage}-instance, onto the
- *   canvas itself; useful e.g. for printing. The default value is `false`.
  * @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] -
  *   A promise that should resolve with an {@link OptionalContentConfig}
  *   created from `PDFDocumentProxy.getOptionalContentConfig`. If `null`,
@@ -1165,6 +1172,18 @@ class PDFDocumentProxy {
  * @typedef {Object} GetOperatorListParameters
  * @property {string} [intent] - Rendering intent, can be 'display', 'print',
  *   or 'any'. The default value is 'display'.
+ * @property {number} [annotationMode] Controls which annotations are included
+ *   in the operatorList, for annotations with appearance-data; the values from
+ *   {@link AnnotationMode} should be used. The following values are supported:
+ *    - `AnnotationMode.DISABLE`, which disables all annotations.
+ *    - `AnnotationMode.ENABLE`, which includes all possible annotations (thus
+ *      it also depends on the `intent`-option, see above).
+ *    - `AnnotationMode.ENABLE_FORMS`, which excludes annotations that contain
+ *      interactive form elements (those will be rendered in the display layer).
+ *    - `AnnotationMode.ENABLE_STORAGE`, which includes all possible annotations
+ *      (as above) but where interactive form elements are updated with data
+ *      from the {@link AnnotationStorage}-instance; useful e.g. for printing.
+ *   The default value is `AnnotationMode.ENABLE`.
  */
 
 /**
@@ -1280,7 +1299,7 @@ class PDFPageProxy {
    *   {Array} of the annotation objects.
    */
   getAnnotations({ intent = "display" } = {}) {
-    const intentArgs = this._transport.getRenderingIntent(intent, {});
+    const intentArgs = this._transport.getRenderingIntent(intent);
 
     let promise = this._annotationPromises.get(intentArgs.cacheKey);
     if (!promise) {
@@ -1324,22 +1343,21 @@ class PDFPageProxy {
     canvasContext,
     viewport,
     intent = "display",
-    renderInteractiveForms = false,
+    annotationMode = AnnotationMode.ENABLE,
     transform = null,
     imageLayer = null,
     canvasFactory = null,
     background = null,
-    includeAnnotationStorage = false,
     optionalContentConfigPromise = null,
   }) {
     if (this._stats) {
       this._stats.time("Overall");
     }
 
-    const intentArgs = this._transport.getRenderingIntent(intent, {
-      renderForms: renderInteractiveForms === true,
-      includeAnnotationStorage: includeAnnotationStorage === true,
-    });
+    const intentArgs = this._transport.getRenderingIntent(
+      intent,
+      annotationMode
+    );
     // If there was a pending destroy, cancel it so no cleanup happens during
     // this call to render.
     this.pendingCleanup = false;
@@ -1460,7 +1478,10 @@ class PDFPageProxy {
    * @returns {Promise<PDFOperatorList>} A promise resolved with an
    *   {@link PDFOperatorList} object that represents the page's operator list.
    */
-  getOperatorList({ intent = "display" } = {}) {
+  getOperatorList({
+    intent = "display",
+    annotationMode = AnnotationMode.ENABLE,
+  } = {}) {
     function operatorListChanged() {
       if (intentState.operatorList.lastChunk) {
         intentState.opListReadCapability.resolve(intentState.operatorList);
@@ -1469,9 +1490,11 @@ class PDFPageProxy {
       }
     }
 
-    const intentArgs = this._transport.getRenderingIntent(intent, {
-      isOpList: true,
-    });
+    const intentArgs = this._transport.getRenderingIntent(
+      intent,
+      annotationMode,
+      /* isOpList = */ true
+    );
     let intentState = this._intentStates.get(intentArgs.cacheKey);
     if (!intentState) {
       intentState = Object.create(null);
@@ -1792,7 +1815,7 @@ class PDFPageProxy {
       }
     }
     intentState.streamReader
-      .cancel(new AbortException(reason?.message))
+      .cancel(new AbortException(reason.message))
       .catch(() => {
         // Avoid "Uncaught promise" messages in the console.
       });
@@ -2351,7 +2374,8 @@ class WorkerTransport {
 
   getRenderingIntent(
     intent,
-    { renderForms = false, includeAnnotationStorage = false, isOpList = false }
+    annotationMode = AnnotationMode.ENABLE,
+    isOpList = false
   ) {
     let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value.
     let lastModified = "";
@@ -2369,13 +2393,22 @@ class WorkerTransport {
         warn(`getRenderingIntent - invalid intent: ${intent}`);
     }
 
-    if (renderForms) {
-      renderingIntent += RenderingIntentFlag.ANNOTATIONS_FORMS;
-    }
-    if (includeAnnotationStorage) {
-      renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;
+    switch (annotationMode) {
+      case AnnotationMode.DISABLE:
+        renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE;
+        break;
+      case AnnotationMode.ENABLE:
+        break;
+      case AnnotationMode.ENABLE_FORMS:
+        renderingIntent += RenderingIntentFlag.ANNOTATIONS_FORMS;
+        break;
+      case AnnotationMode.ENABLE_STORAGE:
+        renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;
 
-      lastModified = this.annotationStorage.lastModified;
+        lastModified = this.annotationStorage.lastModified;
+        break;
+      default:
+        warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`);
     }
 
     if (isOpList) {
diff --git a/src/pdf.js b/src/pdf.js
index 9a1bfce30..8bbf9efa8 100644
--- a/src/pdf.js
+++ b/src/pdf.js
@@ -27,15 +27,7 @@ import {
   RenderingCancelledException,
 } from "./display/display_utils.js";
 import {
-  build,
-  getDocument,
-  LoopbackPort,
-  PDFDataRangeTransport,
-  PDFWorker,
-  setPDFNetworkStreamFactory,
-  version,
-} from "./display/api.js";
-import {
+  AnnotationMode,
   CMapCompressionType,
   createObjectURL,
   createPromiseCapability,
@@ -52,6 +44,15 @@ import {
   Util,
   VerbosityLevel,
 } from "./shared/util.js";
+import {
+  build,
+  getDocument,
+  LoopbackPort,
+  PDFDataRangeTransport,
+  PDFWorker,
+  setPDFNetworkStreamFactory,
+  version,
+} from "./display/api.js";
 import { AnnotationLayer } from "./display/annotation_layer.js";
 import { GlobalWorkerOptions } from "./display/worker_options.js";
 import { isNodeJS } from "./shared/is_node.js";
@@ -110,14 +111,8 @@ export {
   PDFDateString,
   RenderingCancelledException,
   getXfaPageViewport,
-  // From "./display/api.js":
-  build,
-  getDocument,
-  LoopbackPort,
-  PDFDataRangeTransport,
-  PDFWorker,
-  version,
   // From "./shared/util.js":
+  AnnotationMode,
   CMapCompressionType,
   createObjectURL,
   createPromiseCapability,
@@ -133,6 +128,13 @@ export {
   UNSUPPORTED_FEATURES,
   Util,
   VerbosityLevel,
+  // From "./display/api.js":
+  build,
+  getDocument,
+  LoopbackPort,
+  PDFDataRangeTransport,
+  PDFWorker,
+  version,
   // From "./display/annotation_layer.js":
   AnnotationLayer,
   // From "./display/worker_options.js":
diff --git a/src/shared/util.js b/src/shared/util.js
index 9371c3676..d893e398e 100644
--- a/src/shared/util.js
+++ b/src/shared/util.js
@@ -23,9 +23,10 @@ const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
  * how these flags are being used:
  *  - ANY, DISPLAY, and PRINT are the normal rendering intents, note the
  *    `PDFPageProxy.{render, getOperatorList, getAnnotations}`-methods.
- *  - ANNOTATIONS_FORMS, and ANNOTATIONS_STORAGE controls which annotations are
- *    rendered onto the canvas, note the `renderInteractiveForms`- respectively
- *    `includeAnnotationStorage`-options in the `PDFPageProxy.render`-method.
+ *  - ANNOTATIONS_FORMS, ANNOTATIONS_STORAGE, ANNOTATIONS_DISABLE control which
+ *    annotations are rendered onto the canvas (i.e. by being included in the
+ *    operatorList), note the `PDFPageProxy.{render, getOperatorList}`-methods
+ *    and their `annotationMode`-option.
  *  - OPLIST is used with the `PDFPageProxy.getOperatorList`-method, note the
  *    `OperatorList`-constructor (on the worker-thread).
  */
@@ -35,9 +36,17 @@ const RenderingIntentFlag = {
   PRINT: 0x04,
   ANNOTATIONS_FORMS: 0x10,
   ANNOTATIONS_STORAGE: 0x20,
+  ANNOTATIONS_DISABLE: 0x40,
   OPLIST: 0x100,
 };
 
+const AnnotationMode = {
+  DISABLE: 0,
+  ENABLE: 1,
+  ENABLE_FORMS: 2,
+  ENABLE_STORAGE: 3,
+};
+
 // Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.
 const PermissionFlag = {
   PRINT: 0x04,
@@ -1027,6 +1036,7 @@ export {
   AnnotationFieldFlag,
   AnnotationFlag,
   AnnotationMarkedState,
+  AnnotationMode,
   AnnotationReplyType,
   AnnotationReviewState,
   AnnotationStateModelType,
diff --git a/test/driver.js b/test/driver.js
index 63bb73f68..5946561a8 100644
--- a/test/driver.js
+++ b/test/driver.js
@@ -17,6 +17,16 @@
 
 "use strict";
 
+const {
+  AnnotationLayer,
+  AnnotationMode,
+  getDocument,
+  GlobalWorkerOptions,
+  renderTextLayer,
+  XfaLayer,
+} = pdfjsLib;
+const { SimpleLinkService } = pdfjsViewer;
+
 const WAITING_TIME = 100; // ms
 const PDF_TO_CSS_UNITS = 96.0 / 72.0;
 const CMAP_URL = "/build/generic/web/cmaps/";
@@ -162,7 +172,7 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() {
           style.textContent = cssRules;
 
           // Rendering text layer as HTML.
-          var task = pdfjsLib.renderTextLayer({
+          var task = renderTextLayer({
             textContent,
             container: div,
             viewport,
@@ -219,7 +229,7 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() {
     annotations,
     page,
     imageResourcesPath,
-    renderInteractiveForms
+    renderForms = false
   ) {
     return new Promise(function (resolve, reject) {
       // Building SVG with size of the viewport.
@@ -250,11 +260,11 @@ var rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() {
             div,
             annotations,
             page,
-            linkService: new pdfjsViewer.SimpleLinkService(),
+            linkService: new SimpleLinkService(),
             imageResourcesPath,
-            renderInteractiveForms,
+            renderForms,
           };
-          pdfjsLib.AnnotationLayer.render(parameters);
+          AnnotationLayer.render(parameters);
 
           // Inline SVG images from text annotations.
           await resolveImages(div);
@@ -319,7 +329,7 @@ var rasterizeXfaLayer = (function rasterizeXfaLayerClosure() {
         .then(async cssRules => {
           style.textContent = fontRules + "\n" + cssRules;
 
-          pdfjsLib.XfaLayer.render({
+          XfaLayer.render({
             xfa,
             div,
             viewport: viewport.clone({ dontFlip: true }),
@@ -365,7 +375,7 @@ var Driver = (function DriverClosure() {
   // eslint-disable-next-line no-shadow
   function Driver(options) {
     // Configure the global worker options.
-    pdfjsLib.GlobalWorkerOptions.workerSrc = WORKER_SRC;
+    GlobalWorkerOptions.workerSrc = WORKER_SRC;
 
     // Set the passed options
     this.inflight = options.inflight;
@@ -494,7 +504,7 @@ var Driver = (function DriverClosure() {
               .appendChild(xfaStyleElement);
           }
 
-          const loadingTask = pdfjsLib.getDocument({
+          const loadingTask = getDocument({
             url: absoluteUrl,
             password: task.password,
             cMapUrl: CMAP_URL,
@@ -752,12 +762,13 @@ var Driver = (function DriverClosure() {
               var renderContext = {
                 canvasContext: ctx,
                 viewport,
-                renderInteractiveForms: renderForms,
                 optionalContentConfigPromise: task.optionalContentConfigPromise,
               };
-              if (renderPrint) {
+              if (renderForms) {
+                renderContext.annotationMode = AnnotationMode.ENABLE_FORMS;
+              } else if (renderPrint) {
                 if (task.annotationStorage) {
-                  renderContext.includeAnnotationStorage = true;
+                  renderContext.annotationMode = AnnotationMode.ENABLE_STORAGE;
                 }
                 renderContext.intent = "print";
               }
diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js
index 1769601c5..28afdb91e 100644
--- a/test/unit/api_spec.js
+++ b/test/unit/api_spec.js
@@ -14,11 +14,7 @@
  */
 
 import {
-  buildGetDocumentParams,
-  DefaultFileReaderFactory,
-  TEST_PDFS_PATH,
-} from "./test_utils.js";
-import {
+  AnnotationMode,
   createPromiseCapability,
   FontType,
   ImageKind,
@@ -30,6 +26,11 @@ import {
   PermissionFlag,
   StreamType,
 } from "../../src/shared/util.js";
+import {
+  buildGetDocumentParams,
+  DefaultFileReaderFactory,
+  TEST_PDFS_PATH,
+} from "./test_utils.js";
 import {
   DefaultCanvasFactory,
   getDocument,
@@ -1739,6 +1740,56 @@ describe("api", function () {
       await loadingTask.destroy();
     });
 
+    it("gets operator list, with `annotationMode`-option", async function () {
+      const loadingTask = getDocument(buildGetDocumentParams("evaljs.pdf"));
+      const pdfDoc = await loadingTask.promise;
+      const pdfPage = await pdfDoc.getPage(2);
+
+      pdfDoc.annotationStorage.setValue("30R", { value: "test" });
+      pdfDoc.annotationStorage.setValue("31R", { value: true });
+
+      const opListAnnotDisable = await pdfPage.getOperatorList({
+        annotationMode: AnnotationMode.DISABLE,
+      });
+      expect(opListAnnotDisable.fnArray.length).toEqual(0);
+      expect(opListAnnotDisable.argsArray.length).toEqual(0);
+      expect(opListAnnotDisable.lastChunk).toEqual(true);
+
+      const opListAnnotEnable = await pdfPage.getOperatorList({
+        annotationMode: AnnotationMode.ENABLE,
+      });
+      expect(opListAnnotEnable.fnArray.length).toBeGreaterThan(150);
+      expect(opListAnnotEnable.argsArray.length).toBeGreaterThan(150);
+      expect(opListAnnotEnable.lastChunk).toEqual(true);
+
+      const opListAnnotEnableForms = await pdfPage.getOperatorList({
+        annotationMode: AnnotationMode.ENABLE_FORMS,
+      });
+      expect(opListAnnotEnableForms.fnArray.length).toBeGreaterThan(40);
+      expect(opListAnnotEnableForms.argsArray.length).toBeGreaterThan(40);
+      expect(opListAnnotEnableForms.lastChunk).toEqual(true);
+
+      const opListAnnotEnableStorage = await pdfPage.getOperatorList({
+        annotationMode: AnnotationMode.ENABLE_STORAGE,
+      });
+      expect(opListAnnotEnableStorage.fnArray.length).toBeGreaterThan(170);
+      expect(opListAnnotEnableStorage.argsArray.length).toBeGreaterThan(170);
+      expect(opListAnnotEnableStorage.lastChunk).toEqual(true);
+
+      // Sanity check to ensure that the `annotationMode` is correctly applied.
+      expect(opListAnnotDisable.fnArray.length).toBeLessThan(
+        opListAnnotEnableForms.fnArray.length
+      );
+      expect(opListAnnotEnableForms.fnArray.length).toBeLessThan(
+        opListAnnotEnable.fnArray.length
+      );
+      expect(opListAnnotEnable.fnArray.length).toBeLessThan(
+        opListAnnotEnableStorage.fnArray.length
+      );
+
+      await loadingTask.destroy();
+    });
+
     it("gets document stats after parsing page", async function () {
       const stats = await page.getOperatorList().then(function () {
         return pdfDocument.getStats();
diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js
index 0f7ce4060..3c4e3c7d3 100644
--- a/web/annotation_layer_builder.js
+++ b/web/annotation_layer_builder.js
@@ -24,7 +24,7 @@ import { SimpleLinkService } from "./pdf_link_service.js";
  * @property {AnnotationStorage} [annotationStorage]
  * @property {string} [imageResourcesPath] - Path for image resources, mainly
  *   for annotation icons. Include trailing slash.
- * @property {boolean} renderInteractiveForms
+ * @property {boolean} renderForms
  * @property {IPDFLinkService} linkService
  * @property {DownloadManager} downloadManager
  * @property {IL10n} l10n - Localization service.
@@ -44,7 +44,7 @@ class AnnotationLayerBuilder {
     downloadManager,
     annotationStorage = null,
     imageResourcesPath = "",
-    renderInteractiveForms = true,
+    renderForms = true,
     l10n = NullL10n,
     enableScripting = false,
     hasJSActionsPromise = null,
@@ -55,7 +55,7 @@ class AnnotationLayerBuilder {
     this.linkService = linkService;
     this.downloadManager = downloadManager;
     this.imageResourcesPath = imageResourcesPath;
-    this.renderInteractiveForms = renderInteractiveForms;
+    this.renderForms = renderForms;
     this.l10n = l10n;
     this.annotationStorage = annotationStorage;
     this.enableScripting = enableScripting;
@@ -90,7 +90,7 @@ class AnnotationLayerBuilder {
         annotations,
         page: this.pdfPage,
         imageResourcesPath: this.imageResourcesPath,
-        renderInteractiveForms: this.renderInteractiveForms,
+        renderForms: this.renderForms,
         linkService: this.linkService,
         downloadManager: this.downloadManager,
         annotationStorage: this.annotationStorage,
@@ -139,7 +139,7 @@ class DefaultAnnotationLayerFactory {
    * @param {AnnotationStorage} [annotationStorage]
    * @param {string} [imageResourcesPath] - Path for image resources, mainly
    *   for annotation icons. Include trailing slash.
-   * @param {boolean} renderInteractiveForms
+   * @param {boolean} renderForms
    * @param {IL10n} l10n
    * @param {boolean} [enableScripting]
    * @param {Promise<boolean>} [hasJSActionsPromise]
@@ -151,7 +151,7 @@ class DefaultAnnotationLayerFactory {
     pdfPage,
     annotationStorage = null,
     imageResourcesPath = "",
-    renderInteractiveForms = true,
+    renderForms = true,
     l10n = NullL10n,
     enableScripting = false,
     hasJSActionsPromise = null,
@@ -161,7 +161,7 @@ class DefaultAnnotationLayerFactory {
       pageDiv,
       pdfPage,
       imageResourcesPath,
-      renderInteractiveForms,
+      renderForms,
       linkService: new SimpleLinkService(),
       l10n,
       annotationStorage,
diff --git a/web/app.js b/web/app.js
index a00f5ad78..f1c8fa003 100644
--- a/web/app.js
+++ b/web/app.js
@@ -515,8 +515,8 @@ const PDFViewerApplication = {
       renderer: AppOptions.get("renderer"),
       l10n: this.l10n,
       textLayerMode: AppOptions.get("textLayerMode"),
+      annotationMode: AppOptions.get("annotationMode"),
       imageResourcesPath: AppOptions.get("imageResourcesPath"),
-      renderInteractiveForms: AppOptions.get("renderInteractiveForms"),
       enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
       useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
       maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
@@ -1555,7 +1555,7 @@ const PDFViewerApplication = {
       this.fallback(UNSUPPORTED_FEATURES.forms);
     } else if (
       (info.IsAcroFormPresent || info.IsXFAPresent) &&
-      !this.pdfViewer.renderInteractiveForms
+      !this.pdfViewer.renderForms
     ) {
       console.warn("Warning: Interactive form support is not enabled");
       this.fallback(UNSUPPORTED_FEATURES.forms);
diff --git a/web/app_options.js b/web/app_options.js
index 4a0c0245f..094758872 100644
--- a/web/app_options.js
+++ b/web/app_options.js
@@ -59,6 +59,11 @@ const OptionKind = {
  *              values below *explicitly* rather than relying on imported types.
  */
 const defaultOptions = {
+  annotationMode: {
+    /** @type {number} */
+    value: 2,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
+  },
   cursorToolOnLoad: {
     /** @type {number} */
     value: 0,
@@ -145,11 +150,6 @@ const defaultOptions = {
     value: "canvas",
     kind: OptionKind.VIEWER,
   },
-  renderInteractiveForms: {
-    /** @type {boolean} */
-    value: true,
-    kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
-  },
   sidebarViewOnLoad: {
     /** @type {number} */
     value: -1,
diff --git a/web/base_viewer.js b/web/base_viewer.js
index 299f8baef..81a9c7701 100644
--- a/web/base_viewer.js
+++ b/web/base_viewer.js
@@ -13,7 +13,7 @@
  * limitations under the License.
  */
 
-import { createPromiseCapability, version } from "pdfjs-lib";
+import { AnnotationMode, createPromiseCapability, version } from "pdfjs-lib";
 import {
   CSS_UNITS,
   DEFAULT_SCALE,
@@ -67,10 +67,13 @@ const DEFAULT_CACHE_SIZE = 10;
  *   selection and searching is created, and if the improved text selection
  *   behaviour is enabled. The constants from {TextLayerMode} should be used.
  *   The default value is `TextLayerMode.ENABLE`.
+ * @property {number} [annotationMode] - Controls if the annotation layer is
+ *   created, and if interactive form elements or `AnnotationStorage`-data are
+ *   being rendered. The constants from {@link AnnotationMode} should be used;
+ *   see also {@link RenderParameters} and {@link GetOperatorListParameters}.
+ *   The default value is `AnnotationMode.ENABLE_FORMS`.
  * @property {string} [imageResourcesPath] - Path for image resources, mainly
  *   mainly for annotation icons. Include trailing slash.
- * @property {boolean} [renderInteractiveForms] - Enables rendering of
- *   interactive form elements. The default value is `true`.
  * @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
  *   landscape pages upon printing. The default is `false`.
  * @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'.
@@ -182,11 +185,10 @@ class BaseViewer {
     this.findController = options.findController || null;
     this._scriptingManager = options.scriptingManager || null;
     this.removePageBorders = options.removePageBorders || false;
-    this.textLayerMode = Number.isInteger(options.textLayerMode)
-      ? options.textLayerMode
-      : TextLayerMode.ENABLE;
+    this.textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE;
+    this._annotationMode =
+      options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
     this.imageResourcesPath = options.imageResourcesPath || "";
-    this.renderInteractiveForms = options.renderInteractiveForms !== false;
     this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
     this.renderer = options.renderer || RendererType.CANVAS;
     this.useOnlyCssZoom = options.useOnlyCssZoom || false;
@@ -240,6 +242,13 @@ class BaseViewer {
     });
   }
 
+  /**
+   * @type {boolean}
+   */
+  get renderForms() {
+    return this._annotationMode === AnnotationMode.ENABLE_FORMS;
+  }
+
   /**
    * @type {boolean}
    */
@@ -529,6 +538,8 @@ class BaseViewer {
           this.textLayerMode !== TextLayerMode.DISABLE && !isPureXfa
             ? this
             : null;
+        const annotationLayerFactory =
+          this._annotationMode !== AnnotationMode.DISABLE ? this : null;
         const xfaLayerFactory = isPureXfa ? this : null;
 
         for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
@@ -542,12 +553,12 @@ class BaseViewer {
             renderingQueue: this.renderingQueue,
             textLayerFactory,
             textLayerMode: this.textLayerMode,
-            annotationLayerFactory: this,
+            annotationLayerFactory,
+            annotationMode: this._annotationMode,
             xfaLayerFactory,
             textHighlighterFactory: this,
             structTreeLayerFactory: this,
             imageResourcesPath: this.imageResourcesPath,
-            renderInteractiveForms: this.renderInteractiveForms,
             renderer: this.renderer,
             useOnlyCssZoom: this.useOnlyCssZoom,
             maxCanvasPixels: this.maxCanvasPixels,
@@ -1289,7 +1300,7 @@ class BaseViewer {
    *   data in forms.
    * @param {string} [imageResourcesPath] - Path for image resources, mainly
    *   for annotation icons. Include trailing slash.
-   * @param {boolean} renderInteractiveForms
+   * @param {boolean} renderForms
    * @param {IL10n} l10n
    * @param {boolean} [enableScripting]
    * @param {Promise<boolean>} [hasJSActionsPromise]
@@ -1301,7 +1312,7 @@ class BaseViewer {
     pdfPage,
     annotationStorage = null,
     imageResourcesPath = "",
-    renderInteractiveForms = false,
+    renderForms = true,
     l10n = NullL10n,
     enableScripting = null,
     hasJSActionsPromise = null,
@@ -1313,7 +1324,7 @@ class BaseViewer {
       annotationStorage:
         annotationStorage || this.pdfDocument?.annotationStorage,
       imageResourcesPath,
-      renderInteractiveForms,
+      renderForms,
       linkService: this.linkService,
       downloadManager: this.downloadManager,
       l10n,
diff --git a/web/firefox_print_service.js b/web/firefox_print_service.js
index 5fb6dba4e..227632447 100644
--- a/web/firefox_print_service.js
+++ b/web/firefox_print_service.js
@@ -13,7 +13,7 @@
  * limitations under the License.
  */
 
-import { RenderingCancelledException, shadow } from "pdfjs-lib";
+import { AnnotationMode, RenderingCancelledException, shadow } from "pdfjs-lib";
 import { getXfaHtmlForPrinting } from "./print_utils.js";
 import { PDFPrintServiceFactory } from "./app.js";
 
@@ -68,7 +68,7 @@ function composePage(
           transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
           viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }),
           intent: "print",
-          includeAnnotationStorage: true,
+          annotationMode: AnnotationMode.ENABLE_STORAGE,
           optionalContentConfigPromise,
         };
         currentRenderTask = thisRenderTask = pdfPage.render(renderContext);
diff --git a/web/interfaces.js b/web/interfaces.js
index 512d4c42a..bbb58a41b 100644
--- a/web/interfaces.js
+++ b/web/interfaces.js
@@ -186,7 +186,7 @@ class IPDFAnnotationLayerFactory {
    *   data in forms.
    * @param {string} [imageResourcesPath] - Path for image resources, mainly
    *   for annotation icons. Include trailing slash.
-   * @param {boolean} renderInteractiveForms
+   * @param {boolean} renderForms
    * @param {IL10n} l10n
    * @param {boolean} [enableScripting]
    * @param {Promise<boolean>} [hasJSActionsPromise]
@@ -198,7 +198,7 @@ class IPDFAnnotationLayerFactory {
     pdfPage,
     annotationStorage = null,
     imageResourcesPath = "",
-    renderInteractiveForms = true,
+    renderForms = true,
     l10n = undefined,
     enableScripting = false,
     hasJSActionsPromise = null,
diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js
index 3f1769aa3..0b11a9456 100644
--- a/web/pdf_page_view.js
+++ b/web/pdf_page_view.js
@@ -13,6 +13,12 @@
  * limitations under the License.
  */
 
+import {
+  AnnotationMode,
+  createPromiseCapability,
+  RenderingCancelledException,
+  SVGGraphics,
+} from "pdfjs-lib";
 import {
   approximateFraction,
   CSS_UNITS,
@@ -22,11 +28,6 @@ import {
   roundToDivide,
   TextLayerMode,
 } from "./ui_utils.js";
-import {
-  createPromiseCapability,
-  RenderingCancelledException,
-  SVGGraphics,
-} from "pdfjs-lib";
 import { compatibilityParams } from "./app_options.js";
 import { NullL10n } from "./l10n_utils.js";
 import { RenderingStates } from "./pdf_rendering_queue.js";
@@ -47,13 +48,16 @@ import { RenderingStates } from "./pdf_rendering_queue.js";
  *   selection and searching is created, and if the improved text selection
  *   behaviour is enabled. The constants from {TextLayerMode} should be used.
  *   The default value is `TextLayerMode.ENABLE`.
+ * @property {number} [annotationMode] - Controls if the annotation layer is
+ *   created, and if interactive form elements or `AnnotationStorage`-data are
+ *   being rendered. The constants from {@link AnnotationMode} should be used;
+ *   see also {@link RenderParameters} and {@link GetOperatorListParameters}.
+ *   The default value is `AnnotationMode.ENABLE_FORMS`.
  * @property {IPDFAnnotationLayerFactory} annotationLayerFactory
  * @property {IPDFXfaLayerFactory} xfaLayerFactory
  * @property {IPDFStructTreeLayerFactory} structTreeLayerFactory
  * @property {string} [imageResourcesPath] - Path for image resources, mainly
  *   for annotation icons. Include trailing slash.
- * @property {boolean} renderInteractiveForms - Turns on rendering of
- *   interactive form elements. The default value is `true`.
  * @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'.
  * @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default
  *   value is `false`.
@@ -88,11 +92,10 @@ class PDFPageView {
     this._optionalContentConfigPromise =
       options.optionalContentConfigPromise || null;
     this.hasRestrictedScaling = false;
-    this.textLayerMode = Number.isInteger(options.textLayerMode)
-      ? options.textLayerMode
-      : TextLayerMode.ENABLE;
+    this.textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE;
+    this._annotationMode =
+      options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
     this.imageResourcesPath = options.imageResourcesPath || "";
-    this.renderInteractiveForms = options.renderInteractiveForms !== false;
     this.useOnlyCssZoom = options.useOnlyCssZoom || false;
     this.maxCanvasPixels = options.maxCanvasPixels || MAX_CANVAS_PIXELS;
 
@@ -638,7 +641,10 @@ class PDFPageView {
       }
     );
 
-    if (this.annotationLayerFactory) {
+    if (
+      this._annotationMode !== AnnotationMode.DISABLE &&
+      this.annotationLayerFactory
+    ) {
       if (!this.annotationLayer) {
         this.annotationLayer =
           this.annotationLayerFactory.createAnnotationLayerBuilder(
@@ -646,9 +652,9 @@ class PDFPageView {
             pdfPage,
             /* annotationStorage = */ null,
             this.imageResourcesPath,
-            this.renderInteractiveForms,
+            this._annotationMode === AnnotationMode.ENABLE_FORMS,
             this.l10n,
-            /* enableScripting */ null,
+            /* enableScripting = */ null,
             /* hasJSActionsPromise = */ null,
             /* mouseState = */ null
           );
@@ -787,7 +793,7 @@ class PDFPageView {
       canvasContext: ctx,
       transform,
       viewport: this.viewport,
-      renderInteractiveForms: this.renderInteractiveForms,
+      annotationMode: this._annotationMode,
       optionalContentConfigPromise: this._optionalContentConfigPromise,
     };
     const renderTask = this.pdfPage.render(renderContext);
@@ -839,24 +845,28 @@ class PDFPageView {
 
     const pdfPage = this.pdfPage;
     const actualSizeViewport = this.viewport.clone({ scale: CSS_UNITS });
-    const promise = pdfPage.getOperatorList().then(opList => {
-      ensureNotCancelled();
-      const svgGfx = new SVGGraphics(
-        pdfPage.commonObjs,
-        pdfPage.objs,
-        /* forceDataSchema = */ compatibilityParams.disableCreateObjectURL
-      );
-      return svgGfx.getSVG(opList, actualSizeViewport).then(svg => {
+    const promise = pdfPage
+      .getOperatorList({
+        annotationMode: this._annotatationMode,
+      })
+      .then(opList => {
         ensureNotCancelled();
-        this.svg = svg;
-        this.paintedViewportMap.set(svg, actualSizeViewport);
+        const svgGfx = new SVGGraphics(
+          pdfPage.commonObjs,
+          pdfPage.objs,
+          /* forceDataSchema = */ compatibilityParams.disableCreateObjectURL
+        );
+        return svgGfx.getSVG(opList, actualSizeViewport).then(svg => {
+          ensureNotCancelled();
+          this.svg = svg;
+          this.paintedViewportMap.set(svg, actualSizeViewport);
 
-        svg.style.width = wrapper.style.width;
-        svg.style.height = wrapper.style.height;
-        this.renderingState = RenderingStates.FINISHED;
-        wrapper.appendChild(svg);
+          svg.style.width = wrapper.style.width;
+          svg.style.height = wrapper.style.height;
+          this.renderingState = RenderingStates.FINISHED;
+          wrapper.appendChild(svg);
+        });
       });
-    });
 
     return {
       promise,
diff --git a/web/pdf_print_service.js b/web/pdf_print_service.js
index f2bd607f6..58acfeb61 100644
--- a/web/pdf_print_service.js
+++ b/web/pdf_print_service.js
@@ -14,6 +14,7 @@
  */
 
 import { PDFPrintServiceFactory, PDFViewerApplication } from "./app.js";
+import { AnnotationMode } from "pdfjs-lib";
 import { compatibilityParams } from "./app_options.js";
 import { getXfaHtmlForPrinting } from "./print_utils.js";
 
@@ -49,7 +50,7 @@ function renderPage(
       transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0],
       viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }),
       intent: "print",
-      includeAnnotationStorage: true,
+      annotationMode: AnnotationMode.ENABLE_STORAGE,
       optionalContentConfigPromise,
     };
     return pdfPage.render(renderContext).promise;

From 2a0ad8e696dcf16e9b3617275735299d53dc626f Mon Sep 17 00:00:00 2001
From: Jonas Jenwald <jonas.jenwald@gmail.com>
Date: Sat, 21 Aug 2021 15:30:25 +0200
Subject: [PATCH 2/2] Add deprecation warnings for the `renderInteractiveForms`
 and `includeAnnotationStorage` options, in `PDFPageProxy.render`

*This is done separately from the previous patch, to make it easier to revert these changes once they've been included in a couple of releases.*

Please note that because these two options are mutually exclusive, which is a large part of the reason for the previous patch, it's not guaranteed that the fallback-values will always be correct in every situation (but it's the best that we can do).
---
 src/display/api.js | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/src/display/api.js b/src/display/api.js
index 40fe7599f..865c3bb54 100644
--- a/src/display/api.js
+++ b/src/display/api.js
@@ -1350,6 +1350,33 @@ class PDFPageProxy {
     background = null,
     optionalContentConfigPromise = null,
   }) {
+    if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
+      if (arguments[0]?.renderInteractiveForms !== undefined) {
+        deprecated(
+          "render no longer accepts the `renderInteractiveForms`-option, " +
+            "please use the `annotationMode`-option instead."
+        );
+        if (
+          arguments[0].renderInteractiveForms === true &&
+          annotationMode === AnnotationMode.ENABLE
+        ) {
+          annotationMode = AnnotationMode.ENABLE_FORMS;
+        }
+      }
+      if (arguments[0]?.includeAnnotationStorage !== undefined) {
+        deprecated(
+          "render no longer accepts the `includeAnnotationStorage`-option, " +
+            "please use the `annotationMode`-option instead."
+        );
+        if (
+          arguments[0].includeAnnotationStorage === true &&
+          annotationMode === AnnotationMode.ENABLE
+        ) {
+          annotationMode = AnnotationMode.ENABLE_STORAGE;
+        }
+      }
+    }
+
     if (this._stats) {
       this._stats.time("Overall");
     }