Ver código fonte

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

pdfjsbot 2 anos atrás
pai
commit
80e9ead6a6
100 arquivos alterados com 11982 adições e 4981 exclusões
  1. 1 1
      bower.json
  2. 2768 2671
      build/pdf.js
  3. 0 0
      build/pdf.js.map
  4. 0 0
      build/pdf.min.js
  5. 3 3
      build/pdf.sandbox.js
  6. 0 0
      build/pdf.sandbox.js.map
  7. 0 0
      build/pdf.sandbox.min.js
  8. 1074 1000
      build/pdf.worker.js
  9. 0 0
      build/pdf.worker.js.map
  10. 0 0
      build/pdf.worker.min.js
  11. 84 20
      image_decoders/pdf.image_decoders.js
  12. 0 0
      image_decoders/pdf.image_decoders.js.map
  13. 0 0
      image_decoders/pdf.image_decoders.min.js
  14. 324 283
      legacy/build/pdf.js
  15. 0 0
      legacy/build/pdf.js.map
  16. 0 0
      legacy/build/pdf.min.js
  17. 5 5
      legacy/build/pdf.sandbox.js
  18. 0 0
      legacy/build/pdf.sandbox.js.map
  19. 0 0
      legacy/build/pdf.sandbox.min.js
  20. 12 15
      legacy/build/pdf.worker.js
  21. 0 0
      legacy/build/pdf.worker.js.map
  22. 0 0
      legacy/build/pdf.worker.min.js
  23. 1724 117
      legacy/image_decoders/pdf.image_decoders.js
  24. 0 0
      legacy/image_decoders/pdf.image_decoders.js.map
  25. 0 0
      legacy/image_decoders/pdf.image_decoders.min.js
  26. 217 27
      legacy/web/pdf_viewer.css
  27. 82 32
      legacy/web/pdf_viewer.js
  28. 0 0
      legacy/web/pdf_viewer.js.map
  29. 619 102
      lib/core/annotation.js
  30. 3 2
      lib/core/catalog.js
  31. 10 11
      lib/core/cff_parser.js
  32. 46 1
      lib/core/core_utils.js
  33. 3 3
      lib/core/crypto.js
  34. 13 11
      lib/core/default_appearance.js
  35. 94 9
      lib/core/document.js
  36. 50 18
      lib/core/evaluator.js
  37. 5 5
      lib/core/font_renderer.js
  38. 47 17
      lib/core/fonts.js
  39. 1 1
      lib/core/function.js
  40. 24 15
      lib/core/image.js
  41. 11 66
      lib/core/jbig2.js
  42. 3 2
      lib/core/operator_list.js
  43. 3 3
      lib/core/pattern.js
  44. 3 6
      lib/core/primitives.js
  45. 32 1
      lib/core/standard_fonts.js
  46. 4 3
      lib/core/type1_font.js
  47. 8 7
      lib/core/type1_parser.js
  48. 18 8
      lib/core/worker.js
  49. 15 20
      lib/core/writer.js
  50. 1 1
      lib/core/xfa/builder.js
  51. 1 1
      lib/core/xfa/data.js
  52. 2 2
      lib/core/xfa/formcalc_parser.js
  53. 2 2
      lib/core/xfa/html_utils.js
  54. 2 2
      lib/core/xfa/som.js
  55. 50 13
      lib/core/xfa/template.js
  56. 2 2
      lib/core/xfa/text.js
  57. 5 1
      lib/core/xfa/xhtml.js
  58. 1 1
      lib/core/xfa_fonts.js
  59. 2 1
      lib/core/xml_parser.js
  60. 1 1
      lib/core/xref.js
  61. 257 189
      lib/display/annotation_layer.js
  62. 66 10
      lib/display/annotation_storage.js
  63. 49 20
      lib/display/api.js
  64. 7 3
      lib/display/base_factory.js
  65. 23 30
      lib/display/canvas.js
  66. 62 1
      lib/display/display_utils.js
  67. 604 0
      lib/display/editor/annotation_editor_layer.js
  68. 353 0
      lib/display/editor/editor.js
  69. 31 0
      lib/display/editor/fit_curve.js
  70. 384 0
      lib/display/editor/freetext.js
  71. 817 0
      lib/display/editor/ink.js
  72. 814 0
      lib/display/editor/tools.js
  73. 3 3
      lib/display/font_loader.js
  74. 75 33
      lib/display/optional_content_config.js
  75. 27 26
      lib/display/svg.js
  76. 26 11
      lib/display/text_layer.js
  77. 30 18
      lib/display/xfa_layer.js
  78. 9 0
      lib/examples/node/domstubs.js
  79. 37 3
      lib/pdf.js
  80. 3 3
      lib/pdf.sandbox.js
  81. 2 2
      lib/pdf.worker.js
  82. 2 1
      lib/shared/scripting_utils.js
  83. 23 1
      lib/shared/util.js
  84. 273 60
      lib/test/unit/annotation_spec.js
  85. 136 6
      lib/test/unit/api_spec.js
  86. 1 1
      lib/test/unit/custom_spec.js
  87. 2 2
      lib/test/unit/default_appearance_spec.js
  88. 2 2
      lib/test/unit/display_svg_spec.js
  89. 33 0
      lib/test/unit/display_utils_spec.js
  90. 119 0
      lib/test/unit/editor_spec.js
  91. 7 4
      lib/test/unit/function_spec.js
  92. 1 1
      lib/test/unit/jasmine-boot.js
  93. 26 1
      lib/test/unit/pdf_find_controller_spec.js
  94. 14 0
      lib/test/unit/scripting_spec.js
  95. 4 3
      lib/test/unit/test_utils.js
  96. 52 0
      lib/test/unit/text_layer_spec.js
  97. 1 34
      lib/test/unit/ui_utils_spec.js
  98. 122 0
      lib/web/annotation_editor_layer_builder.js
  99. 109 0
      lib/web/annotation_editor_params.js
  100. 1 1
      lib/web/annotation_layer_builder.js

+ 1 - 1
bower.json

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

Diferenças do arquivo suprimidas por serem muito extensas
+ 2768 - 2671
build/pdf.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/pdf.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/pdf.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 3 - 3
build/pdf.sandbox.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/pdf.sandbox.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/pdf.sandbox.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 1074 - 1000
build/pdf.worker.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/pdf.worker.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
build/pdf.worker.min.js


+ 84 - 20
image_decoders/pdf.image_decoders.js

@@ -29,7 +29,7 @@
 		exports["pdfjs-dist/image_decoders/pdf.image_decoders"] = factory();
 	else
 		root["pdfjs-dist/image_decoders/pdf.image_decoders"] = root.pdfjsImageDecoders = factory();
-})(this, () => {
+})(globalThis, () => {
 return /******/ (() => { // webpackBootstrap
 /******/ 	"use strict";
 /******/ 	var __webpack_modules__ = ([
@@ -42,7 +42,7 @@ return /******/ (() => { // webpackBootstrap
 Object.defineProperty(exports, "__esModule", ({
   value: true
 }));
-exports.VerbosityLevel = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.UNSUPPORTED_FEATURES = exports.TextRenderingMode = exports.StreamType = exports.RenderingIntentFlag = exports.PermissionFlag = exports.PasswordResponses = exports.PasswordException = exports.PageActionEventType = exports.OPS = exports.MissingPDFException = exports.InvalidPDFException = exports.ImageKind = exports.IDENTITY_MATRIX = exports.FormatError = exports.FontType = exports.FeatureTest = exports.FONT_IDENTITY_MATRIX = exports.DocumentActionEventType = exports.CMapCompressionType = exports.BaseException = exports.AnnotationType = exports.AnnotationStateModelType = exports.AnnotationReviewState = exports.AnnotationReplyType = exports.AnnotationMode = exports.AnnotationMarkedState = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.AnnotationActionEventType = exports.AbortException = void 0;
+exports.VerbosityLevel = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.UNSUPPORTED_FEATURES = exports.TextRenderingMode = exports.StreamType = exports.RenderingIntentFlag = exports.PermissionFlag = exports.PasswordResponses = exports.PasswordException = exports.PageActionEventType = exports.OPS = exports.MissingPDFException = exports.LINE_FACTOR = exports.LINE_DESCENT_FACTOR = exports.InvalidPDFException = exports.ImageKind = exports.IDENTITY_MATRIX = exports.FormatError = exports.FontType = exports.FeatureTest = exports.FONT_IDENTITY_MATRIX = exports.DocumentActionEventType = exports.CMapCompressionType = exports.BaseException = exports.AnnotationType = exports.AnnotationStateModelType = exports.AnnotationReviewState = exports.AnnotationReplyType = exports.AnnotationMode = exports.AnnotationMarkedState = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationEditorType = exports.AnnotationEditorPrefix = exports.AnnotationEditorParamsType = exports.AnnotationBorderStyleType = exports.AnnotationActionEventType = exports.AbortException = void 0;
 exports.arrayByteLength = arrayByteLength;
 exports.arraysToBytes = arraysToBytes;
 exports.assert = assert;
@@ -75,6 +75,10 @@ const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
 exports.IDENTITY_MATRIX = IDENTITY_MATRIX;
 const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
 exports.FONT_IDENTITY_MATRIX = FONT_IDENTITY_MATRIX;
+const LINE_FACTOR = 1.35;
+exports.LINE_FACTOR = LINE_FACTOR;
+const LINE_DESCENT_FACTOR = 0.35;
+exports.LINE_DESCENT_FACTOR = LINE_DESCENT_FACTOR;
 const RenderingIntentFlag = {
   ANY: 0x01,
   DISPLAY: 0x02,
@@ -92,6 +96,24 @@ const AnnotationMode = {
   ENABLE_STORAGE: 3
 };
 exports.AnnotationMode = AnnotationMode;
+const AnnotationEditorPrefix = "pdfjs_internal_editor_";
+exports.AnnotationEditorPrefix = AnnotationEditorPrefix;
+const AnnotationEditorType = {
+  DISABLE: -1,
+  NONE: 0,
+  FREETEXT: 3,
+  INK: 15
+};
+exports.AnnotationEditorType = AnnotationEditorType;
+const AnnotationEditorParamsType = {
+  FREETEXT_SIZE: 1,
+  FREETEXT_COLOR: 2,
+  FREETEXT_OPACITY: 3,
+  INK_COLOR: 11,
+  INK_THICKNESS: 12,
+  INK_OPACITY: 13
+};
+exports.AnnotationEditorParamsType = AnnotationEditorParamsType;
 const PermissionFlag = {
   PRINT: 0x04,
   MODIFY_CONTENTS: 0x08,
@@ -2433,7 +2455,7 @@ function processSegment(segment, visitor) {
       break;
 
     default:
-      throw new Jbig2Error(`segment type ${header.typeName}(${header.type})` + " is not implemented");
+      throw new Jbig2Error(`segment type ${header.typeName}(${header.type}) is not implemented`);
   }
 
   const callbackName = "on" + header.typeName;
@@ -2614,13 +2636,13 @@ class SimpleSegmentVisitor {
       this.symbols = symbols = {};
     }
 
-    let inputSymbols = [];
+    const inputSymbols = [];
 
-    for (let i = 0, ii = referredSegments.length; i < ii; i++) {
-      const referredSymbols = symbols[referredSegments[i]];
+    for (const referredSegment of referredSegments) {
+      const referredSymbols = symbols[referredSegment];
 
       if (referredSymbols) {
-        inputSymbols = inputSymbols.concat(referredSymbols);
+        inputSymbols.push(...referredSymbols);
       }
     }
 
@@ -2632,13 +2654,13 @@ class SimpleSegmentVisitor {
     const regionInfo = region.info;
     let huffmanTables, huffmanInput;
     const symbols = this.symbols;
-    let inputSymbols = [];
+    const inputSymbols = [];
 
-    for (let i = 0, ii = referredSegments.length; i < ii; i++) {
-      const referredSymbols = symbols[referredSegments[i]];
+    for (const referredSegment of referredSegments) {
+      const referredSymbols = symbols[referredSegment];
 
       if (referredSymbols) {
-        inputSymbols = inputSymbols.concat(referredSymbols);
+        inputSymbols.push(...referredSymbols);
       }
     }
 
@@ -3291,8 +3313,10 @@ exports.escapePDFName = escapePDFName;
 exports.getArrayLookupTableFactory = getArrayLookupTableFactory;
 exports.getInheritableProperty = getInheritableProperty;
 exports.getLookupTableFactory = getLookupTableFactory;
+exports.getNewAnnotationsMap = getNewAnnotationsMap;
 exports.isWhiteSpace = isWhiteSpace;
 exports.log2 = log2;
+exports.numberToString = numberToString;
 exports.parseXFAPath = parseXFAPath;
 exports.readInt8 = readInt8;
 exports.readUint16 = readUint16;
@@ -3586,7 +3610,7 @@ function _collectJS(entry, xref, list, parents) {
         code = js;
       }
 
-      code = code && (0, _util.stringToPDFString)(code);
+      code = code && (0, _util.stringToPDFString)(code).replace(/\u0000/g, "");
 
       if (code) {
         list.push(code);
@@ -3762,6 +3786,49 @@ function recoverJsURL(str) {
   return null;
 }
 
+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 getNewAnnotationsMap(annotationStorage) {
+  if (!annotationStorage) {
+    return null;
+  }
+
+  const newAnnotationsByPage = new Map();
+
+  for (const [key, value] of annotationStorage) {
+    if (!key.startsWith(_util.AnnotationEditorPrefix)) {
+      continue;
+    }
+
+    let annotations = newAnnotationsByPage.get(value.pageIndex);
+
+    if (!annotations) {
+      annotations = [];
+      newAnnotationsByPage.set(value.pageIndex, annotations);
+    }
+
+    annotations.push(value);
+  }
+
+  return newAnnotationsByPage.size > 0 ? newAnnotationsByPage : null;
+}
+
 /***/ }),
 /* 6 */
 /***/ ((__unused_webpack_module, exports, __w_pdfjs_require__) => {
@@ -3794,8 +3861,7 @@ const Name = function NameClosure() {
     }
 
     static get(name) {
-      const nameValue = nameCache[name];
-      return nameValue ? nameValue : nameCache[name] = new Name(name);
+      return nameCache[name] || (nameCache[name] = new Name(name));
     }
 
     static _clearCache() {
@@ -3818,8 +3884,7 @@ const Cmd = function CmdClosure() {
     }
 
     static get(cmd) {
-      const cmdValue = cmdCache[cmd];
-      return cmdValue ? cmdValue : cmdCache[cmd] = new Cmd(cmd);
+      return cmdCache[cmd] || (cmdCache[cmd] = new Cmd(cmd));
     }
 
     static _clearCache() {
@@ -4029,8 +4094,7 @@ const Ref = function RefClosure() {
 
     static get(num, gen) {
       const key = gen === 0 ? `${num}R` : `${num}R${gen}`;
-      const refValue = refCache[key];
-      return refValue ? refValue : refCache[key] = new Ref(num, gen);
+      return refCache[key] || (refCache[key] = new Ref(num, gen));
     }
 
     static _clearCache() {
@@ -8964,8 +9028,8 @@ var _jpg = __w_pdfjs_require__(10);
 
 var _jpx = __w_pdfjs_require__(11);
 
-const pdfjsVersion = '2.14.305';
-const pdfjsBuild = 'eaaa8b4ad';
+const pdfjsVersion = '2.15.349';
+const pdfjsBuild = 'b8aa9c622';
 })();
 
 /******/ 	return __webpack_exports__;

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
image_decoders/pdf.image_decoders.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
image_decoders/pdf.image_decoders.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 324 - 283
legacy/build/pdf.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/build/pdf.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/build/pdf.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 5 - 5
legacy/build/pdf.sandbox.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/build/pdf.sandbox.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/build/pdf.sandbox.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 12 - 15
legacy/build/pdf.worker.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/build/pdf.worker.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/build/pdf.worker.min.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 1724 - 117
legacy/image_decoders/pdf.image_decoders.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/image_decoders/pdf.image_decoders.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/image_decoders/pdf.image_decoders.min.js


+ 217 - 27
legacy/web/pdf_viewer.css

@@ -111,9 +111,30 @@
   --annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
 }
 
+@media (forced-colors: active) {
+  .annotationLayer .textWidgetAnnotation input:required,
+  .annotationLayer .textWidgetAnnotation textarea:required,
+  .annotationLayer .choiceWidgetAnnotation select:required,
+  .annotationLayer .buttonWidgetAnnotation.checkBox input:required,
+  .annotationLayer .buttonWidgetAnnotation.radioButton input:required {
+    outline: 1.5px solid selectedItem;
+  }
+}
+
+.annotationLayer {
+  position: absolute;
+  top: 0;
+  left: 0;
+  pointer-events: none;
+  transform-origin: 0 0;
+}
+
 .annotationLayer section {
   position: absolute;
   text-align: initial;
+  pointer-events: auto;
+  box-sizing: border-box;
+  transform-origin: 0 0;
 }
 
 .annotationLayer .linkAnnotation > a,
@@ -127,10 +148,8 @@
 }
 
 .annotationLayer .buttonWidgetAnnotation.pushButton > canvas {
-  position: relative;
-  top: 0;
-  left: 0;
-  z-index: -1;
+  width: 100%;
+  height: 100%;
 }
 
 .annotationLayer .linkAnnotation > a:hover,
@@ -143,6 +162,8 @@
 .annotationLayer .textAnnotation img {
   position: absolute;
   cursor: pointer;
+  width: 100%;
+  height: 100%;
 }
 
 .annotationLayer .textWidgetAnnotation input,
@@ -153,14 +174,21 @@
   background-image: var(--annotation-unfocused-field-background);
   border: 1px solid transparent;
   box-sizing: border-box;
-  font-size: 9px;
+  font: calc(9px * var(--scale-factor)) sans-serif;
   height: 100%;
   margin: 0;
-  padding: 0 3px;
   vertical-align: top;
   width: 100%;
 }
 
+.annotationLayer .textWidgetAnnotation input:required,
+.annotationLayer .textWidgetAnnotation textarea:required,
+.annotationLayer .choiceWidgetAnnotation select:required,
+.annotationLayer .buttonWidgetAnnotation.checkBox input:required,
+.annotationLayer .buttonWidgetAnnotation.radioButton input:required {
+  outline: 1.5px solid red;
+}
+
 .annotationLayer .choiceWidgetAnnotation select option {
   padding: 0;
 }
@@ -170,8 +198,6 @@
 }
 
 .annotationLayer .textWidgetAnnotation textarea {
-  font: message-box;
-  font-size: 9px;
   resize: none;
 }
 
@@ -213,7 +239,7 @@
 .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:before,
 .annotationLayer .buttonWidgetAnnotation.checkBox input:checked:after,
 .annotationLayer .buttonWidgetAnnotation.radioButton input:checked:before {
-  background-color: rgba(0, 0, 0, 1);
+  background-color: CanvasText;
   content: "";
   display: block;
   position: absolute;
@@ -263,32 +289,40 @@
   -webkit-appearance: none;
      -moz-appearance: none;
           appearance: none;
-  padding: 0;
+}
+
+.annotationLayer .popupTriggerArea {
+  height: 100%;
+  width: 100%;
 }
 
 .annotationLayer .popupWrapper {
   position: absolute;
-  width: 20em;
+  font-size: calc(9px * var(--scale-factor));
+  width: 100%;
+  min-width: calc(180px * var(--scale-factor));
+  pointer-events: none;
 }
 
 .annotationLayer .popup {
   position: absolute;
   z-index: 200;
-  max-width: 20em;
+  max-width: calc(180px * var(--scale-factor));
   background-color: rgba(255, 255, 153, 1);
-  box-shadow: 0 2px 5px rgba(136, 136, 136, 1);
-  border-radius: 2px;
-  padding: 6px;
-  margin-left: 5px;
+  box-shadow: 0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor))
+    rgba(136, 136, 136, 1);
+  border-radius: calc(2px * var(--scale-factor));
+  padding: calc(6px * var(--scale-factor));
+  margin-left: calc(5px * var(--scale-factor));
   cursor: pointer;
   font: message-box;
-  font-size: 9px;
   white-space: normal;
   word-wrap: break-word;
+  pointer-events: auto;
 }
 
 .annotationLayer .popup > * {
-  font-size: 9px;
+  font-size: calc(9px * var(--scale-factor));
 }
 
 .annotationLayer .popup h1 {
@@ -297,17 +331,18 @@
 
 .annotationLayer .popupDate {
   display: inline-block;
-  margin-left: 5px;
+  margin-left: calc(5px * var(--scale-factor));
 }
 
 .annotationLayer .popupContent {
   border-top: 1px solid rgba(51, 51, 51, 1);
-  margin-top: 2px;
-  padding-top: 2px;
+  margin-top: calc(2px * var(--scale-factor));
+  padding-top: calc(2px * var(--scale-factor));
 }
 
 .annotationLayer .richText > * {
   white-space: pre-wrap;
+  font-size: calc(9px * var(--scale-factor));
 }
 
 .annotationLayer .highlightAnnotation,
@@ -327,11 +362,23 @@
   cursor: pointer;
 }
 
+.annotationLayer section svg {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+}
+
 
 :root {
   --xfa-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
 }
 
+@media (forced-colors: active) {
+  .xfaLayer *:required {
+    outline: 1.5px solid selectedItem;
+  }
+}
+
 .xfaLayer .highlight {
   margin: -1px;
   padding: 1px;
@@ -406,6 +453,10 @@
   line-height: inherit;
 }
 
+.xfaLayer *:required {
+  outline: 1.5px solid red;
+}
+
 .xfaLayer div {
   pointer-events: none;
 }
@@ -654,24 +705,163 @@
   }
 }
 
+
+:root {
+  --focus-outline: solid 2px red;
+  --hover-outline: dashed 2px blue;
+  --freetext-line-height: 1.35;
+  --freetext-padding: 2px;
+  --editorInk-editing-cursor: url(images/toolbarButton-editorInk.svg) 0 16;
+}
+
+@media (forced-colors: active) {
+  :root {
+    --focus-outline: solid 3px ButtonText;
+    --hover-outline: dashed 3px ButtonText;
+  }
+}
+
+[data-editor-rotation="90"] {
+  transform: rotate(90deg);
+}
+[data-editor-rotation="180"] {
+  transform: rotate(180deg);
+}
+[data-editor-rotation="270"] {
+  transform: rotate(270deg);
+}
+
+.annotationEditorLayer {
+  background: transparent;
+  position: absolute;
+  top: 0;
+  left: 0;
+  font-size: calc(100px * var(--scale-factor));
+  transform-origin: 0 0;
+}
+
+.annotationEditorLayer .selectedEditor {
+  outline: var(--focus-outline);
+  resize: none;
+}
+
+.annotationEditorLayer .freeTextEditor {
+  position: absolute;
+  background: transparent;
+  border-radius: 3px;
+  padding: calc(var(--freetext-padding) * var(--scale-factor));
+  resize: none;
+  width: auto;
+  height: auto;
+  z-index: 1;
+  transform-origin: 0 0;
+  touch-action: none;
+}
+
+.annotationEditorLayer .freeTextEditor .internal {
+  background: transparent;
+  border: none;
+  top: 0;
+  left: 0;
+  overflow: visible;
+  white-space: nowrap;
+  resize: none;
+  font: 10px sans-serif;
+  line-height: var(--freetext-line-height);
+}
+
+.annotationEditorLayer .freeTextEditor .overlay {
+  position: absolute;
+  display: none;
+  background: transparent;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.annotationEditorLayer .freeTextEditor .overlay.enabled {
+  display: block;
+}
+
+.annotationEditorLayer .freeTextEditor .internal:empty::before {
+  content: attr(default-content);
+  color: gray;
+}
+
+.annotationEditorLayer .freeTextEditor .internal:focus {
+  outline: none;
+}
+
+.annotationEditorLayer .inkEditor.disabled {
+  resize: none;
+}
+
+.annotationEditorLayer .inkEditor.disabled.selectedEditor {
+  resize: horizontal;
+}
+
+.annotationEditorLayer .freeTextEditor:hover:not(.selectedEditor),
+.annotationEditorLayer .inkEditor:hover:not(.selectedEditor) {
+  outline: var(--hover-outline);
+}
+
+.annotationEditorLayer .inkEditor {
+  position: absolute;
+  background: transparent;
+  border-radius: 3px;
+  overflow: auto;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
+  transform-origin: 0 0;
+  cursor: auto;
+}
+
+.annotationEditorLayer .inkEditor.editing {
+  resize: none;
+  cursor: var(--editorInk-editing-cursor), pointer;
+}
+
+.annotationEditorLayer .inkEditor .inkEditorCanvas {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  touch-action: none;
+}
+
 :root {
   --viewer-container-height: 0;
   --pdfViewer-padding-bottom: 0;
   --page-margin: 1px auto -8px;
   --page-border: 9px solid transparent;
+  --page-border-image: url(images/shadow.png) 9 9 repeat;
   --spreadHorizontalWrapped-margin-LR: -3.5px;
-  --zoom-factor: 1;
+  --scale-factor: 1;
 }
 
 @media screen and (forced-colors: active) {
   :root {
     --pdfViewer-padding-bottom: 9px;
-    --page-margin: 9px auto 0;
-    --page-border: none;
-    --spreadHorizontalWrapped-margin-LR: 4.5px;
+    --page-margin: 8px auto -1px;
+    --page-border: 1px solid CanvasText;
+    --page-border-image: none;
+    --spreadHorizontalWrapped-margin-LR: 3.5px;
   }
 }
 
+[data-main-rotation="90"] {
+  transform: rotate(90deg) translateY(-100%);
+}
+[data-main-rotation="180"] {
+  transform: rotate(180deg) translate(-100%, -100%);
+}
+[data-main-rotation="270"] {
+  transform: rotate(270deg) translateX(-100%);
+}
+
 .pdfViewer {
   padding-bottom: var(--pdfViewer-padding-bottom);
 }
@@ -688,9 +878,9 @@
   position: relative;
   overflow: visible;
   border: var(--page-border);
+  -o-border-image: var(--page-border-image);
+     border-image: var(--page-border-image);
   background-clip: content-box;
-  -o-border-image: url(images/shadow.png) 9 9 repeat;
-     border-image: url(images/shadow.png) 9 9 repeat;
   background-color: rgba(255, 255, 255, 1);
 }
 

Diferenças do arquivo suprimidas por serem muito extensas
+ 82 - 32
legacy/web/pdf_viewer.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
legacy/web/pdf_viewer.js.map


Diferenças do arquivo suprimidas por serem muito extensas
+ 619 - 102
lib/core/annotation.js


+ 3 - 2
lib/core/catalog.js

@@ -1058,7 +1058,8 @@ class Catalog {
         javaScript = new Map();
       }
 
-      javaScript.set(name, (0, _util.stringToPDFString)(js));
+      js = (0, _util.stringToPDFString)(js).replace(/\u0000/g, "");
+      javaScript.set(name, js);
     }
 
     if (obj instanceof _primitives.Dict && obj.has("JavaScript")) {
@@ -1290,7 +1291,7 @@ class Catalog {
     }
 
     while (queue.length > 0) {
-      const queueItem = queue[queue.length - 1];
+      const queueItem = queue.at(-1);
       const {
         currentNode,
         posInKids

+ 10 - 11
lib/core/cff_parser.js

@@ -1368,10 +1368,12 @@ class CFFCompiler {
     const output = {
       data: [],
       length: 0,
-      add: function CFFCompiler_add(data) {
+
+      add(data) {
         this.data = this.data.concat(data);
         this.length = this.data.length;
       }
+
     };
     const header = this.compileHeader(cff.header);
     output.add(header);
@@ -1605,12 +1607,9 @@ class CFFCompiler {
   }
 
   compileDict(dict, offsetTracker) {
-    let out = [];
-    const order = dict.order;
-
-    for (let i = 0; i < order.length; ++i) {
-      const key = order[i];
+    const out = [];
 
+    for (const key of dict.order) {
       if (!(key in dict.values)) {
         continue;
       }
@@ -1637,7 +1636,7 @@ class CFFCompiler {
         switch (type) {
           case "num":
           case "sid":
-            out = out.concat(this.encodeNumber(value));
+            out.push(...this.encodeNumber(value));
             break;
 
           case "offset":
@@ -1647,15 +1646,15 @@ class CFFCompiler {
               offsetTracker.track(name, out.length);
             }
 
-            out = out.concat([0x1d, 0, 0, 0, 0]);
+            out.push(0x1d, 0, 0, 0, 0);
             break;
 
           case "array":
           case "delta":
-            out = out.concat(this.encodeNumber(value));
+            out.push(...this.encodeNumber(value));
 
             for (let k = 1, kk = values.length; k < kk; ++k) {
-              out = out.concat(this.encodeNumber(values[k]));
+              out.push(...this.encodeNumber(values[k]));
             }
 
             break;
@@ -1665,7 +1664,7 @@ class CFFCompiler {
         }
       }
 
-      out = out.concat(dict.opcodes[key]);
+      out.push(...dict.opcodes[key]);
     }
 
     return out;

+ 46 - 1
lib/core/core_utils.js

@@ -31,8 +31,10 @@ exports.escapePDFName = escapePDFName;
 exports.getArrayLookupTableFactory = getArrayLookupTableFactory;
 exports.getInheritableProperty = getInheritableProperty;
 exports.getLookupTableFactory = getLookupTableFactory;
+exports.getNewAnnotationsMap = getNewAnnotationsMap;
 exports.isWhiteSpace = isWhiteSpace;
 exports.log2 = log2;
+exports.numberToString = numberToString;
 exports.parseXFAPath = parseXFAPath;
 exports.readInt8 = readInt8;
 exports.readUint16 = readUint16;
@@ -326,7 +328,7 @@ function _collectJS(entry, xref, list, parents) {
         code = js;
       }
 
-      code = code && (0, _util.stringToPDFString)(code);
+      code = code && (0, _util.stringToPDFString)(code).replace(/\u0000/g, "");
 
       if (code) {
         list.push(code);
@@ -500,4 +502,47 @@ function recoverJsURL(str) {
   }
 
   return null;
+}
+
+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 getNewAnnotationsMap(annotationStorage) {
+  if (!annotationStorage) {
+    return null;
+  }
+
+  const newAnnotationsByPage = new Map();
+
+  for (const [key, value] of annotationStorage) {
+    if (!key.startsWith(_util.AnnotationEditorPrefix)) {
+      continue;
+    }
+
+    let annotations = newAnnotationsByPage.get(value.pageIndex);
+
+    if (!annotations) {
+      annotations = [];
+      newAnnotationsByPage.set(value.pageIndex, annotations);
+    }
+
+    annotations.push(value);
+  }
+
+  return newAnnotationsByPage.size > 0 ? newAnnotationsByPage : null;
 }

+ 3 - 3
lib/core/crypto.js

@@ -851,7 +851,7 @@ class AESBaseCipher {
     let outputLength = 16 * result.length;
 
     if (finalize) {
-      const lastBlock = result[result.length - 1];
+      const lastBlock = result.at(-1);
       let psLen = lastBlock[15];
 
       if (psLen <= 16) {
@@ -1103,7 +1103,7 @@ const PDF20 = function PDF20Closure() {
     let e = [0];
     let i = 0;
 
-    while (i < 64 || e[e.length - 1] > i - 32) {
+    while (i < 64 || e.at(-1) > i - 32) {
       const combinedLength = password.length + k.length + userBytes.length,
             combinedArray = new Uint8Array(combinedLength);
       let writeOffset = 0;
@@ -1587,7 +1587,7 @@ const CipherTransformFactory = function CipherTransformFactoryClosure() {
 
     createCipherTransform(num, gen) {
       if (this.algorithm === 4 || this.algorithm === 5) {
-        return new CipherTransform(buildCipherConstructor(this.cf, this.stmf, num, gen, this.encryptionKey), buildCipherConstructor(this.cf, this.strf, num, gen, this.encryptionKey));
+        return new CipherTransform(buildCipherConstructor(this.cf, this.strf, num, gen, this.encryptionKey), buildCipherConstructor(this.cf, this.stmf, num, gen, this.encryptionKey));
       }
 
       const key = buildObjectKey(num, gen, this.encryptionKey, false);

+ 13 - 11
lib/core/default_appearance.js

@@ -25,14 +25,15 @@ Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.createDefaultAppearance = createDefaultAppearance;
+exports.getPdfColor = getPdfColor;
 exports.parseDefaultAppearance = parseDefaultAppearance;
 
+var _core_utils = require("./core_utils.js");
+
 var _util = require("../shared/util.js");
 
 var _colorspace = require("./colorspace.js");
 
-var _core_utils = require("./core_utils.js");
-
 var _evaluator = require("./evaluator.js");
 
 var _primitives = require("./primitives.js");
@@ -115,18 +116,19 @@ function parseDefaultAppearance(str) {
   return new DefaultAppearanceEvaluator(str).parse();
 }
 
+function getPdfColor(color, isFill) {
+  if (color[0] === color[1] && color[1] === color[2]) {
+    const gray = color[0] / 255;
+    return `${(0, _core_utils.numberToString)(gray)} ${isFill ? "g" : "G"}`;
+  }
+
+  return Array.from(color).map(c => (0, _core_utils.numberToString)(c / 255)).join(" ") + ` ${isFill ? "rg" : "RG"}`;
+}
+
 function createDefaultAppearance({
   fontSize,
   fontName,
   fontColor
 }) {
-  let colorCmd;
-
-  if (fontColor.every(c => c === 0)) {
-    colorCmd = "0 g";
-  } else {
-    colorCmd = Array.from(fontColor).map(c => (c / 255).toFixed(2)).join(" ") + " rg";
-  }
-
-  return `/${(0, _core_utils.escapePDFName)(fontName)} ${fontSize} Tf ${colorCmd}`;
+  return `/${(0, _core_utils.escapePDFName)(fontName)} ${fontSize} Tf ${getPdfColor(fontColor, true)}`;
 }

+ 94 - 9
lib/core/document.js

@@ -60,6 +60,8 @@ var _decode_stream = require("./decode_stream.js");
 
 var _struct_tree = require("./struct_tree.js");
 
+var _writer = require("./writer.js");
+
 var _factory = require("./xfa/factory.js");
 
 var _xref = require("./xref.js");
@@ -137,7 +139,9 @@ class Page {
   }
 
   get resources() {
-    return (0, _util.shadow)(this, "resources", this._getInheritableProperty("Resources") || _primitives.Dict.empty);
+    const resources = this._getInheritableProperty("Resources");
+
+    return (0, _util.shadow)(this, "resources", resources instanceof _primitives.Dict ? resources : _primitives.Dict.empty);
   }
 
   _getBoundingBox(name) {
@@ -244,6 +248,55 @@ class Page {
     } : null);
   }
 
+  async saveNewAnnotations(handler, task, annotations) {
+    if (this.xfaFactory) {
+      throw new Error("XFA: Cannot save new annotations.");
+    }
+
+    const partialEvaluator = new _evaluator.PartialEvaluator({
+      xref: this.xref,
+      handler,
+      pageIndex: this.pageIndex,
+      idFactory: this._localIdFactory,
+      fontCache: this.fontCache,
+      builtInCMapCache: this.builtInCMapCache,
+      standardFontDataCache: this.standardFontDataCache,
+      globalImageCache: this.globalImageCache,
+      options: this.evaluatorOptions
+    });
+    const pageDict = this.pageDict;
+    const annotationsArray = this.annotations.slice();
+    const newData = await _annotation.AnnotationFactory.saveNewAnnotations(partialEvaluator, task, annotations);
+
+    for (const {
+      ref
+    } of newData.annotations) {
+      annotationsArray.push(ref);
+    }
+
+    const savedDict = pageDict.get("Annots");
+    pageDict.set("Annots", annotationsArray);
+    const buffer = [];
+    let transform = null;
+
+    if (this.xref.encrypt) {
+      transform = this.xref.encrypt.createCipherTransform(this.ref.num, this.ref.gen);
+    }
+
+    (0, _writer.writeObject)(this.ref, pageDict, buffer, transform);
+
+    if (savedDict) {
+      pageDict.set("Annots", savedDict);
+    }
+
+    const objects = newData.dependencies;
+    objects.push({
+      ref: this.ref,
+      data: buffer.join("")
+    }, ...newData.annotations);
+    return objects;
+  }
+
   save(handler, task, annotationStorage) {
     const partialEvaluator = new _evaluator.PartialEvaluator({
       xref: this.xref,
@@ -270,7 +323,9 @@ class Page {
         }));
       }
 
-      return Promise.all(newRefsPromises);
+      return Promise.all(newRefsPromises).then(function (newRefs) {
+        return newRefs.filter(newRef => !!newRef);
+      });
     });
   }
 
@@ -306,6 +361,17 @@ class Page {
       globalImageCache: this.globalImageCache,
       options: this.evaluatorOptions
     });
+    const newAnnotationsByPage = !this.xfaFactory ? (0, _core_utils.getNewAnnotationsMap)(annotationStorage) : null;
+    let newAnnotationsPromise = Promise.resolve(null);
+
+    if (newAnnotationsByPage) {
+      const newAnnotations = newAnnotationsByPage.get(this.pageIndex);
+
+      if (newAnnotations) {
+        newAnnotationsPromise = _annotation.AnnotationFactory.printNewAnnotations(partialEvaluator, task, newAnnotations);
+      }
+    }
+
     const dataPromises = Promise.all([contentStreamPromise, resourcesPromise]);
     const pageListPromise = dataPromises.then(([contentStream]) => {
       const opList = new _operator_list.OperatorList(intent, sink);
@@ -323,7 +389,11 @@ class Page {
         return opList;
       });
     });
-    return Promise.all([pageListPromise, this._parsedAnnotations]).then(function ([pageOpList, annotations]) {
+    return Promise.all([pageListPromise, this._parsedAnnotations, newAnnotationsPromise]).then(function ([pageOpList, annotations, newAnnotations]) {
+      if (newAnnotations) {
+        annotations = annotations.concat(newAnnotations);
+      }
+
       if (annotations.length === 0 || intent & _util.RenderingIntentFlag.ANNOTATIONS_DISABLE) {
         pageOpList.flush(true);
         return {
@@ -347,14 +417,29 @@ class Page {
       }
 
       return Promise.all(opListPromises).then(function (opLists) {
-        pageOpList.addOp(_util.OPS.beginAnnotations, []);
-
-        for (const opList of opLists) {
+        let form = false,
+            canvas = false;
+
+        for (const {
+          opList,
+          separateForm,
+          separateCanvas
+        } of opLists) {
           pageOpList.addOpList(opList);
+
+          if (separateForm) {
+            form = separateForm;
+          }
+
+          if (separateCanvas) {
+            canvas = separateCanvas;
+          }
         }
 
-        pageOpList.addOp(_util.OPS.endAnnotations, []);
-        pageOpList.flush(true);
+        pageOpList.flush(true, {
+          form,
+          canvas
+        });
         return {
           length: pageOpList.totalLength
         };
@@ -898,7 +983,7 @@ class PDFDocument {
     const pdfFonts = [];
     const initialState = {
       get font() {
-        return pdfFonts[pdfFonts.length - 1];
+        return pdfFonts.at(-1);
       },
 
       set font(font) {

+ 50 - 18
lib/core/evaluator.js

@@ -1038,11 +1038,9 @@ class PartialEvaluator {
     let fontRef;
 
     if (font) {
-      if (!(font instanceof _primitives.Ref)) {
-        throw new _util.FormatError('The "font" object should be a reference.');
+      if (font instanceof _primitives.Ref) {
+        fontRef = font;
       }
-
-      fontRef = font;
     } else {
       const fontRes = resources.get("Font");
 
@@ -2611,7 +2609,7 @@ class PartialEvaluator {
               }
             }
 
-            const item = elements[elements.length - 1];
+            const item = elements.at(-1);
 
             if (typeof item === "string") {
               showSpacedTextBuffer.push(item);
@@ -2830,6 +2828,8 @@ class PartialEvaluator {
             return;
 
           case _util.OPS.beginMarkedContent:
+            flushTextContentItem();
+
             if (includeMarkedContent) {
               textContent.items.push({
                 type: "beginMarkedContent",
@@ -2840,8 +2840,9 @@ class PartialEvaluator {
             break;
 
           case _util.OPS.beginMarkedContentProps:
+            flushTextContentItem();
+
             if (includeMarkedContent) {
-              flushTextContentItem();
               let mcid = null;
 
               if (args[1] instanceof _primitives.Dict) {
@@ -2858,8 +2859,9 @@ class PartialEvaluator {
             break;
 
           case _util.OPS.endMarkedContent:
+            flushTextContentItem();
+
             if (includeMarkedContent) {
-              flushTextContentItem();
               textContent.items.push({
                 type: "endMarkedContent"
               });
@@ -2914,10 +2916,18 @@ class PartialEvaluator {
         };
       }
 
-      const cidToGidMap = dict.get("CIDToGIDMap");
+      try {
+        const cidToGidMap = dict.get("CIDToGIDMap");
+
+        if (cidToGidMap instanceof _base_stream.BaseStream) {
+          cidToGidBytes = cidToGidMap.getBytes();
+        }
+      } catch (ex) {
+        if (!this.options.ignoreErrors) {
+          throw ex;
+        }
 
-      if (cidToGidMap instanceof _base_stream.BaseStream) {
-        cidToGidBytes = cidToGidMap.getBytes();
+        (0, _util.warn)(`extractDataStructures - ignoring CIDToGIDMap data: "${ex}".`);
       }
     }
 
@@ -3861,7 +3871,12 @@ class TranslatedFont {
     const charProcs = this.dict.get("CharProcs");
     const fontResources = this.dict.get("Resources") || resources;
     const charProcOperatorList = Object.create(null);
-    const isEmptyBBox = !translatedFont.bbox || (0, _util.isArrayEqual)(translatedFont.bbox, [0, 0, 0, 0]);
+
+    const fontBBox = _util.Util.normalizeRect(translatedFont.bbox || [0, 0, 0, 0]),
+          width = fontBBox[2] - fontBBox[0],
+          height = fontBBox[3] - fontBBox[1];
+
+    const fontBBoxSize = Math.hypot(width, height);
 
     for (const key of charProcs.getKeys()) {
       loadCharProcsPromise = loadCharProcsPromise.then(() => {
@@ -3874,7 +3889,7 @@ class TranslatedFont {
           operatorList
         }).then(() => {
           if (operatorList.fnArray[0] === _util.OPS.setCharWidthAndBounds) {
-            this._removeType3ColorOperators(operatorList, isEmptyBBox);
+            this._removeType3ColorOperators(operatorList, fontBBoxSize);
           }
 
           charProcOperatorList[key] = operatorList.getIR();
@@ -3901,25 +3916,35 @@ class TranslatedFont {
     return this.type3Loaded;
   }
 
-  _removeType3ColorOperators(operatorList, isEmptyBBox = false) {
-    if (isEmptyBBox) {
+  _removeType3ColorOperators(operatorList, fontBBoxSize = NaN) {
+    const charBBox = _util.Util.normalizeRect(operatorList.argsArray[0].slice(2)),
+          width = charBBox[2] - charBBox[0],
+          height = charBBox[3] - charBBox[1];
+
+    const charBBoxSize = Math.hypot(width, height);
+
+    if (width === 0 || height === 0) {
+      operatorList.fnArray.splice(0, 1);
+      operatorList.argsArray.splice(0, 1);
+    } else if (fontBBoxSize === 0 || Math.round(charBBoxSize / fontBBoxSize) >= 10) {
       if (!this._bbox) {
         this._bbox = [Infinity, Infinity, -Infinity, -Infinity];
       }
 
-      const charBBox = _util.Util.normalizeRect(operatorList.argsArray[0].slice(2));
-
       this._bbox[0] = Math.min(this._bbox[0], charBBox[0]);
       this._bbox[1] = Math.min(this._bbox[1], charBBox[1]);
       this._bbox[2] = Math.max(this._bbox[2], charBBox[2]);
       this._bbox[3] = Math.max(this._bbox[3], charBBox[3]);
     }
 
-    let i = 1,
+    let i = 0,
         ii = operatorList.length;
 
     while (i < ii) {
       switch (operatorList.fnArray[i]) {
+        case _util.OPS.setCharWidthAndBounds:
+          break;
+
         case _util.OPS.setStrokeColorSpace:
         case _util.OPS.setFillColorSpace:
         case _util.OPS.setStrokeColor:
@@ -4469,6 +4494,7 @@ class EvaluatorPreprocessor {
     });
     this.stateManager = stateManager;
     this.nonProcessedArgs = [];
+    this._isPathOp = false;
     this._numInvalidPathOPS = 0;
   }
 
@@ -4495,6 +4521,12 @@ class EvaluatorPreprocessor {
         const numArgs = opSpec.numArgs;
         let argsLength = args !== null ? args.length : 0;
 
+        if (!this._isPathOp) {
+          this._numInvalidPathOPS = 0;
+        }
+
+        this._isPathOp = fn >= _util.OPS.moveTo && fn <= _util.OPS.endPath;
+
         if (!opSpec.variableArgs) {
           if (argsLength !== numArgs) {
             const nonProcessedArgs = this.nonProcessedArgs;
@@ -4517,7 +4549,7 @@ class EvaluatorPreprocessor {
           if (argsLength < numArgs) {
             const partialMsg = `command ${cmd}: expected ${numArgs} args, ` + `but received ${argsLength} args.`;
 
-            if (fn >= _util.OPS.moveTo && fn <= _util.OPS.endPath && ++this._numInvalidPathOPS > EvaluatorPreprocessor.MAX_INVALID_PATH_OPS) {
+            if (this._isPathOp && ++this._numInvalidPathOPS > EvaluatorPreprocessor.MAX_INVALID_PATH_OPS) {
               throw new _util.FormatError(`Invalid ${partialMsg}`);
             }
 

+ 5 - 5
lib/core/font_renderer.js

@@ -310,7 +310,7 @@ function compileGlyf(code, cmds, font) {
 
     const instructionLength = getUint16(code, i);
     i += 2 + instructionLength;
-    const numberOfPoints = endPtsOfContours[endPtsOfContours.length - 1] + 1;
+    const numberOfPoints = endPtsOfContours.at(-1) + 1;
     const points = [];
 
     while (points.length < numberOfPoints) {
@@ -374,13 +374,13 @@ function compileGlyf(code, cmds, font) {
 
       if (contour[0].flags & 1) {
         contour.push(contour[0]);
-      } else if (contour[contour.length - 1].flags & 1) {
-        contour.unshift(contour[contour.length - 1]);
+      } else if (contour.at(-1).flags & 1) {
+        contour.unshift(contour.at(-1));
       } else {
         const p = {
           flags: 1,
-          x: (contour[0].x + contour[contour.length - 1].x) / 2,
-          y: (contour[0].y + contour[contour.length - 1].y) / 2
+          x: (contour[0].x + contour.at(-1).x) / 2,
+          y: (contour[0].y + contour.at(-1).y) / 2
         };
         contour.unshift(p);
         contour.push(p);

+ 47 - 17
lib/core/fonts.js

@@ -332,11 +332,14 @@ function convertCidString(charCode, cid, shouldThrow = false) {
   return cid;
 }
 
-function adjustMapping(charCodeToGlyphId, hasGlyph, newGlyphZeroId) {
+function adjustMapping(charCodeToGlyphId, hasGlyph, newGlyphZeroId, toUnicode) {
   const newMap = Object.create(null);
+  const toUnicodeExtraMap = new Map();
   const toFontChar = [];
+  const usedGlyphIds = new Set();
   let privateUseAreaIndex = 0;
-  let nextAvailableFontCharCode = PRIVATE_USE_AREAS[privateUseAreaIndex][0];
+  const privateUseOffetStart = PRIVATE_USE_AREAS[privateUseAreaIndex][0];
+  let nextAvailableFontCharCode = privateUseOffetStart;
   let privateUseOffetEnd = PRIVATE_USE_AREAS[privateUseAreaIndex][1];
 
   for (let originalCharCode in charCodeToGlyphId) {
@@ -365,6 +368,17 @@ function adjustMapping(charCodeToGlyphId, hasGlyph, newGlyphZeroId) {
       glyphId = newGlyphZeroId;
     }
 
+    let unicode = toUnicode.get(originalCharCode);
+
+    if (typeof unicode === "string") {
+      unicode = unicode.codePointAt(0);
+    }
+
+    if (unicode && unicode < privateUseOffetStart && !usedGlyphIds.has(glyphId)) {
+      toUnicodeExtraMap.set(unicode, glyphId);
+      usedGlyphIds.add(glyphId);
+    }
+
     newMap[fontCharCode] = glyphId;
     toFontChar[originalCharCode] = fontCharCode;
   }
@@ -372,11 +386,12 @@ function adjustMapping(charCodeToGlyphId, hasGlyph, newGlyphZeroId) {
   return {
     toFontChar,
     charCodeToGlyphId: newMap,
+    toUnicodeExtraMap,
     nextAvailableFontCharCode
   };
 }
 
-function getRanges(glyphs, numGlyphs) {
+function getRanges(glyphs, toUnicodeExtraMap, numGlyphs) {
   const codes = [];
 
   for (const charCode in glyphs) {
@@ -390,6 +405,19 @@ function getRanges(glyphs, numGlyphs) {
     });
   }
 
+  if (toUnicodeExtraMap) {
+    for (const [unicode, glyphId] of toUnicodeExtraMap) {
+      if (glyphId >= numGlyphs) {
+        continue;
+      }
+
+      codes.push({
+        fontCharCode: unicode,
+        glyphId
+      });
+    }
+  }
+
   if (codes.length === 0) {
     codes.push({
       fontCharCode: 0,
@@ -425,9 +453,9 @@ function getRanges(glyphs, numGlyphs) {
   return ranges;
 }
 
-function createCmapTable(glyphs, numGlyphs) {
-  const ranges = getRanges(glyphs, numGlyphs);
-  const numTables = ranges[ranges.length - 1][1] > 0xffff ? 2 : 1;
+function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
+  const ranges = getRanges(glyphs, toUnicodeExtraMap, numGlyphs);
+  const numTables = ranges.at(-1)[1] > 0xffff ? 2 : 1;
   let cmap = "\x00\x00" + string16(numTables) + "\x00\x03" + "\x00\x01" + (0, _util.string32)(4 + numTables * 8);
   let i, ii, j, jj;
 
@@ -988,7 +1016,7 @@ class Font {
       const offset = file.getInt32() >>> 0;
       const length = file.getInt32() >>> 0;
       const previousPosition = file.pos;
-      file.pos = file.start ? file.start : 0;
+      file.pos = file.start || 0;
       file.skip(offset);
       const data = file.getBytes(length);
       file.pos = previousPosition;
@@ -1125,7 +1153,7 @@ class Font {
       }
 
       let segment;
-      let start = (file.start ? file.start : 0) + cmap.offset;
+      let start = (file.start || 0) + cmap.offset;
       file.pos = start;
       file.skip(2);
       const numTables = file.getUint16();
@@ -1401,7 +1429,7 @@ class Font {
         return;
       }
 
-      file.pos = (file.start ? file.start : 0) + header.offset;
+      file.pos = (file.start || 0) + header.offset;
       file.pos += 4;
       file.pos += 2;
       file.pos += 2;
@@ -1723,7 +1751,7 @@ class Font {
     }
 
     function readPostScriptTable(post, propertiesObj, maxpNumGlyphs) {
-      const start = (font.start ? font.start : 0) + post.offset;
+      const start = (font.start || 0) + post.offset;
       font.pos = start;
       const length = post.length,
             end = start + length;
@@ -1811,7 +1839,7 @@ class Font {
     }
 
     function readNameTable(nameTable) {
-      const start = (font.start ? font.start : 0) + nameTable.offset;
+      const start = (font.start || 0) + nameTable.offset;
       font.pos = start;
       const names = [[], []];
       const length = nameTable.length,
@@ -1943,7 +1971,7 @@ class Font {
           }
         } else if (op === 0x2b && !tooComplexToFollowFunctions) {
           if (!inFDEF && !inELSE) {
-            funcId = stack[stack.length - 1];
+            funcId = stack.at(-1);
 
             if (isNaN(funcId)) {
               (0, _util.info)("TT: CALL empty stack (or invalid entry).");
@@ -2031,7 +2059,7 @@ class Font {
           --ifLevel;
         } else if (op === 0x1c) {
           if (!inFDEF && !inELSE) {
-            const offset = stack[stack.length - 1];
+            const offset = stack.at(-1);
 
             if (offset > 0) {
               i += offset - 1;
@@ -2492,11 +2520,11 @@ class Font {
     }
 
     if (!properties.cssFontInfo) {
-      const newMapping = adjustMapping(charCodeToGlyphId, hasGlyph, glyphZeroId);
+      const newMapping = adjustMapping(charCodeToGlyphId, hasGlyph, glyphZeroId, this.toUnicode);
       this.toFontChar = newMapping.toFontChar;
       tables.cmap = {
         tag: "cmap",
-        data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphsOut)
+        data: createCmapTable(newMapping.charCodeToGlyphId, newMapping.toUnicodeExtraMap, numGlyphsOut)
       };
 
       if (!tables["OS/2"] || !validateOS2Table(tables["OS/2"], font)) {
@@ -2556,11 +2584,13 @@ class Font {
     const mapping = font.getGlyphMapping(properties);
     let newMapping = null;
     let newCharCodeToGlyphId = mapping;
+    let toUnicodeExtraMap = null;
 
     if (!properties.cssFontInfo) {
-      newMapping = adjustMapping(mapping, font.hasGlyphId.bind(font), glyphZeroId);
+      newMapping = adjustMapping(mapping, font.hasGlyphId.bind(font), glyphZeroId, this.toUnicode);
       this.toFontChar = newMapping.toFontChar;
       newCharCodeToGlyphId = newMapping.charCodeToGlyphId;
+      toUnicodeExtraMap = newMapping.toUnicodeExtraMap;
     }
 
     const numGlyphs = font.numGlyphs;
@@ -2641,7 +2671,7 @@ class Font {
     const builder = new _opentype_file_builder.OpenTypeFileBuilder("\x4F\x54\x54\x4F");
     builder.addTable("CFF ", font.data);
     builder.addTable("OS/2", createOS2Table(properties, newCharCodeToGlyphId));
-    builder.addTable("cmap", createCmapTable(newCharCodeToGlyphId, numGlyphs));
+    builder.addTable("cmap", createCmapTable(newCharCodeToGlyphId, toUnicodeExtraMap, numGlyphs));
     builder.addTable("head", "\x00\x01\x00\x00" + "\x00\x00\x10\x00" + "\x00\x00\x00\x00" + "\x5F\x0F\x3C\xF5" + "\x00\x00" + safeString16(unitsPerEm) + "\x00\x00\x00\x00\x9e\x0b\x7e\x27" + "\x00\x00\x00\x00\x9e\x0b\x7e\x27" + "\x00\x00" + safeString16(properties.descent) + "\x0F\xFF" + safeString16(properties.ascent) + string16(properties.italicAngle ? 2 : 0) + "\x00\x11" + "\x00\x00" + "\x00\x00" + "\x00\x00");
     builder.addTable("hhea", "\x00\x01\x00\x00" + safeString16(properties.ascent) + safeString16(properties.descent) + "\x00\x00" + "\xFF\xFF" + "\x00\x00" + "\x00\x00" + "\x00\x00" + safeString16(properties.capHeight) + safeString16(Math.tan(properties.italicAngle) * properties.xHeight) + "\x00\x00" + "\x00\x00" + "\x00\x00" + "\x00\x00" + "\x00\x00" + "\x00\x00" + string16(numGlyphs));
     builder.addTable("hmtx", function fontFieldsHmtx() {

+ 1 - 1
lib/core/function.js

@@ -1261,7 +1261,7 @@ const PostScriptCompiler = function PostScriptCompilerClosure() {
               break;
             }
 
-            ast1 = stack[stack.length - 1];
+            ast1 = stack.at(-1);
 
             if (ast1.type === "literal" || ast1.type === "var") {
               stack.push(ast1);

+ 24 - 15
lib/core/image.js

@@ -107,26 +107,35 @@ class PDFImage {
     this.image = image;
     const dict = image.dict;
     const filter = dict.get("F", "Filter");
+    let filterName;
 
     if (filter instanceof _primitives.Name) {
-      switch (filter.name) {
-        case "JPXDecode":
-          const jpxImage = new _jpx.JpxImage();
-          jpxImage.parseImageProperties(image.stream);
-          image.stream.reset();
-          image.width = jpxImage.width;
-          image.height = jpxImage.height;
-          image.bitsPerComponent = jpxImage.bitsPerComponent;
-          image.numComps = jpxImage.componentsCount;
-          break;
-
-        case "JBIG2Decode":
-          image.bitsPerComponent = 1;
-          image.numComps = 1;
-          break;
+      filterName = filter.name;
+    } else if (Array.isArray(filter)) {
+      const filterZero = xref.fetchIfRef(filter[0]);
+
+      if (filterZero instanceof _primitives.Name) {
+        filterName = filterZero.name;
       }
     }
 
+    switch (filterName) {
+      case "JPXDecode":
+        const jpxImage = new _jpx.JpxImage();
+        jpxImage.parseImageProperties(image.stream);
+        image.stream.reset();
+        image.width = jpxImage.width;
+        image.height = jpxImage.height;
+        image.bitsPerComponent = jpxImage.bitsPerComponent;
+        image.numComps = jpxImage.componentsCount;
+        break;
+
+      case "JBIG2Decode":
+        image.bitsPerComponent = 1;
+        image.numComps = 1;
+        break;
+    }
+
     let width = dict.get("W", "Width");
     let height = dict.get("H", "Height");
 

+ 11 - 66
lib/core/jbig2.js

@@ -1370,7 +1370,7 @@ function processSegment(segment, visitor) {
       break;
 
     default:
-      throw new Jbig2Error(`segment type ${header.typeName}(${header.type})` + " is not implemented");
+      throw new Jbig2Error(`segment type ${header.typeName}(${header.type}) is not implemented`);
   }
 
   const callbackName = "on" + header.typeName;
@@ -1399,55 +1399,7 @@ function parseJbig2Chunks(chunks) {
 }
 
 function parseJbig2(data) {
-  const end = data.length;
-  let position = 0;
-
-  if (data[position] !== 0x97 || data[position + 1] !== 0x4a || data[position + 2] !== 0x42 || data[position + 3] !== 0x32 || data[position + 4] !== 0x0d || data[position + 5] !== 0x0a || data[position + 6] !== 0x1a || data[position + 7] !== 0x0a) {
-    throw new Jbig2Error("parseJbig2 - invalid header.");
-  }
-
-  const header = Object.create(null);
-  position += 8;
-  const flags = data[position++];
-  header.randomAccess = !(flags & 1);
-
-  if (!(flags & 2)) {
-    header.numberOfPages = (0, _core_utils.readUint32)(data, position);
-    position += 4;
-  }
-
-  const segments = readSegments(header, data, position, end);
-  const visitor = new SimpleSegmentVisitor();
-  processSegments(segments, visitor);
-  const {
-    width,
-    height
-  } = visitor.currentPageInfo;
-  const bitPacked = visitor.buffer;
-  const imgData = new Uint8ClampedArray(width * height);
-  let q = 0,
-      k = 0;
-
-  for (let i = 0; i < height; i++) {
-    let mask = 0,
-        buffer;
-
-    for (let j = 0; j < width; j++) {
-      if (!mask) {
-        mask = 128;
-        buffer = bitPacked[k++];
-      }
-
-      imgData[q++] = buffer & mask ? 0 : 255;
-      mask >>= 1;
-    }
-  }
-
-  return {
-    imgData,
-    width,
-    height
-  };
+  throw new Error("Not implemented: parseJbig2");
 }
 
 class SimpleSegmentVisitor {
@@ -1551,13 +1503,13 @@ class SimpleSegmentVisitor {
       this.symbols = symbols = {};
     }
 
-    let inputSymbols = [];
+    const inputSymbols = [];
 
-    for (let i = 0, ii = referredSegments.length; i < ii; i++) {
-      const referredSymbols = symbols[referredSegments[i]];
+    for (const referredSegment of referredSegments) {
+      const referredSymbols = symbols[referredSegment];
 
       if (referredSymbols) {
-        inputSymbols = inputSymbols.concat(referredSymbols);
+        inputSymbols.push(...referredSymbols);
       }
     }
 
@@ -1569,13 +1521,13 @@ class SimpleSegmentVisitor {
     const regionInfo = region.info;
     let huffmanTables, huffmanInput;
     const symbols = this.symbols;
-    let inputSymbols = [];
+    const inputSymbols = [];
 
-    for (let i = 0, ii = referredSegments.length; i < ii; i++) {
-      const referredSymbols = symbols[referredSegments[i]];
+    for (const referredSegment of referredSegments) {
+      const referredSymbols = symbols[referredSegment];
 
       if (referredSymbols) {
-        inputSymbols = inputSymbols.concat(referredSymbols);
+        inputSymbols.push(...referredSymbols);
       }
     }
 
@@ -2198,14 +2150,7 @@ class Jbig2Image {
   }
 
   parse(data) {
-    const {
-      imgData,
-      width,
-      height
-    } = parseJbig2(data);
-    this.width = width;
-    this.height = height;
-    return imgData;
+    throw new Error("Not implemented: Jbig2Image.parse");
   }
 
 }

+ 3 - 2
lib/core/operator_list.js

@@ -36,7 +36,7 @@ function addState(parentState, pattern, checkFn, iterateFn, processFn) {
     state = state[item] || (state[item] = []);
   }
 
-  state[pattern[pattern.length - 1]] = {
+  state[pattern.at(-1)] = {
     checkFn,
     iterateFn,
     processFn
@@ -640,7 +640,7 @@ class OperatorList {
     return transfers;
   }
 
-  flush(lastChunk = false) {
+  flush(lastChunk = false, separateAnnots = null) {
     this.optimizer.flush();
     const length = this.length;
     this._totalLength += length;
@@ -649,6 +649,7 @@ class OperatorList {
       fnArray: this.fnArray,
       argsArray: this.argsArray,
       lastChunk,
+      separateAnnots,
       length
     }, 1, this._transfers);
 

+ 3 - 3
lib/core/pattern.js

@@ -192,7 +192,7 @@ class RadialAxialShading extends BaseShading {
     }
 
     if (!extendEnd) {
-      colorStops[colorStops.length - 1][0] -= BaseShading.SMALL_NUMBER;
+      colorStops.at(-1)[0] -= BaseShading.SMALL_NUMBER;
       colorStops.push([1, background]);
     }
 
@@ -477,12 +477,12 @@ class MeshShading extends BaseShading {
             break;
 
           case 1:
-            ps.push(ps[ps.length - 2], ps[ps.length - 1]);
+            ps.push(ps.at(-2), ps.at(-1));
             verticesLeft = 1;
             break;
 
           case 2:
-            ps.push(ps[ps.length - 3], ps[ps.length - 1]);
+            ps.push(ps.at(-3), ps.at(-1));
             verticesLeft = 1;
             break;
         }

+ 3 - 6
lib/core/primitives.js

@@ -47,8 +47,7 @@ const Name = function NameClosure() {
     }
 
     static get(name) {
-      const nameValue = nameCache[name];
-      return nameValue ? nameValue : nameCache[name] = new Name(name);
+      return nameCache[name] || (nameCache[name] = new Name(name));
     }
 
     static _clearCache() {
@@ -71,8 +70,7 @@ const Cmd = function CmdClosure() {
     }
 
     static get(cmd) {
-      const cmdValue = cmdCache[cmd];
-      return cmdValue ? cmdValue : cmdCache[cmd] = new Cmd(cmd);
+      return cmdCache[cmd] || (cmdCache[cmd] = new Cmd(cmd));
     }
 
     static _clearCache() {
@@ -282,8 +280,7 @@ const Ref = function RefClosure() {
 
     static get(num, gen) {
       const key = gen === 0 ? `${num}R` : `${num}R${gen}`;
-      const refValue = refCache[key];
-      return refValue ? refValue : refCache[key] = new Ref(num, gen);
+      return refCache[key] || (refCache[key] = new Ref(num, gen));
     }
 
     static _clearCache() {

+ 32 - 1
lib/core/standard_fonts.js

@@ -63,10 +63,14 @@ const getStdFontMap = (0, _core_utils.getLookupTableFactory)(function (t) {
   t["Arial-Bold"] = "Helvetica-Bold";
   t["Arial-BoldItalic"] = "Helvetica-BoldOblique";
   t["Arial-Italic"] = "Helvetica-Oblique";
+  t.ArialMT = "Helvetica";
   t["Arial-BoldItalicMT"] = "Helvetica-BoldOblique";
   t["Arial-BoldMT"] = "Helvetica-Bold";
   t["Arial-ItalicMT"] = "Helvetica-Oblique";
-  t.ArialMT = "Helvetica";
+  t.ArialUnicodeMS = "Helvetica";
+  t["ArialUnicodeMS-Bold"] = "Helvetica-Bold";
+  t["ArialUnicodeMS-BoldItalic"] = "Helvetica-BoldOblique";
+  t["ArialUnicodeMS-Italic"] = "Helvetica-Oblique";
   t["Courier-BoldItalic"] = "Courier-BoldOblique";
   t["Courier-Italic"] = "Courier-Oblique";
   t.CourierNew = "Courier";
@@ -525,6 +529,33 @@ const getGlyphMapForStandardFonts = (0, _core_utils.getLookupTableFactory)(funct
   t[337] = 9552;
   t[493] = 1039;
   t[494] = 1040;
+  t[672] = 1488;
+  t[673] = 1489;
+  t[674] = 1490;
+  t[675] = 1491;
+  t[676] = 1492;
+  t[677] = 1493;
+  t[678] = 1494;
+  t[679] = 1495;
+  t[680] = 1496;
+  t[681] = 1497;
+  t[682] = 1498;
+  t[683] = 1499;
+  t[684] = 1500;
+  t[685] = 1501;
+  t[686] = 1502;
+  t[687] = 1503;
+  t[688] = 1504;
+  t[689] = 1505;
+  t[690] = 1506;
+  t[691] = 1507;
+  t[692] = 1508;
+  t[693] = 1509;
+  t[694] = 1510;
+  t[695] = 1511;
+  t[696] = 1512;
+  t[697] = 1513;
+  t[698] = 1514;
   t[705] = 1524;
   t[706] = 8362;
   t[710] = 64288;

+ 4 - 3
lib/core/type1_font.js

@@ -182,10 +182,11 @@ class Type1Font {
 
   getCharset() {
     const charset = [".notdef"];
-    const charstrings = this.charstrings;
 
-    for (let glyphId = 0; glyphId < charstrings.length; glyphId++) {
-      charset.push(charstrings[glyphId].glyphName);
+    for (const {
+      glyphName
+    } of this.charstrings) {
+      charset.push(glyphName);
     }
 
     return charset;

+ 8 - 7
lib/core/type1_parser.js

@@ -217,7 +217,7 @@ const Type1CharString = function Type1CharStringClosure() {
 
             case (12 << 8) + 6:
               if (seacAnalysisEnabled) {
-                const asb = this.stack[this.stack.length - 5];
+                const asb = this.stack.at(-5);
                 this.seac = this.stack.splice(-4, 4);
                 this.seac[0] += this.lsb - asb;
                 error = this.executeCommand(0, COMMAND_MAP.endchar);
@@ -528,7 +528,7 @@ const Type1Parser = function Type1ParserClosure() {
           privateData
         }
       };
-      let token, length, data, lenIV, encoded;
+      let token, length, data, lenIV;
 
       while ((token = this.getToken()) !== null) {
         if (token !== "/") {
@@ -560,7 +560,7 @@ const Type1Parser = function Type1ParserClosure() {
               this.getToken();
               data = length > 0 ? stream.getBytes(length) : new Uint8Array(0);
               lenIV = program.properties.privateData.lenIV;
-              encoded = this.readCharStrings(data, lenIV);
+              const encoded = this.readCharStrings(data, lenIV);
               this.nextChar();
               token = this.getToken();
 
@@ -588,7 +588,7 @@ const Type1Parser = function Type1ParserClosure() {
               this.getToken();
               data = length > 0 ? stream.getBytes(length) : new Uint8Array(0);
               lenIV = program.properties.privateData.lenIV;
-              encoded = this.readCharStrings(data, lenIV);
+              const encoded = this.readCharStrings(data, lenIV);
               this.nextChar();
               token = this.getToken();
 
@@ -638,9 +638,10 @@ const Type1Parser = function Type1ParserClosure() {
         }
       }
 
-      for (let i = 0; i < charstrings.length; i++) {
-        const glyph = charstrings[i].glyph;
-        encoded = charstrings[i].encoded;
+      for (const {
+        encoded,
+        glyph
+      } of charstrings) {
         const charString = new Type1CharString();
         const error = charString.convert(encoded, subrs, this.seacAnalysisEnabled);
         let output = charString.output;

+ 18 - 8
lib/core/worker.js

@@ -30,6 +30,8 @@ var _util = require("../shared/util.js");
 
 var _primitives = require("./primitives.js");
 
+var _core_utils = require("./core_utils.js");
+
 var _pdf_manager = require("./pdf_manager.js");
 
 var _cleanup_helper = require("./cleanup_helper.js");
@@ -42,8 +44,6 @@ var _message_handler = require("../shared/message_handler.js");
 
 var _worker_stream = require("./worker_stream.js");
 
-var _core_utils = require("./core_utils.js");
-
 class WorkerTask {
   constructor(name) {
     this.name = name;
@@ -99,7 +99,7 @@ class WorkerMessageHandler {
     const WorkerTasks = [];
     const verbosity = (0, _util.getVerbosityLevel)();
     const apiVersion = docParams.apiVersion;
-    const workerVersion = '2.14.305';
+    const workerVersion = '2.15.349';
 
     if (apiVersion !== workerVersion) {
       throw new Error(`The API version "${apiVersion}" does not match ` + `the Worker version "${workerVersion}".`);
@@ -210,8 +210,8 @@ class WorkerMessageHandler {
           rangeChunkSize: source.rangeChunkSize
         }, evaluatorOptions, enableXfa, docBaseUrl);
 
-        for (let i = 0; i < cachedChunks.length; i++) {
-          newPdfManager.sendProgressiveData(cachedChunks[i]);
+        for (const chunk of cachedChunks) {
+          newPdfManager.sendProgressiveData(chunk);
         }
 
         cachedChunks = [];
@@ -466,8 +466,20 @@ class WorkerMessageHandler {
       filename
     }) {
       pdfManager.requestLoadedStream();
+      const newAnnotationsByPage = !isPureXfa ? (0, _core_utils.getNewAnnotationsMap)(annotationStorage) : null;
       const promises = [pdfManager.onLoadedStream(), pdfManager.ensureCatalog("acroForm"), pdfManager.ensureCatalog("acroFormRef"), pdfManager.ensureDoc("xref"), pdfManager.ensureDoc("startXRef")];
 
+      if (newAnnotationsByPage) {
+        for (const [pageIndex, annotations] of newAnnotationsByPage) {
+          promises.push(pdfManager.getPage(pageIndex).then(page => {
+            const task = new WorkerTask(`Save (editor): page ${pageIndex}`);
+            return page.saveNewAnnotations(handler, task, annotations).finally(function () {
+              finishWorkerTask(task);
+            });
+          }));
+        }
+      }
+
       if (isPureXfa) {
         promises.push(pdfManager.serializeXfaData(annotationStorage));
       } else {
@@ -492,9 +504,7 @@ class WorkerMessageHandler {
             return stream.bytes;
           }
         } else {
-          for (const ref of refs) {
-            newRefs = ref.filter(x => x !== null).reduce((a, b) => a.concat(b), newRefs);
-          }
+          newRefs = refs.flat(2);
 
           if (newRefs.length === 0) {
             return stream.bytes;

+ 15 - 20
lib/core/writer.js

@@ -26,6 +26,7 @@ Object.defineProperty(exports, "__esModule", {
 });
 exports.incrementalUpdate = incrementalUpdate;
 exports.writeDict = writeDict;
+exports.writeObject = writeObject;
 
 var _util = require("../shared/util.js");
 
@@ -39,6 +40,18 @@ var _base_stream = require("./base_stream.js");
 
 var _crypto = require("./crypto.js");
 
+function writeObject(ref, obj, buffer, transform) {
+  buffer.push(`${ref.num} ${ref.gen} obj\n`);
+
+  if (obj instanceof _primitives.Dict) {
+    writeDict(obj, buffer, transform);
+  } else if (obj instanceof _base_stream.BaseStream) {
+    writeStream(obj, buffer, transform);
+  }
+
+  buffer.push("\nendobj\n");
+}
+
 function writeDict(dict, buffer, transform) {
   buffer.push("<<");
 
@@ -79,24 +92,6 @@ function writeArray(array, 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 (value instanceof _primitives.Name) {
     buffer.push(`/${(0, _core_utils.escapePDFName)(value.name)}`);
@@ -111,7 +106,7 @@ function writeValue(value, buffer, transform) {
 
     buffer.push(`(${(0, _util.escapeString)(value)})`);
   } else if (typeof value === "number") {
-    buffer.push(numberToString(value));
+    buffer.push((0, _core_utils.numberToString)(value));
   } else if (typeof value === "boolean") {
     buffer.push(value.toString());
   } else if (value instanceof _primitives.Dict) {
@@ -290,7 +285,7 @@ function incrementalUpdate({
   const newXref = new _primitives.Dict(null);
   const refForXrefTable = xrefInfo.newRef;
   let buffer, baseOffset;
-  const lastByte = originalData[originalData.length - 1];
+  const lastByte = originalData.at(-1);
 
   if (lastByte === 0x0a || lastByte === 0x0d) {
     buffer = [];

+ 1 - 1
lib/core/xfa/builder.js

@@ -213,7 +213,7 @@ class Builder {
     const prefixStack = this._namespacePrefixes.get(prefix);
 
     if (prefixStack && prefixStack.length > 0) {
-      return prefixStack[prefixStack.length - 1];
+      return prefixStack.at(-1);
     }
 
     (0, _util.warn)(`Unknown namespace prefix: ${prefix}.`);

+ 1 - 1
lib/core/xfa/data.js

@@ -38,7 +38,7 @@ class DataHandler {
     const stack = [[-1, this.data[_xfa_object.$getChildren]()]];
 
     while (stack.length > 0) {
-      const last = stack[stack.length - 1];
+      const last = stack.at(-1);
       const [i, children] = last;
 
       if (i + 1 === children.length) {

+ 2 - 2
lib/core/xfa/formcalc_parser.js

@@ -323,7 +323,7 @@ class SimpleExprParser {
 
         case _formcalc_lexer.TOKEN.leftParen:
           if (this.last === OPERAND) {
-            const lastOperand = this.operands[this.operands.length - 1];
+            const lastOperand = this.operands.at(-1);
 
             if (!(lastOperand instanceof AstIdentifier)) {
               return [tok, this.getNode()];
@@ -536,7 +536,7 @@ class SimpleExprParser {
 
   flushWithOperator(op) {
     while (true) {
-      const top = this.operators[this.operators.length - 1];
+      const top = this.operators.at(-1);
 
       if (top) {
         if (top.id >= 0 && SimpleExprParser.checkPrecedence(top, op)) {

+ 2 - 2
lib/core/xfa/html_utils.js

@@ -293,7 +293,7 @@ function layoutNode(node, availableSpace) {
       }
     }
 
-    const maxWidth = (!node.w ? availableSpace.width : node.w) - marginH;
+    const maxWidth = (node.w || availableSpace.width) - marginH;
     const fontFinder = node[_xfa_object.$globalData].fontFinder;
 
     if (node.value.exData && node.value.exData[_xfa_object.$content] && node.value.exData.contentType === "text/html") {
@@ -607,7 +607,7 @@ function isPrintOnly(node) {
 function getCurrentPara(node) {
   const stack = node[_xfa_object.$getTemplateRoot]()[_xfa_object.$extra].paraStack;
 
-  return stack.length ? stack[stack.length - 1] : null;
+  return stack.length ? stack.at(-1) : null;
 }
 
 function setPara(node, nodeStyle, value) {

+ 2 - 2
lib/core/xfa/som.js

@@ -86,7 +86,7 @@ function parseExpression(expr, dotDotAllowed, noExpr = true) {
         return null;
       }
 
-      parsed[parsed.length - 1].index = parseIndex(match[0]);
+      parsed.at(-1).index = parseIndex(match[0]);
       pos += match[0].length + 1;
       continue;
     }
@@ -251,7 +251,7 @@ function searchNode(root, container, expr, dotDotAllowed = true, useCache = true
     if (isFinite(index)) {
       root = nodes.filter(node => index < node.length).map(node => node[index]);
     } else {
-      root = nodes.reduce((acc, node) => acc.concat(node), []);
+      root = nodes.flat();
     }
   }
 

+ 50 - 13
lib/core/xfa/template.js

@@ -103,6 +103,10 @@ function* getContainedChildren(node) {
   }
 }
 
+function isRequired(node) {
+  return node.validate && node.validate.nullTest === "error";
+}
+
 function setTabIndex(node) {
   while (node) {
     if (!node.traversal) {
@@ -370,7 +374,7 @@ class Arc extends _xfa_object.XFAObject {
   }
 
   [_xfa_object.$toHTML]() {
-    const edge = this.edge ? this.edge : new Edge({});
+    const edge = this.edge || new Edge({});
 
     const edgeStyle = edge[_xfa_object.$toStyle]();
 
@@ -703,7 +707,7 @@ class Border extends _xfa_object.XFAObject {
       const edges = this.edge.children.slice();
 
       if (edges.length < 4) {
-        const defaultEdge = edges[edges.length - 1] || new Edge({});
+        const defaultEdge = edges.at(-1) || new Edge({});
 
         for (let i = edges.length; i < 4; i++) {
           edges.push(defaultEdge);
@@ -755,7 +759,7 @@ class Border extends _xfa_object.XFAObject {
       const cornerStyles = this.corner.children.map(node => node[_xfa_object.$toStyle]());
 
       if (cornerStyles.length === 2 || cornerStyles.length === 3) {
-        const last = cornerStyles[cornerStyles.length - 1];
+        const last = cornerStyles.at(-1);
 
         for (let i = cornerStyles.length; i < 4; i++) {
           cornerStyles.push(last);
@@ -1154,7 +1158,8 @@ class CheckButton extends _xfa_object.XFAObject {
         checked,
         xfaOn: exportedValue.on,
         xfaOff: exportedValue.off,
-        "aria-label": ariaLabel(field)
+        "aria-label": ariaLabel(field),
+        "aria-required": false
       }
     };
 
@@ -1162,6 +1167,11 @@ class CheckButton extends _xfa_object.XFAObject {
       input.attributes.name = groupId;
     }
 
+    if (isRequired(field)) {
+      input.attributes["aria-required"] = true;
+      input.attributes.required = true;
+    }
+
     return _utils.HTMLResult.success({
       name: "label",
       attributes: {
@@ -1200,7 +1210,7 @@ class ChoiceList extends _xfa_object.XFAObject {
 
     const fontSize = field.font && field.font.size || 10;
     const optionStyle = {
-      fontSize: `calc(${fontSize}px * var(--zoom-factor))`
+      fontSize: `calc(${fontSize}px * var(--scale-factor))`
     };
     const children = [];
 
@@ -1255,9 +1265,15 @@ class ChoiceList extends _xfa_object.XFAObject {
       fieldId: field[_xfa_object.$uid],
       dataId: field[_xfa_object.$data] && field[_xfa_object.$data][_xfa_object.$uid] || field[_xfa_object.$uid],
       style,
-      "aria-label": ariaLabel(field)
+      "aria-label": ariaLabel(field),
+      "aria-required": false
     };
 
+    if (isRequired(field)) {
+      selectAttributes["aria-required"] = true;
+      selectAttributes.required = true;
+    }
+
     if (this.open === "multiSelect") {
       selectAttributes.multiple = true;
     }
@@ -1468,9 +1484,16 @@ class DateTimeEdit extends _xfa_object.XFAObject {
         dataId: field[_xfa_object.$data] && field[_xfa_object.$data][_xfa_object.$uid] || field[_xfa_object.$uid],
         class: ["xfaTextfield"],
         style,
-        "aria-label": ariaLabel(field)
+        "aria-label": ariaLabel(field),
+        "aria-required": false
       }
     };
+
+    if (isRequired(field)) {
+      html.attributes["aria-required"] = true;
+      html.attributes.required = true;
+    }
+
     return _utils.HTMLResult.success({
       name: "label",
       attributes: {
@@ -3178,7 +3201,7 @@ class Line extends _xfa_object.XFAObject {
   [_xfa_object.$toHTML]() {
     const parent = this[_xfa_object.$getParent]()[_xfa_object.$getParent]();
 
-    const edge = this.edge ? this.edge : new Edge({});
+    const edge = this.edge || new Edge({});
 
     const edgeStyle = edge[_xfa_object.$toStyle]();
 
@@ -3388,9 +3411,16 @@ class NumericEdit extends _xfa_object.XFAObject {
         dataId: field[_xfa_object.$data] && field[_xfa_object.$data][_xfa_object.$uid] || field[_xfa_object.$uid],
         class: ["xfaTextfield"],
         style,
-        "aria-label": ariaLabel(field)
+        "aria-label": ariaLabel(field),
+        "aria-required": false
       }
     };
+
+    if (isRequired(field)) {
+      html.attributes["aria-required"] = true;
+      html.attributes.required = true;
+    }
+
     return _utils.HTMLResult.success({
       name: "label",
       attributes: {
@@ -4972,7 +5002,7 @@ class Template extends _xfa_object.XFAObject {
           if (target instanceof PageArea) {
             targetPageArea = target;
           } else if (target instanceof ContentArea) {
-            const index = contentAreas.findIndex(e => e === target);
+            const index = contentAreas.indexOf(target);
 
             if (index !== -1) {
               if (index > currentIndex) {
@@ -4982,7 +5012,7 @@ class Template extends _xfa_object.XFAObject {
               }
             } else {
               targetPageArea = target[_xfa_object.$getParent]();
-              startIndex = targetPageArea.contentArea.children.findIndex(e => e === target);
+              startIndex = targetPageArea.contentArea.children.indexOf(target);
             }
           }
 
@@ -5156,7 +5186,8 @@ class TextEdit extends _xfa_object.XFAObject {
           fieldId: field[_xfa_object.$uid],
           class: ["xfaTextfield"],
           style,
-          "aria-label": ariaLabel(field)
+          "aria-label": ariaLabel(field),
+          "aria-required": false
         }
       };
     } else {
@@ -5168,11 +5199,17 @@ class TextEdit extends _xfa_object.XFAObject {
           fieldId: field[_xfa_object.$uid],
           class: ["xfaTextfield"],
           style,
-          "aria-label": ariaLabel(field)
+          "aria-label": ariaLabel(field),
+          "aria-required": false
         }
       };
     }
 
+    if (isRequired(field)) {
+      html.attributes["aria-required"] = true;
+      html.attributes.required = true;
+    }
+
     return _utils.HTMLResult.success({
       name: "label",
       attributes: {

+ 2 - 2
lib/core/xfa/text.js

@@ -101,7 +101,7 @@ class FontSelector {
   }
 
   pushData(xfaFont, margin, lineHeight) {
-    const lastFont = this.stack[this.stack.length - 1];
+    const lastFont = this.stack.at(-1);
 
     for (const name of ["typeface", "posture", "weight", "size", "letterSpacing"]) {
       if (!xfaFont[name]) {
@@ -129,7 +129,7 @@ class FontSelector {
   }
 
   topFont() {
-    return this.stack[this.stack.length - 1];
+    return this.stack.at(-1);
   }
 
 }

+ 5 - 1
lib/core/xfa/xhtml.js

@@ -99,6 +99,10 @@ function mapStyle(styleStr, node, richText) {
     style.verticalAlign = (0, _html_utils.measureToString)(Math.sign((0, _utils.getMeasurement)(style.verticalAlign)) * fontSize * VERTICAL_FACTOR);
   }
 
+  if (richText && style.fontSize) {
+    style.fontSize = `calc(${style.fontSize} * var(--scale-factor))`;
+  }
+
   (0, _html_utils.fixTextIndent)(style);
   return style;
 }
@@ -448,7 +452,7 @@ class P extends XhtmlObject {
   [_xfa_object.$text]() {
     const siblings = this[_xfa_object.$getParent]()[_xfa_object.$getChildren]();
 
-    if (siblings[siblings.length - 1] === this) {
+    if (siblings.at(-1) === this) {
       return super[_xfa_object.$text]();
     }
 

+ 1 - 1
lib/core/xfa_fonts.js

@@ -236,7 +236,7 @@ function getXfaFontDict(name) {
   dict.set("CIDToGIDMap", _primitives.Name.get("Identity"));
   dict.set("W", widths);
   dict.set("FirstChar", widths[0]);
-  dict.set("LastChar", widths[widths.length - 2] + widths[widths.length - 1].length - 1);
+  dict.set("LastChar", widths.at(-2) + widths.at(-1).length - 1);
   const descriptor = new _primitives.Dict(null);
   dict.set("FontDescriptor", descriptor);
   const systemInfo = new _primitives.Dict(null);

+ 2 - 1
lib/core/xml_parser.js

@@ -547,7 +547,8 @@ class SimpleXMLParser extends XMLParserBase {
 
   onEndElement(name) {
     this._currentFragment = this._stack.pop() || [];
-    const lastElement = this._currentFragment[this._currentFragment.length - 1];
+
+    const lastElement = this._currentFragment.at(-1);
 
     if (!lastElement) {
       return null;

+ 1 - 1
lib/core/xref.js

@@ -52,7 +52,7 @@ class XRef {
 
   getNewRef() {
     if (this._newRefNum === null) {
-      this._newRefNum = this.entries.length;
+      this._newRefNum = this.entries.length || 1;
     }
 
     return _primitives.Ref.get(this._newRefNum++, 0);

+ 257 - 189
lib/display/annotation_layer.js

@@ -37,6 +37,7 @@ var _scripting_utils = require("../shared/scripting_utils.js");
 var _xfa_layer = require("./xfa_layer.js");
 
 const DEFAULT_TAB_INDEX = 1000;
+const DEFAULT_FONT_SIZE = 9;
 const GetElementsByNameSet = new WeakSet();
 
 function getRectDims(rect) {
@@ -167,48 +168,24 @@ class AnnotationElement {
           page = this.page,
           viewport = this.viewport;
     const container = document.createElement("section");
-    let {
+    const {
       width,
       height
     } = getRectDims(data.rect);
+    const [pageLLx, pageLLy, pageURx, pageURy] = viewport.viewBox;
+    const pageWidth = pageURx - pageLLx;
+    const pageHeight = pageURy - pageLLy;
     container.setAttribute("data-annotation-id", data.id);
 
     const rect = _util.Util.normalizeRect([data.rect[0], page.view[3] - data.rect[1] + page.view[1], data.rect[2], page.view[3] - data.rect[3] + page.view[1]]);
 
-    if (data.hasOwnCanvas) {
-      const transform = viewport.transform.slice();
-
-      const [scaleX, scaleY] = _util.Util.singularValueDecompose2dScale(transform);
-
-      width = Math.ceil(width * scaleX);
-      height = Math.ceil(height * scaleY);
-      rect[0] *= scaleX;
-      rect[1] *= scaleY;
-
-      for (let i = 0; i < 4; i++) {
-        transform[i] = Math.sign(transform[i]);
-      }
-
-      container.style.transform = `matrix(${transform.join(",")})`;
-    } else {
-      container.style.transform = `matrix(${viewport.transform.join(",")})`;
-    }
-
-    container.style.transformOrigin = `${-rect[0]}px ${-rect[1]}px`;
-
     if (!ignoreBorder && data.borderStyle.width > 0) {
       container.style.borderWidth = `${data.borderStyle.width}px`;
-
-      if (data.borderStyle.style !== _util.AnnotationBorderStyleType.UNDERLINE) {
-        width -= 2 * data.borderStyle.width;
-        height -= 2 * data.borderStyle.width;
-      }
-
       const horizontalRadius = data.borderStyle.horizontalCornerRadius;
       const verticalRadius = data.borderStyle.verticalCornerRadius;
 
       if (horizontalRadius > 0 || verticalRadius > 0) {
-        const radius = `${horizontalRadius}px / ${verticalRadius}px`;
+        const radius = `calc(${horizontalRadius}px * var(--scale-factor)) / calc(${verticalRadius}px * var(--scale-factor))`;
         container.style.borderRadius = radius;
       }
 
@@ -237,28 +214,54 @@ class AnnotationElement {
           break;
       }
 
-      const borderColor = data.borderColor || data.color || null;
+      const borderColor = data.borderColor || null;
 
       if (borderColor) {
-        container.style.borderColor = _util.Util.makeHexColor(data.color[0] | 0, data.color[1] | 0, data.color[2] | 0);
+        container.style.borderColor = _util.Util.makeHexColor(borderColor[0] | 0, borderColor[1] | 0, borderColor[2] | 0);
       } else {
         container.style.borderWidth = 0;
       }
     }
 
-    container.style.left = `${rect[0]}px`;
-    container.style.top = `${rect[1]}px`;
+    container.style.left = `${100 * (rect[0] - pageLLx) / pageWidth}%`;
+    container.style.top = `${100 * (rect[1] - pageLLy) / pageHeight}%`;
+    const {
+      rotation
+    } = data;
 
-    if (data.hasOwnCanvas) {
-      container.style.width = container.style.height = "auto";
+    if (data.hasOwnCanvas || rotation === 0) {
+      container.style.width = `${100 * width / pageWidth}%`;
+      container.style.height = `${100 * height / pageHeight}%`;
     } else {
-      container.style.width = `${width}px`;
-      container.style.height = `${height}px`;
+      this.setRotation(rotation, container);
     }
 
     return container;
   }
 
+  setRotation(angle, container = this.container) {
+    const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
+    const pageWidth = pageURx - pageLLx;
+    const pageHeight = pageURy - pageLLy;
+    const {
+      width,
+      height
+    } = getRectDims(this.data.rect);
+    let elementWidth, elementHeight;
+
+    if (angle % 180 === 0) {
+      elementWidth = 100 * width / pageWidth;
+      elementHeight = 100 * height / pageHeight;
+    } else {
+      elementWidth = 100 * height / pageWidth;
+      elementHeight = 100 * width / pageHeight;
+    }
+
+    container.style.width = `${elementWidth}%`;
+    container.style.height = `${elementHeight}%`;
+    container.setAttribute("data-main-rotation", (360 - angle) % 360);
+  }
+
   get _commonActions() {
     const setColor = (jsName, styleName, event) => {
       const color = event.detail[jsName];
@@ -268,7 +271,7 @@ class AnnotationElement {
     return (0, _util.shadow)(this, "_commonActions", {
       display: event => {
         const hidden = event.detail.display % 2 === 1;
-        event.target.style.visibility = hidden ? "hidden" : "visible";
+        this.container.style.visibility = hidden ? "hidden" : "visible";
         this.annotationStorage.setValue(this.data.id, {
           hidden,
           print: event.detail.display === 0 || event.detail.display === 3
@@ -280,7 +283,7 @@ class AnnotationElement {
         });
       },
       hidden: event => {
-        event.target.style.visibility = event.detail.hidden ? "hidden" : "visible";
+        this.container.style.visibility = event.detail.hidden ? "hidden" : "visible";
         this.annotationStorage.setValue(this.data.id, {
           hidden: event.detail.hidden
         });
@@ -301,11 +304,7 @@ class AnnotationElement {
         }
       },
       required: event => {
-        if (event.detail.required) {
-          event.target.setAttribute("required", "");
-        } else {
-          event.target.removeAttribute("required");
-        }
+        this._setRequired(event.target, event.detail.required);
       },
       bgColor: event => {
         setColor("bgColor", "backgroundColor", event);
@@ -324,6 +323,13 @@ class AnnotationElement {
       },
       strokeColor: event => {
         setColor("strokeColor", "borderColor", event);
+      },
+      rotation: event => {
+        const angle = event.detail.rotation;
+        this.setRotation(angle);
+        this.annotationStorage.setValue(this.data.id, {
+          rotation: angle
+        });
       }
     });
   }
@@ -393,9 +399,8 @@ class AnnotationElement {
 
     if (!trigger) {
       trigger = document.createElement("div");
-      trigger.style.height = container.style.height;
-      trigger.style.width = container.style.width;
-      container.appendChild(trigger);
+      trigger.className = "popupTriggerArea";
+      container.append(trigger);
     }
 
     const popupElement = new PopupElement({
@@ -409,8 +414,8 @@ class AnnotationElement {
       hideWrapper: true
     });
     const popup = popupElement.render();
-    popup.style.left = container.style.width;
-    container.appendChild(popup);
+    popup.style.left = "100%";
+    container.append(popup);
   }
 
   _renderQuadrilaterals(className) {
@@ -446,7 +451,7 @@ class AnnotationElement {
           }
 
           const exportValue = typeof exportValues === "string" ? exportValues : null;
-          const domElement = document.getElementById(id);
+          const domElement = document.querySelector(`[data-element-id="${id}"]`);
 
           if (domElement && !GetElementsByNameSet.has(domElement)) {
             (0, _util.warn)(`_getElementsByName - element not allowed: ${id}`);
@@ -500,12 +505,12 @@ class AnnotationElement {
 
 class LinkAnnotationElement extends AnnotationElement {
   constructor(parameters, options = null) {
-    const isRenderable = !!(parameters.data.url || parameters.data.dest || parameters.data.action || parameters.data.isTooltipOnly || parameters.data.resetForm || parameters.data.actions && (parameters.data.actions.Action || parameters.data.actions["Mouse Up"] || parameters.data.actions["Mouse Down"]));
     super(parameters, {
-      isRenderable,
+      isRenderable: true,
       ignoreBorder: !!options?.ignoreBorder,
       createQuadrilaterals: true
     });
+    this.isTooltipOnly = parameters.data.isTooltipOnly;
   }
 
   render() {
@@ -514,39 +519,52 @@ class LinkAnnotationElement extends AnnotationElement {
       linkService
     } = this;
     const link = document.createElement("a");
+    link.setAttribute("data-element-id", data.id);
+    let isBound = false;
 
     if (data.url) {
       linkService.addLinkAttributes(link, data.url, data.newWindow);
+      isBound = true;
     } else if (data.action) {
       this._bindNamedAction(link, data.action);
+
+      isBound = true;
     } else if (data.dest) {
       this._bindLink(link, data.dest);
-    } else {
-      let hasClickAction = false;
 
+      isBound = true;
+    } else {
       if (data.actions && (data.actions.Action || data.actions["Mouse Up"] || data.actions["Mouse Down"]) && this.enableScripting && this.hasJSActions) {
-        hasClickAction = true;
-
         this._bindJSAction(link, data);
+
+        isBound = true;
       }
 
       if (data.resetForm) {
         this._bindResetFormAction(link, data.resetForm);
-      } else if (!hasClickAction) {
+
+        isBound = true;
+      } else if (this.isTooltipOnly && !isBound) {
         this._bindLink(link, "");
+
+        isBound = true;
       }
     }
 
     if (this.quadrilaterals) {
       return this._renderQuadrilaterals("linkAnnotation").map((quadrilateral, index) => {
         const linkElement = index === 0 ? link : link.cloneNode();
-        quadrilateral.appendChild(linkElement);
+        quadrilateral.append(linkElement);
         return quadrilateral;
       });
     }
 
     this.container.className = "linkAnnotation";
-    this.container.appendChild(link);
+
+    if (isBound) {
+      this.container.append(link);
+    }
+
     return this.container;
   }
 
@@ -707,9 +725,12 @@ class LinkAnnotationElement extends AnnotationElement {
             continue;
         }
 
-        const domElement = document.getElementById(id);
+        const domElement = document.querySelector(`[data-element-id="${id}"]`);
 
-        if (!domElement || !GetElementsByNameSet.has(domElement)) {
+        if (!domElement) {
+          continue;
+        } else if (!GetElementsByNameSet.has(domElement)) {
+          (0, _util.warn)(`_bindResetFormAction - element not allowed: ${id}`);
           continue;
         }
 
@@ -744,8 +765,6 @@ class TextAnnotationElement extends AnnotationElement {
   render() {
     this.container.className = "textAnnotation";
     const image = document.createElement("img");
-    image.style.height = this.container.style.height;
-    image.style.width = this.container.style.width;
     image.src = this.imageResourcesPath + "annotation-" + this.data.name.toLowerCase() + ".svg";
     image.alt = "[{{type}} Annotation]";
     image.dataset.l10nId = "text_annotation_type";
@@ -757,7 +776,7 @@ class TextAnnotationElement extends AnnotationElement {
       this._createPopup(image, this.data);
     }
 
-    this.container.appendChild(image);
+    this.container.append(image);
     return this.container;
   }
 
@@ -821,6 +840,43 @@ class WidgetAnnotationElement extends AnnotationElement {
     element.style.backgroundColor = color === null ? "transparent" : _util.Util.makeHexColor(color[0], color[1], color[2]);
   }
 
+  _setTextStyle(element) {
+    const TEXT_ALIGNMENT = ["left", "center", "right"];
+    const {
+      fontColor
+    } = this.data.defaultAppearanceData;
+    const fontSize = this.data.defaultAppearanceData.fontSize || DEFAULT_FONT_SIZE;
+    const style = element.style;
+    let computedFontSize;
+
+    if (this.data.multiLine) {
+      const height = Math.abs(this.data.rect[3] - this.data.rect[1]);
+      const numberOfLines = Math.round(height / (_util.LINE_FACTOR * fontSize)) || 1;
+      const lineHeight = height / numberOfLines;
+      computedFontSize = Math.min(fontSize, Math.round(lineHeight / _util.LINE_FACTOR));
+    } else {
+      const height = Math.abs(this.data.rect[3] - this.data.rect[1]);
+      computedFontSize = Math.min(fontSize, Math.round(height / _util.LINE_FACTOR));
+    }
+
+    style.fontSize = `calc(${computedFontSize}px * var(--scale-factor))`;
+    style.color = _util.Util.makeHexColor(fontColor[0], fontColor[1], fontColor[2]);
+
+    if (this.data.textAlignment !== null) {
+      style.textAlign = TEXT_ALIGNMENT[this.data.textAlignment];
+    }
+  }
+
+  _setRequired(element, isRequired) {
+    if (isRequired) {
+      element.setAttribute("required", true);
+    } else {
+      element.removeAttribute("required");
+    }
+
+    element.setAttribute("aria-required", isRequired);
+  }
+
 }
 
 class TextWidgetAnnotationElement extends WidgetAnnotationElement {
@@ -857,7 +913,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
       });
       const textContent = storedData.formattedValue || storedData.value || "";
       const elementData = {
-        userValue: null,
+        userValue: textContent,
         formattedValue: null,
         valueOnFocus: ""
       };
@@ -865,18 +921,28 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
       if (this.data.multiLine) {
         element = document.createElement("textarea");
         element.textContent = textContent;
+
+        if (this.data.doNotScroll) {
+          element.style.overflowY = "hidden";
+        }
       } else {
         element = document.createElement("input");
         element.type = "text";
         element.setAttribute("value", textContent);
+
+        if (this.data.doNotScroll) {
+          element.style.overflowX = "hidden";
+        }
       }
 
       GetElementsByNameSet.add(element);
+      element.setAttribute("data-element-id", id);
       element.disabled = this.data.readOnly;
       element.name = this.data.fieldName;
       element.tabIndex = DEFAULT_TAB_INDEX;
-      elementData.userValue = textContent;
-      element.setAttribute("id", id);
+
+      this._setRequired(element, this.data.required);
+
       element.addEventListener("input", event => {
         storage.setValue(id, {
           value: event.target.value
@@ -1088,7 +1154,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
         const fieldWidth = this.data.rect[2] - this.data.rect[0];
         const combWidth = fieldWidth / this.data.maxLen;
         element.classList.add("comb");
-        element.style.letterSpacing = `calc(${combWidth}px - 1ch)`;
+        element.style.letterSpacing = `calc(${combWidth}px * var(--scale-factor) - 1ch)`;
       }
     } else {
       element = document.createElement("div");
@@ -1103,29 +1169,10 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
 
     this._setDefaultPropertiesFromJS(element);
 
-    this.container.appendChild(element);
+    this.container.append(element);
     return this.container;
   }
 
-  _setTextStyle(element) {
-    const TEXT_ALIGNMENT = ["left", "center", "right"];
-    const {
-      fontSize,
-      fontColor
-    } = this.data.defaultAppearanceData;
-    const style = element.style;
-
-    if (fontSize) {
-      style.fontSize = `${fontSize}px`;
-    }
-
-    style.color = _util.Util.makeHexColor(fontColor[0], fontColor[1], fontColor[2]);
-
-    if (this.data.textAlignment !== null) {
-      style.textAlign = TEXT_ALIGNMENT[this.data.textAlignment];
-    }
-  }
-
 }
 
 class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
@@ -1153,7 +1200,11 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
     this.container.className = "buttonWidgetAnnotation checkBox";
     const element = document.createElement("input");
     GetElementsByNameSet.add(element);
+    element.setAttribute("data-element-id", id);
     element.disabled = data.readOnly;
+
+    this._setRequired(element, this.data.required);
+
     element.type = "checkbox";
     element.name = data.fieldName;
 
@@ -1161,7 +1212,6 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
       element.setAttribute("checked", true);
     }
 
-    element.setAttribute("id", id);
     element.setAttribute("exportValue", data.exportValue);
     element.tabIndex = DEFAULT_TAB_INDEX;
     element.addEventListener("change", event => {
@@ -1213,7 +1263,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
 
     this._setDefaultPropertiesFromJS(element);
 
-    this.container.appendChild(element);
+    this.container.append(element);
     return this.container;
   }
 
@@ -1244,7 +1294,11 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
 
     const element = document.createElement("input");
     GetElementsByNameSet.add(element);
+    element.setAttribute("data-element-id", id);
     element.disabled = data.readOnly;
+
+    this._setRequired(element, this.data.required);
+
     element.type = "radio";
     element.name = data.fieldName;
 
@@ -1252,7 +1306,6 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
       element.setAttribute("checked", true);
     }
 
-    element.setAttribute("id", id);
     element.tabIndex = DEFAULT_TAB_INDEX;
     element.addEventListener("change", event => {
       const {
@@ -1306,7 +1359,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
 
     this._setDefaultPropertiesFromJS(element);
 
-    this.container.appendChild(element);
+    this.container.append(element);
     return this.container;
   }
 
@@ -1327,7 +1380,15 @@ class PushButtonWidgetAnnotationElement extends LinkAnnotationElement {
       container.title = this.data.alternativeText;
     }
 
-    this._setDefaultPropertiesFromJS(container);
+    const linkElement = container.lastChild;
+
+    if (this.enableScripting && this.hasJSActions && linkElement) {
+      this._setDefaultPropertiesFromJS(linkElement);
+
+      linkElement.addEventListener("updatefromsandbox", jsEvent => {
+        this._dispatchEventFromSandbox({}, jsEvent);
+      });
+    }
 
     return container;
   }
@@ -1348,22 +1409,16 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
     const storedData = storage.getValue(id, {
       value: this.data.fieldValue
     });
-    let {
-      fontSize
-    } = this.data.defaultAppearanceData;
-
-    if (!fontSize) {
-      fontSize = 9;
-    }
-
-    const fontSizeStyle = `calc(${fontSize}px * var(--zoom-factor))`;
     const selectElement = document.createElement("select");
     GetElementsByNameSet.add(selectElement);
+    selectElement.setAttribute("data-element-id", id);
     selectElement.disabled = this.data.readOnly;
+
+    this._setRequired(selectElement, this.data.required);
+
     selectElement.name = this.data.fieldName;
-    selectElement.setAttribute("id", id);
     selectElement.tabIndex = DEFAULT_TAB_INDEX;
-    selectElement.style.fontSize = `${fontSize}px`;
+    let addAnEmptyEntry = this.data.combo && this.data.options.length > 0;
 
     if (!this.data.combo) {
       selectElement.size = this.data.options.length;
@@ -1386,15 +1441,30 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
       optionElement.textContent = option.displayValue;
       optionElement.value = option.exportValue;
 
-      if (this.data.combo) {
-        optionElement.style.fontSize = fontSizeStyle;
-      }
-
       if (storedData.value.includes(option.exportValue)) {
         optionElement.setAttribute("selected", true);
+        addAnEmptyEntry = false;
       }
 
-      selectElement.appendChild(optionElement);
+      selectElement.append(optionElement);
+    }
+
+    let removeEmptyEntry = null;
+
+    if (addAnEmptyEntry) {
+      const noneOptionElement = document.createElement("option");
+      noneOptionElement.value = " ";
+      noneOptionElement.setAttribute("hidden", true);
+      noneOptionElement.setAttribute("selected", true);
+      selectElement.prepend(noneOptionElement);
+
+      removeEmptyEntry = () => {
+        noneOptionElement.remove();
+        selectElement.removeEventListener("input", removeEmptyEntry);
+        removeEmptyEntry = null;
+      };
+
+      selectElement.addEventListener("input", removeEmptyEntry);
     }
 
     const getValue = (event, isExport) => {
@@ -1422,6 +1492,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
       selectElement.addEventListener("updatefromsandbox", jsEvent => {
         const actions = {
           value(event) {
+            removeEmptyEntry?.();
             const value = event.detail.value;
             const values = new Set(Array.isArray(value) ? value : [value]);
 
@@ -1475,10 +1546,17 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
               displayValue,
               exportValue
             } = event.detail.insert;
+            const selectChild = selectElement.children[index];
             const optionElement = document.createElement("option");
             optionElement.textContent = displayValue;
             optionElement.value = exportValue;
-            selectElement.insertBefore(optionElement, selectElement.children[index]);
+
+            if (selectChild) {
+              selectChild.before(optionElement);
+            } else {
+              selectElement.append(optionElement);
+            }
+
             storage.setValue(id, {
               value: getValue(event, true),
               items: getItems(event)
@@ -1502,7 +1580,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
               const optionElement = document.createElement("option");
               optionElement.textContent = displayValue;
               optionElement.value = exportValue;
-              selectElement.appendChild(optionElement);
+              selectElement.append(optionElement);
             }
 
             if (selectElement.options.length > 0) {
@@ -1564,11 +1642,15 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
       });
     }
 
+    if (this.data.combo) {
+      this._setTextStyle(selectElement);
+    } else {}
+
     this._setBackgroundColor(selectElement);
 
     this._setDefaultPropertiesFromJS(selectElement);
 
-    this.container.appendChild(selectElement);
+    this.container.append(selectElement);
     return this.container;
   }
 
@@ -1612,10 +1694,12 @@ class PopupAnnotationElement extends AnnotationElement {
 
     const popupLeft = rect[0] + this.data.parentRect[2] - this.data.parentRect[0];
     const popupTop = rect[1];
-    this.container.style.transformOrigin = `${-popupLeft}px ${-popupTop}px`;
-    this.container.style.left = `${popupLeft}px`;
-    this.container.style.top = `${popupTop}px`;
-    this.container.appendChild(popup.render());
+    const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
+    const pageWidth = pageURx - pageLLx;
+    const pageHeight = pageURy - pageLLy;
+    this.container.style.left = `${100 * (popupLeft - pageLLx) / pageWidth}%`;
+    this.container.style.top = `${100 * (popupTop - pageLLy) / pageHeight}%`;
+    this.container.append(popup.render());
     return this.container;
   }
 
@@ -1654,7 +1738,7 @@ class PopupElement {
     const title = document.createElement("h1");
     title.dir = this.titleObj.dir;
     title.textContent = this.titleObj.str;
-    popup.appendChild(title);
+    popup.append(title);
 
     const dateObject = _display_utils.PDFDateString.toDateObject(this.modificationDate);
 
@@ -1667,7 +1751,7 @@ class PopupElement {
         date: dateObject.toLocaleDateString(),
         time: dateObject.toLocaleTimeString()
       });
-      popup.appendChild(modificationDate);
+      popup.append(modificationDate);
     }
 
     if (this.richText?.str && (!this.contentsObj?.str || this.contentsObj.str === this.richText.str)) {
@@ -1681,7 +1765,7 @@ class PopupElement {
     } else {
       const contents = this._formatContents(this.contentsObj);
 
-      popup.appendChild(contents);
+      popup.append(contents);
     }
 
     if (!Array.isArray(this.trigger)) {
@@ -1695,7 +1779,7 @@ class PopupElement {
     }
 
     popup.addEventListener("click", this._hide.bind(this, true));
-    wrapper.appendChild(popup);
+    wrapper.append(popup);
     return wrapper;
   }
 
@@ -1710,10 +1794,10 @@ class PopupElement {
 
     for (let i = 0, ii = lines.length; i < ii; ++i) {
       const line = lines[i];
-      p.appendChild(document.createTextNode(line));
+      p.append(document.createTextNode(line));
 
       if (i < ii - 1) {
-        p.appendChild(document.createElement("br"));
+        p.append(document.createElement("br"));
       }
     }
 
@@ -1789,7 +1873,7 @@ class LineAnnotationElement extends AnnotationElement {
       width,
       height
     } = getRectDims(data.rect);
-    const svg = this.svgFactory.create(width, height);
+    const svg = this.svgFactory.create(width, height, true);
     const line = this.svgFactory.createElement("svg:line");
     line.setAttribute("x1", data.rect[2] - data.lineCoordinates[0]);
     line.setAttribute("y1", data.rect[3] - data.lineCoordinates[1]);
@@ -1798,7 +1882,7 @@ class LineAnnotationElement extends AnnotationElement {
     line.setAttribute("stroke-width", data.borderStyle.width || 1);
     line.setAttribute("stroke", "transparent");
     line.setAttribute("fill", "transparent");
-    svg.appendChild(line);
+    svg.append(line);
     this.container.append(svg);
 
     this._createPopup(line, data);
@@ -1824,7 +1908,7 @@ class SquareAnnotationElement extends AnnotationElement {
       width,
       height
     } = getRectDims(data.rect);
-    const svg = this.svgFactory.create(width, height);
+    const svg = this.svgFactory.create(width, height, true);
     const borderWidth = data.borderStyle.width;
     const square = this.svgFactory.createElement("svg:rect");
     square.setAttribute("x", borderWidth / 2);
@@ -1834,7 +1918,7 @@ class SquareAnnotationElement extends AnnotationElement {
     square.setAttribute("stroke-width", borderWidth || 1);
     square.setAttribute("stroke", "transparent");
     square.setAttribute("fill", "transparent");
-    svg.appendChild(square);
+    svg.append(square);
     this.container.append(svg);
 
     this._createPopup(square, data);
@@ -1860,7 +1944,7 @@ class CircleAnnotationElement extends AnnotationElement {
       width,
       height
     } = getRectDims(data.rect);
-    const svg = this.svgFactory.create(width, height);
+    const svg = this.svgFactory.create(width, height, true);
     const borderWidth = data.borderStyle.width;
     const circle = this.svgFactory.createElement("svg:ellipse");
     circle.setAttribute("cx", width / 2);
@@ -1870,7 +1954,7 @@ class CircleAnnotationElement extends AnnotationElement {
     circle.setAttribute("stroke-width", borderWidth || 1);
     circle.setAttribute("stroke", "transparent");
     circle.setAttribute("fill", "transparent");
-    svg.appendChild(circle);
+    svg.append(circle);
     this.container.append(svg);
 
     this._createPopup(circle, data);
@@ -1898,7 +1982,7 @@ class PolylineAnnotationElement extends AnnotationElement {
       width,
       height
     } = getRectDims(data.rect);
-    const svg = this.svgFactory.create(width, height);
+    const svg = this.svgFactory.create(width, height, true);
     let points = [];
 
     for (const coordinate of data.vertices) {
@@ -1913,7 +1997,7 @@ class PolylineAnnotationElement extends AnnotationElement {
     polyline.setAttribute("stroke-width", data.borderStyle.width || 1);
     polyline.setAttribute("stroke", "transparent");
     polyline.setAttribute("fill", "transparent");
-    svg.appendChild(polyline);
+    svg.append(polyline);
     this.container.append(svg);
 
     this._createPopup(polyline, data);
@@ -1971,7 +2055,7 @@ class InkAnnotationElement extends AnnotationElement {
       width,
       height
     } = getRectDims(data.rect);
-    const svg = this.svgFactory.create(width, height);
+    const svg = this.svgFactory.create(width, height, true);
 
     for (const inkList of data.inkLists) {
       let points = [];
@@ -1991,7 +2075,7 @@ class InkAnnotationElement extends AnnotationElement {
 
       this._createPopup(polyline, data);
 
-      svg.appendChild(polyline);
+      svg.append(polyline);
     }
 
     this.container.append(svg);
@@ -2142,15 +2226,14 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
   render() {
     this.container.className = "fileAttachmentAnnotation";
     const trigger = document.createElement("div");
-    trigger.style.height = this.container.style.height;
-    trigger.style.width = this.container.style.width;
+    trigger.className = "popupTriggerArea";
     trigger.addEventListener("dblclick", this._download.bind(this));
 
     if (!this.data.hasPopup && (this.data.titleObj?.str || this.data.contentsObj?.str || this.data.richText)) {
       this._createPopup(trigger, this.data);
     }
 
-    this.container.appendChild(trigger);
+    this.container.append(trigger);
     return this.container;
   }
 
@@ -2162,14 +2245,25 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
 
 class AnnotationLayer {
   static render(parameters) {
+    const {
+      annotations,
+      div,
+      viewport
+    } = parameters;
+    this.#setDimensions(div, viewport);
     const sortedAnnotations = [],
           popupAnnotations = [];
 
-    for (const data of parameters.annotations) {
+    for (const data of annotations) {
       if (!data) {
         continue;
       }
 
+      if (data.annotationType === _util.AnnotationType.POPUP) {
+        popupAnnotations.push(data);
+        continue;
+      }
+
       const {
         width,
         height
@@ -2179,11 +2273,6 @@ class AnnotationLayer {
         continue;
       }
 
-      if (data.annotationType === _util.AnnotationType.POPUP) {
-        popupAnnotations.push(data);
-        continue;
-      }
-
       sortedAnnotations.push(data);
     }
 
@@ -2191,14 +2280,12 @@ class AnnotationLayer {
       sortedAnnotations.push(...popupAnnotations);
     }
 
-    const div = parameters.div;
-
     for (const data of sortedAnnotations) {
       const element = AnnotationElementFactory.create({
         data,
         layer: div,
         page: parameters.page,
-        viewport: parameters.viewport,
+        viewport,
         linkService: parameters.linkService,
         downloadManager: parameters.downloadManager,
         imageResourcesPath: parameters.imageResourcesPath || "",
@@ -2222,13 +2309,13 @@ class AnnotationLayer {
 
         if (Array.isArray(rendered)) {
           for (const renderedElement of rendered) {
-            div.appendChild(renderedElement);
+            div.append(renderedElement);
           }
         } else {
           if (element instanceof PopupAnnotationElement) {
             div.prepend(rendered);
           } else {
-            div.appendChild(rendered);
+            div.append(rendered);
           }
         }
       }
@@ -2239,52 +2326,31 @@ class AnnotationLayer {
 
   static update(parameters) {
     const {
-      page,
-      viewport,
-      annotations,
       annotationCanvasMap,
-      div
+      div,
+      viewport
     } = parameters;
-    const transform = viewport.transform;
-    const matrix = `matrix(${transform.join(",")})`;
-    let scale, ownMatrix;
-
-    for (const data of annotations) {
-      const elements = div.querySelectorAll(`[data-annotation-id="${data.id}"]`);
-
-      if (elements) {
-        for (const element of elements) {
-          if (data.hasOwnCanvas) {
-            const rect = _util.Util.normalizeRect([data.rect[0], page.view[3] - data.rect[1] + page.view[1], data.rect[2], page.view[3] - data.rect[3] + page.view[1]]);
-
-            if (!ownMatrix) {
-              scale = Math.abs(transform[0] || transform[1]);
-              const ownTransform = transform.slice();
-
-              for (let i = 0; i < 4; i++) {
-                ownTransform[i] = Math.sign(ownTransform[i]);
-              }
-
-              ownMatrix = `matrix(${ownTransform.join(",")})`;
-            }
-
-            const left = rect[0] * scale;
-            const top = rect[1] * scale;
-            element.style.left = `${left}px`;
-            element.style.top = `${top}px`;
-            element.style.transformOrigin = `${-left}px ${-top}px`;
-            element.style.transform = ownMatrix;
-          } else {
-            element.style.transform = matrix;
-          }
-        }
-      }
-    }
-
+    this.#setDimensions(div, viewport);
     this.#setAnnotationCanvasMap(div, annotationCanvasMap);
     div.hidden = false;
   }
 
+  static #setDimensions(div, {
+    width,
+    height,
+    rotation
+  }) {
+    const {
+      style
+    } = div;
+    const flipOrientation = rotation % 180 !== 0,
+          widthStr = Math.floor(width) + "px",
+          heightStr = Math.floor(height) + "px";
+    style.width = flipOrientation ? heightStr : widthStr;
+    style.height = flipOrientation ? widthStr : heightStr;
+    div.setAttribute("data-main-rotation", rotation);
+  }
+
   static #setAnnotationCanvasMap(div, annotationCanvasMap) {
     if (!annotationCanvasMap) {
       return;
@@ -2301,10 +2367,12 @@ class AnnotationLayer {
         firstChild
       } = element;
 
-      if (firstChild.nodeName === "CANVAS") {
-        element.replaceChild(canvas, firstChild);
+      if (!firstChild) {
+        element.append(canvas);
+      } else if (firstChild.nodeName === "CANVAS") {
+        firstChild.replaceWith(canvas);
       } else {
-        element.insertBefore(canvas, firstChild);
+        firstChild.before(canvas);
       }
     }
 

+ 66 - 10
lib/display/annotation_storage.js

@@ -24,12 +24,14 @@
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.AnnotationStorage = void 0;
-
-var _murmurhash = require("../shared/murmurhash3.js");
+exports.PrintAnnotationStorage = exports.AnnotationStorage = void 0;
 
 var _util = require("../shared/util.js");
 
+var _editor = require("./editor/editor.js");
+
+var _murmurhash = require("../shared/murmurhash3.js");
+
 class AnnotationStorage {
   constructor() {
     this._storage = new Map();
@@ -52,6 +54,14 @@ class AnnotationStorage {
     return this._storage.get(key);
   }
 
+  removeKey(key) {
+    this._storage.delete(key);
+
+    if (this._storage.size === 0) {
+      this.resetModified();
+    }
+  }
+
   setValue(key, value) {
     const obj = this._storage.get(key);
 
@@ -71,10 +81,14 @@ class AnnotationStorage {
     }
 
     if (modified) {
-      this._setModified();
+      this.#setModified();
     }
   }
 
+  has(key) {
+    return this._storage.has(key);
+  }
+
   getAll() {
     return this._storage.size > 0 ? (0, _util.objectFromMap)(this._storage) : null;
   }
@@ -83,7 +97,7 @@ class AnnotationStorage {
     return this._storage.size;
   }
 
-  _setModified() {
+  #setModified() {
     if (!this._modified) {
       this._modified = true;
 
@@ -103,15 +117,37 @@ class AnnotationStorage {
     }
   }
 
+  get print() {
+    return new PrintAnnotationStorage(this);
+  }
+
   get serializable() {
-    return this._storage.size > 0 ? this._storage : null;
+    if (this._storage.size === 0) {
+      return null;
+    }
+
+    const clone = new Map();
+
+    for (const [key, val] of this._storage) {
+      const serialized = val instanceof _editor.AnnotationEditor ? val.serialize() : val;
+
+      if (serialized) {
+        clone.set(key, serialized);
+      }
+    }
+
+    return clone;
   }
 
-  get hash() {
+  static getHash(map) {
+    if (!map) {
+      return "";
+    }
+
     const hash = new _murmurhash.MurmurHash3_64();
 
-    for (const [key, value] of this._storage) {
-      hash.update(`${key}:${JSON.stringify(value)}`);
+    for (const [key, val] of map) {
+      hash.update(`${key}:${JSON.stringify(val)}`);
     }
 
     return hash.hexdigest();
@@ -119,4 +155,24 @@ class AnnotationStorage {
 
 }
 
-exports.AnnotationStorage = AnnotationStorage;
+exports.AnnotationStorage = AnnotationStorage;
+
+class PrintAnnotationStorage extends AnnotationStorage {
+  #serializable = null;
+
+  constructor(parent) {
+    super();
+    this.#serializable = structuredClone(parent.serializable);
+  }
+
+  get print() {
+    (0, _util.unreachable)("Should not call PrintAnnotationStorage.print");
+  }
+
+  get serializable() {
+    return this.#serializable;
+  }
+
+}
+
+exports.PrintAnnotationStorage = PrintAnnotationStorage;

+ 49 - 20
lib/display/api.js

@@ -31,12 +31,12 @@ exports.version = void 0;
 
 var _util = require("../shared/util.js");
 
+var _annotation_storage = require("./annotation_storage.js");
+
 var _display_utils = require("./display_utils.js");
 
 var _font_loader = require("./font_loader.js");
 
-var _annotation_storage = require("./annotation_storage.js");
-
 var _canvas = require("./canvas.js");
 
 var _worker_options = require("./worker_options.js");
@@ -292,7 +292,7 @@ async function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
 
   const workerId = await worker.messageHandler.sendWithPromise("GetDocRequest", {
     docId,
-    apiVersion: '2.14.305',
+    apiVersion: '2.15.349',
     source: {
       data: source.data,
       url: source.url,
@@ -313,6 +313,10 @@ async function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
     standardFontDataUrl: source.useWorkerFetch ? source.standardFontDataUrl : null
   });
 
+  if (source.data) {
+    source.data = null;
+  }
+
   if (worker.destroyed) {
     throw new Error("Worker was destroyed");
   }
@@ -705,7 +709,8 @@ class PDFPageProxy {
     background = null,
     optionalContentConfigPromise = null,
     annotationCanvasMap = null,
-    pageColors = null
+    pageColors = null,
+    printAnnotationStorage = null
   }) {
     if (arguments[0]?.renderInteractiveForms !== undefined) {
       (0, _display_utils.deprecated)("render no longer accepts the `renderInteractiveForms`-option, " + "please use the `annotationMode`-option instead.");
@@ -727,7 +732,7 @@ class PDFPageProxy {
       this._stats.time("Overall");
     }
 
-    const intentArgs = this._transport.getRenderingIntent(intent, annotationMode);
+    const intentArgs = this._transport.getRenderingIntent(intent, annotationMode, printAnnotationStorage);
 
     this.pendingCleanup = false;
 
@@ -758,7 +763,8 @@ class PDFPageProxy {
       intentState.operatorList = {
         fnArray: [],
         argsArray: [],
-        lastChunk: false
+        lastChunk: false,
+        separateAnnots: null
       };
 
       if (this._stats) {
@@ -837,7 +843,8 @@ class PDFPageProxy {
 
   getOperatorList({
     intent = "display",
-    annotationMode = _util.AnnotationMode.ENABLE
+    annotationMode = _util.AnnotationMode.ENABLE,
+    printAnnotationStorage = null
   } = {}) {
     function operatorListChanged() {
       if (intentState.operatorList.lastChunk) {
@@ -846,7 +853,7 @@ class PDFPageProxy {
       }
     }
 
-    const intentArgs = this._transport.getRenderingIntent(intent, annotationMode, true);
+    const intentArgs = this._transport.getRenderingIntent(intent, annotationMode, printAnnotationStorage, true);
 
     let intentState = this._intentStates.get(intentArgs.cacheKey);
 
@@ -866,7 +873,8 @@ class PDFPageProxy {
       intentState.operatorList = {
         fnArray: [],
         argsArray: [],
-        lastChunk: false
+        lastChunk: false,
+        separateAnnots: null
       };
 
       if (this._stats) {
@@ -1038,6 +1046,7 @@ class PDFPageProxy {
     }
 
     intentState.operatorList.lastChunk = operatorListChunk.lastChunk;
+    intentState.operatorList.separateAnnots = operatorListChunk.separateAnnots;
 
     for (const internalRenderTask of intentState.renderTasks) {
       internalRenderTask.operatorListChanged();
@@ -1050,13 +1059,14 @@ class PDFPageProxy {
 
   _pumpOperatorList({
     renderingIntent,
-    cacheKey
+    cacheKey,
+    annotationStorageMap
   }) {
     const readableStream = this._transport.messageHandler.sendWithStream("GetOperatorList", {
       pageIndex: this._pageIndex,
       intent: renderingIntent,
       cacheKey,
-      annotationStorage: renderingIntent & _util.RenderingIntentFlag.ANNOTATIONS_STORAGE ? this._transport.annotationStorage.serializable : null
+      annotationStorage: annotationStorageMap
     });
 
     const reader = readableStream.getReader();
@@ -1542,9 +1552,9 @@ class WorkerTransport {
     return this.#docStats;
   }
 
-  getRenderingIntent(intent, annotationMode = _util.AnnotationMode.ENABLE, isOpList = false) {
+  getRenderingIntent(intent, annotationMode = _util.AnnotationMode.ENABLE, printAnnotationStorage = null, isOpList = false) {
     let renderingIntent = _util.RenderingIntentFlag.DISPLAY;
-    let annotationHash = "";
+    let annotationMap = null;
 
     switch (intent) {
       case "any":
@@ -1576,7 +1586,8 @@ class WorkerTransport {
 
       case _util.AnnotationMode.ENABLE_STORAGE:
         renderingIntent += _util.RenderingIntentFlag.ANNOTATIONS_STORAGE;
-        annotationHash = this.annotationStorage.hash;
+        const annotationStorage = renderingIntent & _util.RenderingIntentFlag.PRINT && printAnnotationStorage instanceof _annotation_storage.PrintAnnotationStorage ? printAnnotationStorage : this.annotationStorage;
+        annotationMap = annotationStorage.serializable;
         break;
 
       default:
@@ -1589,7 +1600,8 @@ class WorkerTransport {
 
     return {
       renderingIntent,
-      cacheKey: `${renderingIntent}_${annotationHash}`
+      cacheKey: `${renderingIntent}_${_annotation_storage.AnnotationStorage.getHash(annotationMap)}`,
+      annotationStorageMap: annotationMap
     };
   }
 
@@ -2236,17 +2248,34 @@ class PDFObjects {
 }
 
 class RenderTask {
+  #internalRenderTask = null;
+
   constructor(internalRenderTask) {
-    this._internalRenderTask = internalRenderTask;
+    this.#internalRenderTask = internalRenderTask;
     this.onContinue = null;
   }
 
   get promise() {
-    return this._internalRenderTask.capability.promise;
+    return this.#internalRenderTask.capability.promise;
   }
 
   cancel() {
-    this._internalRenderTask.cancel();
+    this.#internalRenderTask.cancel();
+  }
+
+  get separateAnnots() {
+    const {
+      separateAnnots
+    } = this.#internalRenderTask.operatorList;
+
+    if (!separateAnnots) {
+      return false;
+    }
+
+    const {
+      annotationCanvasMap
+    } = this.#internalRenderTask;
+    return separateAnnots.form || separateAnnots.canvas && annotationCanvasMap?.size > 0;
   }
 
 }
@@ -2425,7 +2454,7 @@ class InternalRenderTask {
 
 }
 
-const version = '2.14.305';
+const version = '2.15.349';
 exports.version = version;
-const build = 'eaaa8b4ad';
+const build = 'b8aa9c622';
 exports.build = build;

+ 7 - 3
lib/display/base_factory.js

@@ -162,7 +162,7 @@ class BaseSVGFactory {
     }
   }
 
-  create(width, height) {
+  create(width, height, skipDimensions = false) {
     if (width <= 0 || height <= 0) {
       throw new Error("Invalid SVG dimensions");
     }
@@ -170,8 +170,12 @@ class BaseSVGFactory {
     const svg = this._createSVG("svg:svg");
 
     svg.setAttribute("version", "1.1");
-    svg.setAttribute("width", `${width}px`);
-    svg.setAttribute("height", `${height}px`);
+
+    if (!skipDimensions) {
+      svg.setAttribute("width", `${width}px`);
+      svg.setAttribute("height", `${height}px`);
+    }
+
     svg.setAttribute("preserveAspectRatio", "none");
     svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
     return svg;

+ 23 - 30
lib/display/canvas.js

@@ -28,14 +28,14 @@ exports.CanvasGraphics = void 0;
 
 var _util = require("../shared/util.js");
 
+var _display_utils = require("./display_utils.js");
+
 var _pattern_helper = require("./pattern_helper.js");
 
 var _image_utils = require("../shared/image_utils.js");
 
 var _is_node = require("../shared/is_node.js");
 
-var _display_utils = require("./display_utils.js");
-
 const MIN_FONT_SIZE = 16;
 const MAX_FONT_SIZE = 100;
 const MAX_GROUP_SIZE = 4096;
@@ -1140,10 +1140,7 @@ class CanvasGraphics {
       if (fg === "#000000" && bg === "#ffffff" || fg === bg || !isValidDefaultBg) {
         this.foregroundColor = this.backgroundColor = null;
       } else {
-        const cB = parseInt(defaultBg.slice(1), 16);
-        const rB = (cB && 0xff0000) >> 16;
-        const gB = (cB && 0x00ff00) >> 8;
-        const bB = cB && 0x0000ff;
+        const [rB, gB, bB] = (0, _display_utils.getRGB)(defaultBg);
 
         const newComp = x => {
           x /= 255;
@@ -1245,7 +1242,7 @@ class CanvasGraphics {
     }
   }
 
-  endDrawing() {
+  #restoreInitialState() {
     while (this.stateStack.length || this.inSMaskMode) {
       this.restore();
     }
@@ -1260,7 +1257,10 @@ class CanvasGraphics {
       this.ctx.restore();
       this.transparentCanvas = null;
     }
+  }
 
+  endDrawing() {
+    this.#restoreInitialState();
     this.cachedCanvases.clear();
     this.cachedPatterns.clear();
 
@@ -1845,8 +1845,7 @@ class CanvasGraphics {
     ctx.save();
     ctx.beginPath();
 
-    for (let i = 0; i < paths.length; i++) {
-      const path = paths[i];
+    for (const path of paths) {
       ctx.setTransform.apply(ctx, path.transform);
       ctx.translate(path.x, path.y);
       path.addToPath(ctx, path.fontSize);
@@ -2512,20 +2511,15 @@ class CanvasGraphics {
     }
   }
 
-  beginAnnotations() {
+  beginAnnotation(id, rect, transform, matrix, hasOwnCanvas) {
+    this.#restoreInitialState();
+    resetCtxToDefault(this.ctx, this.foregroundColor);
+    this.ctx.save();
     this.save();
 
     if (this.baseTransform) {
       this.ctx.setTransform.apply(this.ctx, this.baseTransform);
     }
-  }
-
-  endAnnotations() {
-    this.restore();
-  }
-
-  beginAnnotation(id, rect, transform, matrix, hasOwnCanvas) {
-    this.save();
 
     if (Array.isArray(rect) && rect.length === 4) {
       const width = rect[2] - rect[0];
@@ -2552,14 +2546,11 @@ class CanvasGraphics {
           canvas,
           context
         } = this.annotationCanvas;
-        const viewportScaleFactorStr = `var(--zoom-factor) * ${_display_utils.PixelsPerInch.PDF_TO_CSS_UNITS}`;
-        canvas.style.width = `calc(${width}px * ${viewportScaleFactorStr})`;
-        canvas.style.height = `calc(${height}px * ${viewportScaleFactorStr})`;
         this.annotationCanvasMap.set(id, canvas);
         this.annotationCanvas.savedCtx = this.ctx;
         this.ctx = context;
-        this.ctx.setTransform(scaleX, 0, 0, -scaleY, 0, height * scaleY);
         addContextCurrentTransform(this.ctx);
+        this.ctx.setTransform(scaleX, 0, 0, -scaleY, 0, height * scaleY);
         resetCtxToDefault(this.ctx, this.foregroundColor);
       } else {
         resetCtxToDefault(this.ctx, this.foregroundColor);
@@ -2580,8 +2571,6 @@ class CanvasGraphics {
       delete this.annotationCanvas.savedCtx;
       delete this.annotationCanvas;
     }
-
-    this.restore();
   }
 
   paintImageMaskXObject(img) {
@@ -2652,20 +2641,24 @@ class CanvasGraphics {
     const fillColor = this.current.fillColor;
     const isPatternFill = this.current.patternFill;
 
-    for (let i = 0, ii = images.length; i < ii; i++) {
-      const image = images[i];
-      const width = image.width,
-            height = image.height;
+    for (const image of images) {
+      const {
+        data,
+        width,
+        height,
+        transform
+      } = image;
       const maskCanvas = this.cachedCanvases.getCanvas("maskCanvas", width, height, false);
       const maskCtx = maskCanvas.context;
       maskCtx.save();
-      putBinaryImageMask(maskCtx, image);
+      const img = this.getObject(data, image);
+      putBinaryImageMask(maskCtx, img);
       maskCtx.globalCompositeOperation = "source-in";
       maskCtx.fillStyle = isPatternFill ? fillColor.getPattern(maskCtx, this, ctx.mozCurrentTransformInverse, _pattern_helper.PathType.FILL) : fillColor;
       maskCtx.fillRect(0, 0, width, height);
       maskCtx.restore();
       ctx.save();
-      ctx.transform.apply(ctx, image.transform);
+      ctx.transform.apply(ctx, transform);
       ctx.scale(1, -1);
       drawImageAtIntegerCoords(ctx, maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1);
       ctx.restore();

+ 62 - 1
lib/display/display_utils.js

@@ -25,9 +25,12 @@ Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.StatTimer = exports.RenderingCancelledException = exports.PixelsPerInch = exports.PageViewport = exports.PDFDateString = exports.DOMStandardFontDataFactory = exports.DOMSVGFactory = exports.DOMCanvasFactory = exports.DOMCMapReaderFactory = void 0;
+exports.binarySearchFirstItem = binarySearchFirstItem;
 exports.deprecated = deprecated;
+exports.getColorValues = getColorValues;
 exports.getFilenameFromUrl = getFilenameFromUrl;
 exports.getPdfFilenameFromUrl = getPdfFilenameFromUrl;
+exports.getRGB = getRGB;
 exports.getXfaPageViewport = getXfaPageViewport;
 exports.isDataScheme = isDataScheme;
 exports.isPdfFile = isPdfFile;
@@ -403,7 +406,7 @@ function loadScript(src, removeScriptElement = false) {
       reject(new Error(`Cannot load script at: ${script.src}`));
     };
 
-    (document.head || document.documentElement).appendChild(script);
+    (document.head || document.documentElement).append(script);
   });
 }
 
@@ -475,4 +478,62 @@ function getXfaPageViewport(xfaPage, {
     scale,
     rotation
   });
+}
+
+function getRGB(color) {
+  if (color.startsWith("#")) {
+    const colorRGB = parseInt(color.slice(1), 16);
+    return [(colorRGB & 0xff0000) >> 16, (colorRGB & 0x00ff00) >> 8, colorRGB & 0x0000ff];
+  }
+
+  if (color.startsWith("rgb(")) {
+    return color.slice(4, -1).split(",").map(x => parseInt(x));
+  }
+
+  if (color.startsWith("rgba(")) {
+    return color.slice(5, -1).split(",").map(x => parseInt(x)).slice(0, 3);
+  }
+
+  (0, _util.warn)(`Not a valid color format: "${color}"`);
+  return [0, 0, 0];
+}
+
+function getColorValues(colors) {
+  const span = document.createElement("span");
+  span.style.visibility = "hidden";
+  document.body.append(span);
+
+  for (const name of colors.keys()) {
+    span.style.color = name;
+    const computedColor = window.getComputedStyle(span).color;
+    colors.set(name, getRGB(computedColor));
+  }
+
+  span.remove();
+}
+
+function binarySearchFirstItem(items, condition, start = 0) {
+  let minIndex = start;
+  let maxIndex = items.length - 1;
+
+  if (maxIndex < 0 || !condition(items[maxIndex])) {
+    return items.length;
+  }
+
+  if (condition(items[minIndex])) {
+    return minIndex;
+  }
+
+  while (minIndex < maxIndex) {
+    const currentIndex = minIndex + maxIndex >> 1;
+    const currentItem = items[currentIndex];
+
+    if (condition(currentItem)) {
+      maxIndex = currentIndex;
+    } else {
+      minIndex = currentIndex + 1;
+    }
+  }
+
+  return minIndex;
 }

+ 604 - 0
lib/display/editor/annotation_editor_layer.js

@@ -0,0 +1,604 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.AnnotationEditorLayer = void 0;
+
+var _util = require("../../shared/util.js");
+
+var _tools = require("./tools.js");
+
+var _display_utils = require("../display_utils.js");
+
+var _freetext = require("./freetext.js");
+
+var _ink = require("./ink.js");
+
+class AnnotationEditorLayer {
+  #allowClick = false;
+  #boundPointerup = this.pointerup.bind(this);
+  #boundPointerdown = this.pointerdown.bind(this);
+  #editors = new Map();
+  #isCleaningUp = false;
+  #textLayerMap = new WeakMap();
+  #textNodes = new Map();
+  #uiManager;
+  #waitingEditors = new Set();
+  static _initialized = false;
+
+  constructor(options) {
+    if (!AnnotationEditorLayer._initialized) {
+      AnnotationEditorLayer._initialized = true;
+
+      _freetext.FreeTextEditor.initialize(options.l10n);
+
+      _ink.InkEditor.initialize(options.l10n);
+
+      options.uiManager.registerEditorTypes([_freetext.FreeTextEditor, _ink.InkEditor]);
+    }
+
+    this.#uiManager = options.uiManager;
+    this.annotationStorage = options.annotationStorage;
+    this.pageIndex = options.pageIndex;
+    this.div = options.div;
+    this.#uiManager.addLayer(this);
+  }
+
+  get textLayerElements() {
+    const textLayer = this.div.parentNode.getElementsByClassName("textLayer").item(0);
+
+    if (!textLayer) {
+      return (0, _util.shadow)(this, "textLayerElements", null);
+    }
+
+    let textChildren = this.#textLayerMap.get(textLayer);
+
+    if (textChildren) {
+      return textChildren;
+    }
+
+    textChildren = textLayer.querySelectorAll(`span[role="presentation"]`);
+
+    if (textChildren.length === 0) {
+      return (0, _util.shadow)(this, "textLayerElements", null);
+    }
+
+    textChildren = Array.from(textChildren);
+    textChildren.sort(AnnotationEditorLayer.#compareElementPositions);
+    this.#textLayerMap.set(textLayer, textChildren);
+    return textChildren;
+  }
+
+  get #hasTextLayer() {
+    return !!this.div.parentNode.querySelector(".textLayer .endOfContent");
+  }
+
+  updateToolbar(mode) {
+    this.#uiManager.updateToolbar(mode);
+  }
+
+  updateMode(mode = this.#uiManager.getMode()) {
+    this.#cleanup();
+
+    if (mode === _util.AnnotationEditorType.INK) {
+      this.addInkEditorIfNeeded(false);
+      this.disableClick();
+    } else {
+      this.enableClick();
+    }
+
+    this.#uiManager.unselectAll();
+  }
+
+  addInkEditorIfNeeded(isCommitting) {
+    if (!isCommitting && this.#uiManager.getMode() !== _util.AnnotationEditorType.INK) {
+      return;
+    }
+
+    if (!isCommitting) {
+      for (const editor of this.#editors.values()) {
+        if (editor.isEmpty()) {
+          editor.setInBackground();
+          return;
+        }
+      }
+    }
+
+    const editor = this.#createAndAddNewEditor({
+      offsetX: 0,
+      offsetY: 0
+    });
+    editor.setInBackground();
+  }
+
+  setEditingState(isEditing) {
+    this.#uiManager.setEditingState(isEditing);
+  }
+
+  addCommands(params) {
+    this.#uiManager.addCommands(params);
+  }
+
+  enable() {
+    this.div.style.pointerEvents = "auto";
+
+    for (const editor of this.#editors.values()) {
+      editor.enableEditing();
+    }
+  }
+
+  disable() {
+    this.div.style.pointerEvents = "none";
+
+    for (const editor of this.#editors.values()) {
+      editor.disableEditing();
+    }
+  }
+
+  setActiveEditor(editor) {
+    const currentActive = this.#uiManager.getActive();
+
+    if (currentActive === editor) {
+      return;
+    }
+
+    this.#uiManager.setActiveEditor(editor);
+  }
+
+  enableClick() {
+    this.div.addEventListener("pointerdown", this.#boundPointerdown);
+    this.div.addEventListener("pointerup", this.#boundPointerup);
+  }
+
+  disableClick() {
+    this.div.removeEventListener("pointerdown", this.#boundPointerdown);
+    this.div.removeEventListener("pointerup", this.#boundPointerup);
+  }
+
+  attach(editor) {
+    this.#editors.set(editor.id, editor);
+  }
+
+  detach(editor) {
+    this.#editors.delete(editor.id);
+    this.removePointerInTextLayer(editor);
+  }
+
+  remove(editor) {
+    this.#uiManager.removeEditor(editor);
+    this.detach(editor);
+    this.annotationStorage.removeKey(editor.id);
+    editor.div.style.display = "none";
+    setTimeout(() => {
+      editor.div.style.display = "";
+      editor.div.remove();
+      editor.isAttachedToDOM = false;
+
+      if (document.activeElement === document.body) {
+        this.#uiManager.focusMainContainer();
+      }
+    }, 0);
+
+    if (!this.#isCleaningUp) {
+      this.addInkEditorIfNeeded(false);
+    }
+  }
+
+  #changeParent(editor) {
+    if (editor.parent === this) {
+      return;
+    }
+
+    this.attach(editor);
+    editor.pageIndex = this.pageIndex;
+    editor.parent?.detach(editor);
+    editor.parent = this;
+
+    if (editor.div && editor.isAttachedToDOM) {
+      editor.div.remove();
+      this.div.append(editor.div);
+    }
+  }
+
+  static #compareElementPositions(e1, e2) {
+    const rect1 = e1.getBoundingClientRect();
+    const rect2 = e2.getBoundingClientRect();
+
+    if (rect1.y + rect1.height <= rect2.y) {
+      return -1;
+    }
+
+    if (rect2.y + rect2.height <= rect1.y) {
+      return +1;
+    }
+
+    const centerX1 = rect1.x + rect1.width / 2;
+    const centerX2 = rect2.x + rect2.width / 2;
+    return centerX1 - centerX2;
+  }
+
+  onTextLayerRendered() {
+    this.#textNodes.clear();
+
+    for (const editor of this.#waitingEditors) {
+      if (editor.isAttachedToDOM) {
+        this.addPointerInTextLayer(editor);
+      }
+    }
+
+    this.#waitingEditors.clear();
+  }
+
+  removePointerInTextLayer(editor) {
+    if (!this.#hasTextLayer) {
+      this.#waitingEditors.delete(editor);
+      return;
+    }
+
+    const {
+      id
+    } = editor;
+    const node = this.#textNodes.get(id);
+
+    if (!node) {
+      return;
+    }
+
+    this.#textNodes.delete(id);
+    let owns = node.getAttribute("aria-owns");
+
+    if (owns?.includes(id)) {
+      owns = owns.split(" ").filter(x => x !== id).join(" ");
+
+      if (owns) {
+        node.setAttribute("aria-owns", owns);
+      } else {
+        node.removeAttribute("aria-owns");
+        node.setAttribute("role", "presentation");
+      }
+    }
+  }
+
+  addPointerInTextLayer(editor) {
+    if (!this.#hasTextLayer) {
+      this.#waitingEditors.add(editor);
+      return;
+    }
+
+    this.removePointerInTextLayer(editor);
+    const children = this.textLayerElements;
+
+    if (!children) {
+      return;
+    }
+
+    const {
+      contentDiv
+    } = editor;
+    const id = editor.getIdForTextLayer();
+    const index = (0, _display_utils.binarySearchFirstItem)(children, node => AnnotationEditorLayer.#compareElementPositions(contentDiv, node) < 0);
+    const node = children[Math.max(0, index - 1)];
+    const owns = node.getAttribute("aria-owns");
+
+    if (!owns?.includes(id)) {
+      node.setAttribute("aria-owns", owns ? `${owns} ${id}` : id);
+    }
+
+    node.removeAttribute("role");
+    this.#textNodes.set(id, node);
+  }
+
+  moveDivInDOM(editor) {
+    this.addPointerInTextLayer(editor);
+    const {
+      div,
+      contentDiv
+    } = editor;
+
+    if (!this.div.hasChildNodes()) {
+      this.div.append(div);
+      return;
+    }
+
+    const children = Array.from(this.div.childNodes).filter(node => node !== div);
+
+    if (children.length === 0) {
+      return;
+    }
+
+    const index = (0, _display_utils.binarySearchFirstItem)(children, node => AnnotationEditorLayer.#compareElementPositions(contentDiv, node) < 0);
+
+    if (index === 0) {
+      children[0].before(div);
+    } else {
+      children[index - 1].after(div);
+    }
+  }
+
+  add(editor) {
+    this.#changeParent(editor);
+    this.addToAnnotationStorage(editor);
+    this.#uiManager.addEditor(editor);
+    this.attach(editor);
+
+    if (!editor.isAttachedToDOM) {
+      const div = editor.render();
+      this.div.append(div);
+      editor.isAttachedToDOM = true;
+    }
+
+    this.moveDivInDOM(editor);
+    editor.onceAdded();
+  }
+
+  addToAnnotationStorage(editor) {
+    if (!editor.isEmpty() && !this.annotationStorage.has(editor.id)) {
+      this.annotationStorage.setValue(editor.id, editor);
+    }
+  }
+
+  addOrRebuild(editor) {
+    if (editor.needsToBeRebuilt()) {
+      editor.rebuild();
+    } else {
+      this.add(editor);
+    }
+  }
+
+  addANewEditor(editor) {
+    const cmd = () => {
+      this.addOrRebuild(editor);
+    };
+
+    const undo = () => {
+      editor.remove();
+    };
+
+    this.addCommands({
+      cmd,
+      undo,
+      mustExec: true
+    });
+  }
+
+  addUndoableEditor(editor) {
+    const cmd = () => {
+      this.addOrRebuild(editor);
+    };
+
+    const undo = () => {
+      editor.remove();
+    };
+
+    this.addCommands({
+      cmd,
+      undo,
+      mustExec: false
+    });
+  }
+
+  getNextId() {
+    return this.#uiManager.getId();
+  }
+
+  #createNewEditor(params) {
+    switch (this.#uiManager.getMode()) {
+      case _util.AnnotationEditorType.FREETEXT:
+        return new _freetext.FreeTextEditor(params);
+
+      case _util.AnnotationEditorType.INK:
+        return new _ink.InkEditor(params);
+    }
+
+    return null;
+  }
+
+  deserialize(data) {
+    switch (data.annotationType) {
+      case _util.AnnotationEditorType.FREETEXT:
+        return _freetext.FreeTextEditor.deserialize(data, this);
+
+      case _util.AnnotationEditorType.INK:
+        return _ink.InkEditor.deserialize(data, this);
+    }
+
+    return null;
+  }
+
+  #createAndAddNewEditor(event) {
+    const id = this.getNextId();
+    const editor = this.#createNewEditor({
+      parent: this,
+      id,
+      x: event.offsetX,
+      y: event.offsetY
+    });
+
+    if (editor) {
+      this.add(editor);
+    }
+
+    return editor;
+  }
+
+  setSelected(editor) {
+    this.#uiManager.setSelected(editor);
+  }
+
+  toggleSelected(editor) {
+    this.#uiManager.toggleSelected(editor);
+  }
+
+  isSelected(editor) {
+    return this.#uiManager.isSelected(editor);
+  }
+
+  unselect(editor) {
+    this.#uiManager.unselect(editor);
+  }
+
+  pointerup(event) {
+    const isMac = _tools.KeyboardManager.platform.isMac;
+
+    if (event.button !== 0 || event.ctrlKey && isMac) {
+      return;
+    }
+
+    if (event.target !== this.div) {
+      return;
+    }
+
+    if (!this.#allowClick) {
+      this.#allowClick = true;
+      return;
+    }
+
+    this.#createAndAddNewEditor(event);
+  }
+
+  pointerdown(event) {
+    const isMac = _tools.KeyboardManager.platform.isMac;
+
+    if (event.button !== 0 || event.ctrlKey && isMac) {
+      return;
+    }
+
+    if (event.target !== this.div) {
+      return;
+    }
+
+    const editor = this.#uiManager.getActive();
+    this.#allowClick = !editor || editor.isEmpty();
+  }
+
+  drop(event) {
+    const id = event.dataTransfer.getData("text/plain");
+    const editor = this.#uiManager.getEditor(id);
+
+    if (!editor) {
+      return;
+    }
+
+    event.preventDefault();
+    event.dataTransfer.dropEffect = "move";
+    this.#changeParent(editor);
+    const rect = this.div.getBoundingClientRect();
+    const endX = event.clientX - rect.x;
+    const endY = event.clientY - rect.y;
+    editor.translate(endX - editor.startX, endY - editor.startY);
+    this.moveDivInDOM(editor);
+    editor.div.focus();
+  }
+
+  dragover(event) {
+    event.preventDefault();
+  }
+
+  destroy() {
+    if (this.#uiManager.getActive()?.parent === this) {
+      this.#uiManager.setActiveEditor(null);
+    }
+
+    for (const editor of this.#editors.values()) {
+      this.removePointerInTextLayer(editor);
+      editor.isAttachedToDOM = false;
+      editor.div.remove();
+      editor.parent = null;
+    }
+
+    this.#textNodes.clear();
+    this.div = null;
+    this.#editors.clear();
+    this.#waitingEditors.clear();
+    this.#uiManager.removeLayer(this);
+  }
+
+  #cleanup() {
+    this.#isCleaningUp = true;
+
+    for (const editor of this.#editors.values()) {
+      if (editor.isEmpty()) {
+        editor.remove();
+      }
+    }
+
+    this.#isCleaningUp = false;
+  }
+
+  render(parameters) {
+    this.viewport = parameters.viewport;
+    (0, _tools.bindEvents)(this, this.div, ["dragover", "drop"]);
+    this.setDimensions();
+
+    for (const editor of this.#uiManager.getEditors(this.pageIndex)) {
+      this.add(editor);
+    }
+
+    this.updateMode();
+  }
+
+  update(parameters) {
+    this.viewport = parameters.viewport;
+    this.setDimensions();
+    this.updateMode();
+  }
+
+  get scaleFactor() {
+    return this.viewport.scale;
+  }
+
+  get pageDimensions() {
+    const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
+    const width = pageURx - pageLLx;
+    const height = pageURy - pageLLy;
+    return [width, height];
+  }
+
+  get viewportBaseDimensions() {
+    const {
+      width,
+      height,
+      rotation
+    } = this.viewport;
+    return rotation % 180 === 0 ? [width, height] : [height, width];
+  }
+
+  setDimensions() {
+    const {
+      width,
+      height,
+      rotation
+    } = this.viewport;
+    const flipOrientation = rotation % 180 !== 0,
+          widthStr = Math.floor(width) + "px",
+          heightStr = Math.floor(height) + "px";
+    this.div.style.width = flipOrientation ? heightStr : widthStr;
+    this.div.style.height = flipOrientation ? widthStr : heightStr;
+    this.div.setAttribute("data-main-rotation", rotation);
+  }
+
+}
+
+exports.AnnotationEditorLayer = AnnotationEditorLayer;

+ 353 - 0
lib/display/editor/editor.js

@@ -0,0 +1,353 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.AnnotationEditor = void 0;
+
+var _tools = require("./tools.js");
+
+var _util = require("../../shared/util.js");
+
+class AnnotationEditor {
+  #boundFocusin = this.focusin.bind(this);
+  #boundFocusout = this.focusout.bind(this);
+  #hasBeenSelected = false;
+  #isEditing = false;
+  #isInEditMode = false;
+  #zIndex = AnnotationEditor._zIndex++;
+  static _colorManager = new _tools.ColorManager();
+  static _zIndex = 1;
+
+  constructor(parameters) {
+    if (this.constructor === AnnotationEditor) {
+      (0, _util.unreachable)("Cannot initialize AnnotationEditor.");
+    }
+
+    this.parent = parameters.parent;
+    this.id = parameters.id;
+    this.width = this.height = null;
+    this.pageIndex = parameters.parent.pageIndex;
+    this.name = parameters.name;
+    this.div = null;
+    const [width, height] = this.parent.viewportBaseDimensions;
+    this.x = parameters.x / width;
+    this.y = parameters.y / height;
+    this.rotation = this.parent.viewport.rotation;
+    this.isAttachedToDOM = false;
+  }
+
+  static get _defaultLineColor() {
+    return (0, _util.shadow)(this, "_defaultLineColor", this._colorManager.getHexCode("CanvasText"));
+  }
+
+  setInBackground() {
+    this.div.style.zIndex = 0;
+  }
+
+  setInForeground() {
+    this.div.style.zIndex = this.#zIndex;
+  }
+
+  focusin(event) {
+    if (!this.#hasBeenSelected) {
+      this.parent.setSelected(this);
+    } else {
+      this.#hasBeenSelected = false;
+    }
+  }
+
+  focusout(event) {
+    if (!this.isAttachedToDOM) {
+      return;
+    }
+
+    const target = event.relatedTarget;
+
+    if (target?.closest(`#${this.id}`)) {
+      return;
+    }
+
+    event.preventDefault();
+
+    if (!this.parent.isMultipleSelection) {
+      this.commitOrRemove();
+    }
+  }
+
+  commitOrRemove() {
+    if (this.isEmpty()) {
+      this.remove();
+    } else {
+      this.commit();
+    }
+  }
+
+  commit() {
+    this.parent.addToAnnotationStorage(this);
+  }
+
+  dragstart(event) {
+    const rect = this.parent.div.getBoundingClientRect();
+    this.startX = event.clientX - rect.x;
+    this.startY = event.clientY - rect.y;
+    event.dataTransfer.setData("text/plain", this.id);
+    event.dataTransfer.effectAllowed = "move";
+  }
+
+  setAt(x, y, tx, ty) {
+    const [width, height] = this.parent.viewportBaseDimensions;
+    [tx, ty] = this.screenToPageTranslation(tx, ty);
+    this.x = (x + tx) / width;
+    this.y = (y + ty) / height;
+    this.div.style.left = `${100 * this.x}%`;
+    this.div.style.top = `${100 * this.y}%`;
+  }
+
+  translate(x, y) {
+    const [width, height] = this.parent.viewportBaseDimensions;
+    [x, y] = this.screenToPageTranslation(x, y);
+    this.x += x / width;
+    this.y += y / height;
+    this.div.style.left = `${100 * this.x}%`;
+    this.div.style.top = `${100 * this.y}%`;
+  }
+
+  screenToPageTranslation(x, y) {
+    const {
+      rotation
+    } = this.parent.viewport;
+
+    switch (rotation) {
+      case 90:
+        return [y, -x];
+
+      case 180:
+        return [-x, -y];
+
+      case 270:
+        return [-y, x];
+
+      default:
+        return [x, y];
+    }
+  }
+
+  setDims(width, height) {
+    const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+    this.div.style.width = `${100 * width / parentWidth}%`;
+    this.div.style.height = `${100 * height / parentHeight}%`;
+  }
+
+  getInitialTranslation() {
+    return [0, 0];
+  }
+
+  render() {
+    this.div = document.createElement("div");
+    this.div.setAttribute("data-editor-rotation", (360 - this.rotation) % 360);
+    this.div.className = this.name;
+    this.div.setAttribute("id", this.id);
+    this.div.setAttribute("tabIndex", 0);
+    this.setInForeground();
+    this.div.addEventListener("focusin", this.#boundFocusin);
+    this.div.addEventListener("focusout", this.#boundFocusout);
+    const [tx, ty] = this.getInitialTranslation();
+    this.translate(tx, ty);
+    (0, _tools.bindEvents)(this, this.div, ["dragstart", "pointerdown"]);
+    return this.div;
+  }
+
+  pointerdown(event) {
+    const isMac = _tools.KeyboardManager.platform.isMac;
+
+    if (event.button !== 0 || event.ctrlKey && isMac) {
+      event.preventDefault();
+      return;
+    }
+
+    if (event.ctrlKey && !isMac || event.shiftKey || event.metaKey && isMac) {
+      this.parent.toggleSelected(this);
+    } else {
+      this.parent.setSelected(this);
+    }
+
+    this.#hasBeenSelected = true;
+  }
+
+  getRect(tx, ty) {
+    const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+    const [pageWidth, pageHeight] = this.parent.pageDimensions;
+    const shiftX = pageWidth * tx / parentWidth;
+    const shiftY = pageHeight * ty / parentHeight;
+    const x = this.x * pageWidth;
+    const y = this.y * pageHeight;
+    const width = this.width * pageWidth;
+    const height = this.height * pageHeight;
+
+    switch (this.rotation) {
+      case 0:
+        return [x + shiftX, pageHeight - y - shiftY - height, x + shiftX + width, pageHeight - y - shiftY];
+
+      case 90:
+        return [x + shiftY, pageHeight - y + shiftX, x + shiftY + height, pageHeight - y + shiftX + width];
+
+      case 180:
+        return [x - shiftX - width, pageHeight - y + shiftY, x - shiftX, pageHeight - y + shiftY + height];
+
+      case 270:
+        return [x - shiftY - height, pageHeight - y - shiftX - width, x - shiftY, pageHeight - y - shiftX];
+
+      default:
+        throw new Error("Invalid rotation");
+    }
+  }
+
+  getRectInCurrentCoords(rect, pageHeight) {
+    const [x1, y1, x2, y2] = rect;
+    const width = x2 - x1;
+    const height = y2 - y1;
+
+    switch (this.rotation) {
+      case 0:
+        return [x1, pageHeight - y2, width, height];
+
+      case 90:
+        return [x1, pageHeight - y1, height, width];
+
+      case 180:
+        return [x2, pageHeight - y1, width, height];
+
+      case 270:
+        return [x2, pageHeight - y2, height, width];
+
+      default:
+        throw new Error("Invalid rotation");
+    }
+  }
+
+  onceAdded() {}
+
+  isEmpty() {
+    return false;
+  }
+
+  enableEditMode() {
+    this.#isInEditMode = true;
+  }
+
+  disableEditMode() {
+    this.#isInEditMode = false;
+  }
+
+  isInEditMode() {
+    return this.#isInEditMode;
+  }
+
+  shouldGetKeyboardEvents() {
+    return false;
+  }
+
+  needsToBeRebuilt() {
+    return this.div && !this.isAttachedToDOM;
+  }
+
+  rebuild() {
+    this.div?.addEventListener("focusin", this.#boundFocusin);
+  }
+
+  serialize() {
+    (0, _util.unreachable)("An editor must be serializable");
+  }
+
+  static deserialize(data, parent) {
+    const editor = new this.prototype.constructor({
+      parent,
+      id: parent.getNextId()
+    });
+    editor.rotation = data.rotation;
+    const [pageWidth, pageHeight] = parent.pageDimensions;
+    const [x, y, width, height] = editor.getRectInCurrentCoords(data.rect, pageHeight);
+    editor.x = x / pageWidth;
+    editor.y = y / pageHeight;
+    editor.width = width / pageWidth;
+    editor.height = height / pageHeight;
+    return editor;
+  }
+
+  remove() {
+    this.div.removeEventListener("focusin", this.#boundFocusin);
+    this.div.removeEventListener("focusout", this.#boundFocusout);
+
+    if (!this.isEmpty()) {
+      this.commit();
+    }
+
+    this.parent.remove(this);
+  }
+
+  select() {
+    this.div?.classList.add("selectedEditor");
+  }
+
+  unselect() {
+    this.div?.classList.remove("selectedEditor");
+  }
+
+  updateParams(type, value) {}
+
+  disableEditing() {}
+
+  enableEditing() {}
+
+  getIdForTextLayer() {
+    return this.id;
+  }
+
+  get propertiesToUpdate() {
+    return {};
+  }
+
+  get contentDiv() {
+    return this.div;
+  }
+
+  get isEditing() {
+    return this.#isEditing;
+  }
+
+  set isEditing(value) {
+    this.#isEditing = value;
+
+    if (value) {
+      this.parent.setSelected(this);
+      this.parent.setActiveEditor(this);
+    } else {
+      this.parent.setActiveEditor(null);
+    }
+  }
+
+}
+
+exports.AnnotationEditor = AnnotationEditor;

+ 31 - 0
lib/display/editor/fit_curve.js

@@ -0,0 +1,31 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.fitCurve = void 0;
+
+const fitCurve = require("fit-curve");
+
+exports.fitCurve = fitCurve;

+ 384 - 0
lib/display/editor/freetext.js

@@ -0,0 +1,384 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.FreeTextEditor = void 0;
+
+var _util = require("../../shared/util.js");
+
+var _tools = require("./tools.js");
+
+var _editor = require("./editor.js");
+
+class FreeTextEditor extends _editor.AnnotationEditor {
+  #boundEditorDivBlur = this.editorDivBlur.bind(this);
+  #boundEditorDivFocus = this.editorDivFocus.bind(this);
+  #boundEditorDivKeydown = this.editorDivKeydown.bind(this);
+  #color;
+  #content = "";
+  #contentHTML = "";
+  #hasAlreadyBeenCommitted = false;
+  #fontSize;
+  static _freeTextDefaultContent = "";
+  static _l10nPromise;
+  static _internalPadding = 0;
+  static _defaultColor = null;
+  static _defaultFontSize = 10;
+  static _keyboardManager = new _tools.KeyboardManager([[["ctrl+Enter", "mac+meta+Enter", "Escape", "mac+Escape"], FreeTextEditor.prototype.commitOrRemove]]);
+
+  constructor(params) {
+    super({ ...params,
+      name: "freeTextEditor"
+    });
+    this.#color = params.color || FreeTextEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor;
+    this.#fontSize = params.fontSize || FreeTextEditor._defaultFontSize;
+  }
+
+  static initialize(l10n) {
+    this._l10nPromise = new Map(["free_text_default_content", "editor_free_text_aria_label"].map(str => [str, l10n.get(str)]));
+    const style = getComputedStyle(document.documentElement);
+    this._internalPadding = parseFloat(style.getPropertyValue("--freetext-padding"));
+  }
+
+  static updateDefaultParams(type, value) {
+    switch (type) {
+      case _util.AnnotationEditorParamsType.FREETEXT_SIZE:
+        FreeTextEditor._defaultFontSize = value;
+        break;
+
+      case _util.AnnotationEditorParamsType.FREETEXT_COLOR:
+        FreeTextEditor._defaultColor = value;
+        break;
+    }
+  }
+
+  updateParams(type, value) {
+    switch (type) {
+      case _util.AnnotationEditorParamsType.FREETEXT_SIZE:
+        this.#updateFontSize(value);
+        break;
+
+      case _util.AnnotationEditorParamsType.FREETEXT_COLOR:
+        this.#updateColor(value);
+        break;
+    }
+  }
+
+  static get defaultPropertiesToUpdate() {
+    return [[_util.AnnotationEditorParamsType.FREETEXT_SIZE, FreeTextEditor._defaultFontSize], [_util.AnnotationEditorParamsType.FREETEXT_COLOR, FreeTextEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor]];
+  }
+
+  get propertiesToUpdate() {
+    return [[_util.AnnotationEditorParamsType.FREETEXT_SIZE, this.#fontSize], [_util.AnnotationEditorParamsType.FREETEXT_COLOR, this.#color]];
+  }
+
+  #updateFontSize(fontSize) {
+    const setFontsize = size => {
+      this.editorDiv.style.fontSize = `calc(${size}px * var(--scale-factor))`;
+      this.translate(0, -(size - this.#fontSize) * this.parent.scaleFactor);
+      this.#fontSize = size;
+      this.#setEditorDimensions();
+    };
+
+    const savedFontsize = this.#fontSize;
+    this.parent.addCommands({
+      cmd: () => {
+        setFontsize(fontSize);
+      },
+      undo: () => {
+        setFontsize(savedFontsize);
+      },
+      mustExec: true,
+      type: _util.AnnotationEditorParamsType.FREETEXT_SIZE,
+      overwriteIfSameType: true,
+      keepUndo: true
+    });
+  }
+
+  #updateColor(color) {
+    const savedColor = this.#color;
+    this.parent.addCommands({
+      cmd: () => {
+        this.#color = color;
+        this.editorDiv.style.color = color;
+      },
+      undo: () => {
+        this.#color = savedColor;
+        this.editorDiv.style.color = savedColor;
+      },
+      mustExec: true,
+      type: _util.AnnotationEditorParamsType.FREETEXT_COLOR,
+      overwriteIfSameType: true,
+      keepUndo: true
+    });
+  }
+
+  getInitialTranslation() {
+    return [-FreeTextEditor._internalPadding * this.parent.scaleFactor, -(FreeTextEditor._internalPadding + this.#fontSize) * this.parent.scaleFactor];
+  }
+
+  rebuild() {
+    super.rebuild();
+
+    if (this.div === null) {
+      return;
+    }
+
+    if (!this.isAttachedToDOM) {
+      this.parent.add(this);
+    }
+  }
+
+  enableEditMode() {
+    if (this.isInEditMode()) {
+      return;
+    }
+
+    this.parent.setEditingState(false);
+    this.parent.updateToolbar(_util.AnnotationEditorType.FREETEXT);
+    super.enableEditMode();
+    this.overlayDiv.classList.remove("enabled");
+    this.editorDiv.contentEditable = true;
+    this.div.draggable = false;
+    this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown);
+    this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus);
+    this.editorDiv.addEventListener("blur", this.#boundEditorDivBlur);
+  }
+
+  disableEditMode() {
+    if (!this.isInEditMode()) {
+      return;
+    }
+
+    this.parent.setEditingState(true);
+    super.disableEditMode();
+    this.overlayDiv.classList.add("enabled");
+    this.editorDiv.contentEditable = false;
+    this.div.draggable = true;
+    this.editorDiv.removeEventListener("keydown", this.#boundEditorDivKeydown);
+    this.editorDiv.removeEventListener("focus", this.#boundEditorDivFocus);
+    this.editorDiv.removeEventListener("blur", this.#boundEditorDivBlur);
+    this.div.focus();
+    this.isEditing = false;
+  }
+
+  focusin(event) {
+    super.focusin(event);
+
+    if (event.target !== this.editorDiv) {
+      this.editorDiv.focus();
+    }
+  }
+
+  onceAdded() {
+    if (this.width) {
+      return;
+    }
+
+    this.enableEditMode();
+    this.editorDiv.focus();
+  }
+
+  isEmpty() {
+    return !this.editorDiv || this.editorDiv.innerText.trim() === "";
+  }
+
+  remove() {
+    this.isEditing = false;
+    this.parent.setEditingState(true);
+    super.remove();
+  }
+
+  #extractText() {
+    const divs = this.editorDiv.getElementsByTagName("div");
+
+    if (divs.length === 0) {
+      return this.editorDiv.innerText;
+    }
+
+    const buffer = [];
+
+    for (let i = 0, ii = divs.length; i < ii; i++) {
+      const div = divs[i];
+      const first = div.firstChild;
+
+      if (first?.nodeName === "#text") {
+        buffer.push(first.data);
+      } else {
+        buffer.push("");
+      }
+    }
+
+    return buffer.join("\n");
+  }
+
+  #setEditorDimensions() {
+    const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+    const rect = this.div.getBoundingClientRect();
+    this.width = rect.width / parentWidth;
+    this.height = rect.height / parentHeight;
+  }
+
+  commit() {
+    super.commit();
+
+    if (!this.#hasAlreadyBeenCommitted) {
+      this.#hasAlreadyBeenCommitted = true;
+      this.parent.addUndoableEditor(this);
+    }
+
+    this.disableEditMode();
+    this.#contentHTML = this.editorDiv.innerHTML;
+    this.#content = this.#extractText().trimEnd();
+    this.#setEditorDimensions();
+  }
+
+  shouldGetKeyboardEvents() {
+    return this.isInEditMode();
+  }
+
+  dblclick(event) {
+    this.enableEditMode();
+    this.editorDiv.focus();
+  }
+
+  keydown(event) {
+    if (event.target === this.div && event.key === "Enter") {
+      this.enableEditMode();
+      this.editorDiv.focus();
+    }
+  }
+
+  editorDivKeydown(event) {
+    FreeTextEditor._keyboardManager.exec(this, event);
+  }
+
+  editorDivFocus(event) {
+    this.isEditing = true;
+  }
+
+  editorDivBlur(event) {
+    this.isEditing = false;
+  }
+
+  disableEditing() {
+    this.editorDiv.setAttribute("role", "comment");
+    this.editorDiv.removeAttribute("aria-multiline");
+  }
+
+  enableEditing() {
+    this.editorDiv.setAttribute("role", "textbox");
+    this.editorDiv.setAttribute("aria-multiline", true);
+  }
+
+  getIdForTextLayer() {
+    return this.editorDiv.id;
+  }
+
+  render() {
+    if (this.div) {
+      return this.div;
+    }
+
+    let baseX, baseY;
+
+    if (this.width) {
+      baseX = this.x;
+      baseY = this.y;
+    }
+
+    super.render();
+    this.editorDiv = document.createElement("div");
+    this.editorDiv.className = "internal";
+    this.editorDiv.setAttribute("id", `${this.id}-editor`);
+    this.enableEditing();
+
+    FreeTextEditor._l10nPromise.get("editor_free_text_aria_label").then(msg => this.editorDiv?.setAttribute("aria-label", msg));
+
+    FreeTextEditor._l10nPromise.get("free_text_default_content").then(msg => this.editorDiv?.setAttribute("default-content", msg));
+
+    this.editorDiv.contentEditable = true;
+    const {
+      style
+    } = this.editorDiv;
+    style.fontSize = `calc(${this.#fontSize}px * var(--scale-factor))`;
+    style.color = this.#color;
+    this.div.append(this.editorDiv);
+    this.overlayDiv = document.createElement("div");
+    this.overlayDiv.classList.add("overlay", "enabled");
+    this.div.append(this.overlayDiv);
+    (0, _tools.bindEvents)(this, this.div, ["dblclick", "keydown"]);
+
+    if (this.width) {
+      const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+      this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
+      this.editorDiv.innerHTML = this.#contentHTML;
+      this.div.draggable = true;
+      this.editorDiv.contentEditable = false;
+    } else {
+      this.div.draggable = false;
+      this.editorDiv.contentEditable = true;
+    }
+
+    return this.div;
+  }
+
+  get contentDiv() {
+    return this.editorDiv;
+  }
+
+  static deserialize(data, parent) {
+    const editor = super.deserialize(data, parent);
+    editor.#fontSize = data.fontSize;
+    editor.#color = _util.Util.makeHexColor(...data.color);
+    editor.#content = data.value;
+    editor.#contentHTML = data.value.split("\n").map(line => `<div>${line}</div>`).join("");
+    return editor;
+  }
+
+  serialize() {
+    if (this.isEmpty()) {
+      return null;
+    }
+
+    const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor;
+    const rect = this.getRect(padding, padding);
+
+    const color = _editor.AnnotationEditor._colorManager.convert(getComputedStyle(this.editorDiv).color);
+
+    return {
+      annotationType: _util.AnnotationEditorType.FREETEXT,
+      color,
+      fontSize: this.#fontSize,
+      value: this.#content,
+      pageIndex: this.parent.pageIndex,
+      rect,
+      rotation: this.rotation
+    };
+  }
+
+}
+
+exports.FreeTextEditor = FreeTextEditor;

+ 817 - 0
lib/display/editor/ink.js

@@ -0,0 +1,817 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.InkEditor = void 0;
+Object.defineProperty(exports, "fitCurve", {
+  enumerable: true,
+  get: function () {
+    return _fit_curve.fitCurve;
+  }
+});
+
+var _util = require("../../shared/util.js");
+
+var _editor = require("./editor.js");
+
+var _fit_curve = require("./fit_curve");
+
+var _tools = require("./tools.js");
+
+const RESIZER_SIZE = 16;
+
+class InkEditor extends _editor.AnnotationEditor {
+  #aspectRatio = 0;
+  #baseHeight = 0;
+  #baseWidth = 0;
+  #boundCanvasPointermove = this.canvasPointermove.bind(this);
+  #boundCanvasPointerleave = this.canvasPointerleave.bind(this);
+  #boundCanvasPointerup = this.canvasPointerup.bind(this);
+  #boundCanvasPointerdown = this.canvasPointerdown.bind(this);
+  #disableEditing = false;
+  #isCanvasInitialized = false;
+  #lastPoint = null;
+  #observer = null;
+  #realWidth = 0;
+  #realHeight = 0;
+  #requestFrameCallback = null;
+  static _defaultColor = null;
+  static _defaultOpacity = 1;
+  static _defaultThickness = 1;
+  static _l10nPromise;
+
+  constructor(params) {
+    super({ ...params,
+      name: "inkEditor"
+    });
+    this.color = params.color || null;
+    this.thickness = params.thickness || null;
+    this.opacity = params.opacity || null;
+    this.paths = [];
+    this.bezierPath2D = [];
+    this.currentPath = [];
+    this.scaleFactor = 1;
+    this.translationX = this.translationY = 0;
+    this.x = 0;
+    this.y = 0;
+  }
+
+  static initialize(l10n) {
+    this._l10nPromise = new Map(["editor_ink_canvas_aria_label", "editor_ink_aria_label"].map(str => [str, l10n.get(str)]));
+  }
+
+  static updateDefaultParams(type, value) {
+    switch (type) {
+      case _util.AnnotationEditorParamsType.INK_THICKNESS:
+        InkEditor._defaultThickness = value;
+        break;
+
+      case _util.AnnotationEditorParamsType.INK_COLOR:
+        InkEditor._defaultColor = value;
+        break;
+
+      case _util.AnnotationEditorParamsType.INK_OPACITY:
+        InkEditor._defaultOpacity = value / 100;
+        break;
+    }
+  }
+
+  updateParams(type, value) {
+    switch (type) {
+      case _util.AnnotationEditorParamsType.INK_THICKNESS:
+        this.#updateThickness(value);
+        break;
+
+      case _util.AnnotationEditorParamsType.INK_COLOR:
+        this.#updateColor(value);
+        break;
+
+      case _util.AnnotationEditorParamsType.INK_OPACITY:
+        this.#updateOpacity(value);
+        break;
+    }
+  }
+
+  static get defaultPropertiesToUpdate() {
+    return [[_util.AnnotationEditorParamsType.INK_THICKNESS, InkEditor._defaultThickness], [_util.AnnotationEditorParamsType.INK_COLOR, InkEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor], [_util.AnnotationEditorParamsType.INK_OPACITY, Math.round(InkEditor._defaultOpacity * 100)]];
+  }
+
+  get propertiesToUpdate() {
+    return [[_util.AnnotationEditorParamsType.INK_THICKNESS, this.thickness || InkEditor._defaultThickness], [_util.AnnotationEditorParamsType.INK_COLOR, this.color || InkEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor], [_util.AnnotationEditorParamsType.INK_OPACITY, Math.round(100 * (this.opacity ?? InkEditor._defaultOpacity))]];
+  }
+
+  #updateThickness(thickness) {
+    const savedThickness = this.thickness;
+    this.parent.addCommands({
+      cmd: () => {
+        this.thickness = thickness;
+        this.#fitToContent();
+      },
+      undo: () => {
+        this.thickness = savedThickness;
+        this.#fitToContent();
+      },
+      mustExec: true,
+      type: _util.AnnotationEditorParamsType.INK_THICKNESS,
+      overwriteIfSameType: true,
+      keepUndo: true
+    });
+  }
+
+  #updateColor(color) {
+    const savedColor = this.color;
+    this.parent.addCommands({
+      cmd: () => {
+        this.color = color;
+        this.#redraw();
+      },
+      undo: () => {
+        this.color = savedColor;
+        this.#redraw();
+      },
+      mustExec: true,
+      type: _util.AnnotationEditorParamsType.INK_COLOR,
+      overwriteIfSameType: true,
+      keepUndo: true
+    });
+  }
+
+  #updateOpacity(opacity) {
+    opacity /= 100;
+    const savedOpacity = this.opacity;
+    this.parent.addCommands({
+      cmd: () => {
+        this.opacity = opacity;
+        this.#redraw();
+      },
+      undo: () => {
+        this.opacity = savedOpacity;
+        this.#redraw();
+      },
+      mustExec: true,
+      type: _util.AnnotationEditorParamsType.INK_OPACITY,
+      overwriteIfSameType: true,
+      keepUndo: true
+    });
+  }
+
+  rebuild() {
+    super.rebuild();
+
+    if (this.div === null) {
+      return;
+    }
+
+    if (!this.canvas) {
+      this.#createCanvas();
+      this.#createObserver();
+    }
+
+    if (!this.isAttachedToDOM) {
+      this.parent.add(this);
+      this.#setCanvasDims();
+    }
+
+    this.#fitToContent();
+  }
+
+  remove() {
+    if (this.canvas === null) {
+      return;
+    }
+
+    if (!this.isEmpty()) {
+      this.commit();
+    }
+
+    this.canvas.width = this.canvas.height = 0;
+    this.canvas.remove();
+    this.canvas = null;
+    this.#observer.disconnect();
+    this.#observer = null;
+    super.remove();
+  }
+
+  enableEditMode() {
+    if (this.#disableEditing || this.canvas === null) {
+      return;
+    }
+
+    super.enableEditMode();
+    this.div.draggable = false;
+    this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown);
+    this.canvas.addEventListener("pointerup", this.#boundCanvasPointerup);
+  }
+
+  disableEditMode() {
+    if (!this.isInEditMode() || this.canvas === null) {
+      return;
+    }
+
+    super.disableEditMode();
+    this.div.draggable = !this.isEmpty();
+    this.div.classList.remove("editing");
+    this.canvas.removeEventListener("pointerdown", this.#boundCanvasPointerdown);
+    this.canvas.removeEventListener("pointerup", this.#boundCanvasPointerup);
+  }
+
+  onceAdded() {
+    this.div.draggable = !this.isEmpty();
+  }
+
+  isEmpty() {
+    return this.paths.length === 0 || this.paths.length === 1 && this.paths[0].length === 0;
+  }
+
+  #getInitialBBox() {
+    const {
+      width,
+      height,
+      rotation
+    } = this.parent.viewport;
+
+    switch (rotation) {
+      case 90:
+        return [0, width, width, height];
+
+      case 180:
+        return [width, height, width, height];
+
+      case 270:
+        return [height, 0, width, height];
+
+      default:
+        return [0, 0, width, height];
+    }
+  }
+
+  #setStroke() {
+    this.ctx.lineWidth = this.thickness * this.parent.scaleFactor / this.scaleFactor;
+    this.ctx.lineCap = "round";
+    this.ctx.lineJoin = "round";
+    this.ctx.miterLimit = 10;
+    this.ctx.strokeStyle = `${this.color}${(0, _tools.opacityToHex)(this.opacity)}`;
+  }
+
+  #startDrawing(x, y) {
+    this.isEditing = true;
+
+    if (!this.#isCanvasInitialized) {
+      this.#isCanvasInitialized = true;
+      this.#setCanvasDims();
+      this.thickness ||= InkEditor._defaultThickness;
+      this.color ||= InkEditor._defaultColor || _editor.AnnotationEditor._defaultLineColor;
+      this.opacity ??= InkEditor._defaultOpacity;
+    }
+
+    this.currentPath.push([x, y]);
+    this.#lastPoint = null;
+    this.#setStroke();
+    this.ctx.beginPath();
+    this.ctx.moveTo(x, y);
+
+    this.#requestFrameCallback = () => {
+      if (!this.#requestFrameCallback) {
+        return;
+      }
+
+      if (this.#lastPoint) {
+        if (this.isEmpty()) {
+          this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+          this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+        } else {
+          this.#redraw();
+        }
+
+        this.ctx.lineTo(...this.#lastPoint);
+        this.#lastPoint = null;
+        this.ctx.stroke();
+      }
+
+      window.requestAnimationFrame(this.#requestFrameCallback);
+    };
+
+    window.requestAnimationFrame(this.#requestFrameCallback);
+  }
+
+  #draw(x, y) {
+    const [lastX, lastY] = this.currentPath.at(-1);
+
+    if (x === lastX && y === lastY) {
+      return;
+    }
+
+    this.currentPath.push([x, y]);
+    this.#lastPoint = [x, y];
+  }
+
+  #stopDrawing(x, y) {
+    this.ctx.closePath();
+    this.#requestFrameCallback = null;
+    x = Math.min(Math.max(x, 0), this.canvas.width);
+    y = Math.min(Math.max(y, 0), this.canvas.height);
+    const [lastX, lastY] = this.currentPath.at(-1);
+
+    if (x !== lastX || y !== lastY) {
+      this.currentPath.push([x, y]);
+    }
+
+    let bezier;
+
+    if (this.currentPath.length !== 1) {
+      bezier = (0, _fit_curve.fitCurve)(this.currentPath, 30, null);
+    } else {
+      const xy = [x, y];
+      bezier = [[xy, xy.slice(), xy.slice(), xy]];
+    }
+
+    const path2D = InkEditor.#buildPath2D(bezier);
+    this.currentPath.length = 0;
+
+    const cmd = () => {
+      this.paths.push(bezier);
+      this.bezierPath2D.push(path2D);
+      this.rebuild();
+    };
+
+    const undo = () => {
+      this.paths.pop();
+      this.bezierPath2D.pop();
+
+      if (this.paths.length === 0) {
+        this.remove();
+      } else {
+        if (!this.canvas) {
+          this.#createCanvas();
+          this.#createObserver();
+        }
+
+        this.#fitToContent();
+      }
+    };
+
+    this.parent.addCommands({
+      cmd,
+      undo,
+      mustExec: true
+    });
+  }
+
+  #redraw() {
+    if (this.isEmpty()) {
+      this.#updateTransform();
+      return;
+    }
+
+    this.#setStroke();
+    const {
+      canvas,
+      ctx
+    } = this;
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    this.#updateTransform();
+
+    for (const path of this.bezierPath2D) {
+      ctx.stroke(path);
+    }
+  }
+
+  commit() {
+    if (this.#disableEditing) {
+      return;
+    }
+
+    super.commit();
+    this.isEditing = false;
+    this.disableEditMode();
+    this.setInForeground();
+    this.#disableEditing = true;
+    this.div.classList.add("disabled");
+    this.#fitToContent(true);
+    this.parent.addInkEditorIfNeeded(true);
+    this.parent.moveDivInDOM(this);
+    this.div.focus();
+  }
+
+  focusin(event) {
+    super.focusin(event);
+    this.enableEditMode();
+  }
+
+  canvasPointerdown(event) {
+    if (event.button !== 0 || !this.isInEditMode() || this.#disableEditing) {
+      return;
+    }
+
+    this.setInForeground();
+
+    if (event.type !== "mouse") {
+      this.div.focus();
+    }
+
+    event.stopPropagation();
+    this.canvas.addEventListener("pointerleave", this.#boundCanvasPointerleave);
+    this.canvas.addEventListener("pointermove", this.#boundCanvasPointermove);
+    this.#startDrawing(event.offsetX, event.offsetY);
+  }
+
+  canvasPointermove(event) {
+    event.stopPropagation();
+    this.#draw(event.offsetX, event.offsetY);
+  }
+
+  canvasPointerup(event) {
+    if (event.button !== 0) {
+      return;
+    }
+
+    if (this.isInEditMode() && this.currentPath.length !== 0) {
+      event.stopPropagation();
+      this.#endDrawing(event);
+      this.setInBackground();
+    }
+  }
+
+  canvasPointerleave(event) {
+    this.#endDrawing(event);
+    this.setInBackground();
+  }
+
+  #endDrawing(event) {
+    this.#stopDrawing(event.offsetX, event.offsetY);
+    this.canvas.removeEventListener("pointerleave", this.#boundCanvasPointerleave);
+    this.canvas.removeEventListener("pointermove", this.#boundCanvasPointermove);
+    this.parent.addToAnnotationStorage(this);
+  }
+
+  #createCanvas() {
+    this.canvas = document.createElement("canvas");
+    this.canvas.width = this.canvas.height = 0;
+    this.canvas.className = "inkEditorCanvas";
+
+    InkEditor._l10nPromise.get("editor_ink_canvas_aria_label").then(msg => this.canvas?.setAttribute("aria-label", msg));
+
+    this.div.append(this.canvas);
+    this.ctx = this.canvas.getContext("2d");
+  }
+
+  #createObserver() {
+    this.#observer = new ResizeObserver(entries => {
+      const rect = entries[0].contentRect;
+
+      if (rect.width && rect.height) {
+        this.setDimensions(rect.width, rect.height);
+      }
+    });
+    this.#observer.observe(this.div);
+  }
+
+  render() {
+    if (this.div) {
+      return this.div;
+    }
+
+    let baseX, baseY;
+
+    if (this.width) {
+      baseX = this.x;
+      baseY = this.y;
+    }
+
+    super.render();
+
+    InkEditor._l10nPromise.get("editor_ink_aria_label").then(msg => this.div?.setAttribute("aria-label", msg));
+
+    const [x, y, w, h] = this.#getInitialBBox();
+    this.setAt(x, y, 0, 0);
+    this.setDims(w, h);
+    this.#createCanvas();
+
+    if (this.width) {
+      const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+      this.setAt(baseX * parentWidth, baseY * parentHeight, this.width * parentWidth, this.height * parentHeight);
+      this.#isCanvasInitialized = true;
+      this.#setCanvasDims();
+      this.setDims(this.width * parentWidth, this.height * parentHeight);
+      this.#redraw();
+      this.#setMinDims();
+      this.div.classList.add("disabled");
+    } else {
+      this.div.classList.add("editing");
+      this.enableEditMode();
+    }
+
+    this.#createObserver();
+    return this.div;
+  }
+
+  #setCanvasDims() {
+    if (!this.#isCanvasInitialized) {
+      return;
+    }
+
+    const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+    this.canvas.width = Math.ceil(this.width * parentWidth);
+    this.canvas.height = Math.ceil(this.height * parentHeight);
+    this.#updateTransform();
+  }
+
+  setDimensions(width, height) {
+    const roundedWidth = Math.round(width);
+    const roundedHeight = Math.round(height);
+
+    if (this.#realWidth === roundedWidth && this.#realHeight === roundedHeight) {
+      return;
+    }
+
+    this.#realWidth = roundedWidth;
+    this.#realHeight = roundedHeight;
+    this.canvas.style.visibility = "hidden";
+
+    if (this.#aspectRatio && Math.abs(this.#aspectRatio - width / height) > 1e-2) {
+      height = Math.ceil(width / this.#aspectRatio);
+      this.setDims(width, height);
+    }
+
+    const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+    this.width = width / parentWidth;
+    this.height = height / parentHeight;
+
+    if (this.#disableEditing) {
+      this.#setScaleFactor(width, height);
+    }
+
+    this.#setCanvasDims();
+    this.#redraw();
+    this.canvas.style.visibility = "visible";
+  }
+
+  #setScaleFactor(width, height) {
+    const padding = this.#getPadding();
+    const scaleFactorW = (width - padding) / this.#baseWidth;
+    const scaleFactorH = (height - padding) / this.#baseHeight;
+    this.scaleFactor = Math.min(scaleFactorW, scaleFactorH);
+  }
+
+  #updateTransform() {
+    const padding = this.#getPadding() / 2;
+    this.ctx.setTransform(this.scaleFactor, 0, 0, this.scaleFactor, this.translationX * this.scaleFactor + padding, this.translationY * this.scaleFactor + padding);
+  }
+
+  static #buildPath2D(bezier) {
+    const path2D = new Path2D();
+
+    for (let i = 0, ii = bezier.length; i < ii; i++) {
+      const [first, control1, control2, second] = bezier[i];
+
+      if (i === 0) {
+        path2D.moveTo(...first);
+      }
+
+      path2D.bezierCurveTo(control1[0], control1[1], control2[0], control2[1], second[0], second[1]);
+    }
+
+    return path2D;
+  }
+
+  #serializePaths(s, tx, ty, h) {
+    const NUMBER_OF_POINTS_ON_BEZIER_CURVE = 4;
+    const paths = [];
+    const padding = this.thickness / 2;
+    let buffer, points;
+
+    for (const bezier of this.paths) {
+      buffer = [];
+      points = [];
+
+      for (let i = 0, ii = bezier.length; i < ii; i++) {
+        const [first, control1, control2, second] = bezier[i];
+        const p10 = s * (first[0] + tx) + padding;
+        const p11 = h - s * (first[1] + ty) - padding;
+        const p20 = s * (control1[0] + tx) + padding;
+        const p21 = h - s * (control1[1] + ty) - padding;
+        const p30 = s * (control2[0] + tx) + padding;
+        const p31 = h - s * (control2[1] + ty) - padding;
+        const p40 = s * (second[0] + tx) + padding;
+        const p41 = h - s * (second[1] + ty) - padding;
+
+        if (i === 0) {
+          buffer.push(p10, p11);
+          points.push(p10, p11);
+        }
+
+        buffer.push(p20, p21, p30, p31, p40, p41);
+        this.#extractPointsOnBezier(p10, p11, p20, p21, p30, p31, p40, p41, NUMBER_OF_POINTS_ON_BEZIER_CURVE, points);
+      }
+
+      paths.push({
+        bezier: buffer,
+        points
+      });
+    }
+
+    return paths;
+  }
+
+  #extractPointsOnBezier(p10, p11, p20, p21, p30, p31, p40, p41, n, points) {
+    if (this.#isAlmostFlat(p10, p11, p20, p21, p30, p31, p40, p41)) {
+      points.push(p40, p41);
+      return;
+    }
+
+    for (let i = 1; i < n - 1; i++) {
+      const t = i / n;
+      const mt = 1 - t;
+      let q10 = t * p10 + mt * p20;
+      let q11 = t * p11 + mt * p21;
+      let q20 = t * p20 + mt * p30;
+      let q21 = t * p21 + mt * p31;
+      const q30 = t * p30 + mt * p40;
+      const q31 = t * p31 + mt * p41;
+      q10 = t * q10 + mt * q20;
+      q11 = t * q11 + mt * q21;
+      q20 = t * q20 + mt * q30;
+      q21 = t * q21 + mt * q31;
+      q10 = t * q10 + mt * q20;
+      q11 = t * q11 + mt * q21;
+      points.push(q10, q11);
+    }
+
+    points.push(p40, p41);
+  }
+
+  #isAlmostFlat(p10, p11, p20, p21, p30, p31, p40, p41) {
+    const tol = 10;
+    const ax = (3 * p20 - 2 * p10 - p40) ** 2;
+    const ay = (3 * p21 - 2 * p11 - p41) ** 2;
+    const bx = (3 * p30 - p10 - 2 * p40) ** 2;
+    const by = (3 * p31 - p11 - 2 * p41) ** 2;
+    return Math.max(ax, bx) + Math.max(ay, by) <= tol;
+  }
+
+  #getBbox() {
+    let xMin = Infinity;
+    let xMax = -Infinity;
+    let yMin = Infinity;
+    let yMax = -Infinity;
+
+    for (const path of this.paths) {
+      for (const [first, control1, control2, second] of path) {
+        const bbox = _util.Util.bezierBoundingBox(...first, ...control1, ...control2, ...second);
+
+        xMin = Math.min(xMin, bbox[0]);
+        yMin = Math.min(yMin, bbox[1]);
+        xMax = Math.max(xMax, bbox[2]);
+        yMax = Math.max(yMax, bbox[3]);
+      }
+    }
+
+    return [xMin, yMin, xMax, yMax];
+  }
+
+  #getPadding() {
+    return this.#disableEditing ? Math.ceil(this.thickness * this.parent.scaleFactor) : 0;
+  }
+
+  #fitToContent(firstTime = false) {
+    if (this.isEmpty()) {
+      return;
+    }
+
+    if (!this.#disableEditing) {
+      this.#redraw();
+      return;
+    }
+
+    const bbox = this.#getBbox();
+    const padding = this.#getPadding();
+    this.#baseWidth = Math.max(RESIZER_SIZE, bbox[2] - bbox[0]);
+    this.#baseHeight = Math.max(RESIZER_SIZE, bbox[3] - bbox[1]);
+    const width = Math.ceil(padding + this.#baseWidth * this.scaleFactor);
+    const height = Math.ceil(padding + this.#baseHeight * this.scaleFactor);
+    const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
+    this.width = width / parentWidth;
+    this.height = height / parentHeight;
+    this.#aspectRatio = width / height;
+    this.#setMinDims();
+    const prevTranslationX = this.translationX;
+    const prevTranslationY = this.translationY;
+    this.translationX = -bbox[0];
+    this.translationY = -bbox[1];
+    this.#setCanvasDims();
+    this.#redraw();
+    this.#realWidth = width;
+    this.#realHeight = height;
+    this.setDims(width, height);
+    const unscaledPadding = firstTime ? padding / this.scaleFactor / 2 : 0;
+    this.translate(prevTranslationX - this.translationX - unscaledPadding, prevTranslationY - this.translationY - unscaledPadding);
+  }
+
+  #setMinDims() {
+    const {
+      style
+    } = this.div;
+
+    if (this.#aspectRatio >= 1) {
+      style.minHeight = `${RESIZER_SIZE}px`;
+      style.minWidth = `${Math.round(this.#aspectRatio * RESIZER_SIZE)}px`;
+    } else {
+      style.minWidth = `${RESIZER_SIZE}px`;
+      style.minHeight = `${Math.round(RESIZER_SIZE / this.#aspectRatio)}px`;
+    }
+  }
+
+  static deserialize(data, parent) {
+    const editor = super.deserialize(data, parent);
+    editor.thickness = data.thickness;
+    editor.color = _util.Util.makeHexColor(...data.color);
+    editor.opacity = data.opacity;
+    const [pageWidth, pageHeight] = parent.pageDimensions;
+    const width = editor.width * pageWidth;
+    const height = editor.height * pageHeight;
+    const scaleFactor = parent.scaleFactor;
+    const padding = data.thickness / 2;
+    editor.#aspectRatio = width / height;
+    editor.#disableEditing = true;
+    editor.#realWidth = Math.round(width);
+    editor.#realHeight = Math.round(height);
+
+    for (const {
+      bezier
+    } of data.paths) {
+      const path = [];
+      editor.paths.push(path);
+      let p0 = scaleFactor * (bezier[0] - padding);
+      let p1 = scaleFactor * (height - bezier[1] - padding);
+
+      for (let i = 2, ii = bezier.length; i < ii; i += 6) {
+        const p10 = scaleFactor * (bezier[i] - padding);
+        const p11 = scaleFactor * (height - bezier[i + 1] - padding);
+        const p20 = scaleFactor * (bezier[i + 2] - padding);
+        const p21 = scaleFactor * (height - bezier[i + 3] - padding);
+        const p30 = scaleFactor * (bezier[i + 4] - padding);
+        const p31 = scaleFactor * (height - bezier[i + 5] - padding);
+        path.push([[p0, p1], [p10, p11], [p20, p21], [p30, p31]]);
+        p0 = p30;
+        p1 = p31;
+      }
+
+      const path2D = this.#buildPath2D(path);
+      editor.bezierPath2D.push(path2D);
+    }
+
+    const bbox = editor.#getBbox();
+    editor.#baseWidth = bbox[2] - bbox[0];
+    editor.#baseHeight = bbox[3] - bbox[1];
+    editor.#setScaleFactor(width, height);
+    return editor;
+  }
+
+  serialize() {
+    if (this.isEmpty()) {
+      return null;
+    }
+
+    const rect = this.getRect(0, 0);
+    const height = this.rotation % 180 === 0 ? rect[3] - rect[1] : rect[2] - rect[0];
+
+    const color = _editor.AnnotationEditor._colorManager.convert(this.ctx.strokeStyle);
+
+    return {
+      annotationType: _util.AnnotationEditorType.INK,
+      color,
+      thickness: this.thickness,
+      opacity: this.opacity,
+      paths: this.#serializePaths(this.scaleFactor / this.parent.scaleFactor, this.translationX, this.translationY, height),
+      pageIndex: this.parent.pageIndex,
+      rect,
+      rotation: this.rotation
+    };
+  }
+
+}
+
+exports.InkEditor = InkEditor;

+ 814 - 0
lib/display/editor/tools.js

@@ -0,0 +1,814 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.KeyboardManager = exports.CommandManager = exports.ColorManager = exports.AnnotationEditorUIManager = void 0;
+exports.bindEvents = bindEvents;
+exports.opacityToHex = opacityToHex;
+
+var _util = require("../../shared/util.js");
+
+var _display_utils = require("../display_utils.js");
+
+function bindEvents(obj, element, names) {
+  for (const name of names) {
+    element.addEventListener(name, obj[name].bind(obj));
+  }
+}
+
+function opacityToHex(opacity) {
+  return Math.round(Math.min(255, Math.max(1, 255 * opacity))).toString(16).padStart(2, "0");
+}
+
+class IdManager {
+  #id = 0;
+
+  getId() {
+    return `${_util.AnnotationEditorPrefix}${this.#id++}`;
+  }
+
+}
+
+class CommandManager {
+  #commands = [];
+  #locked = false;
+  #maxSize;
+  #position = -1;
+
+  constructor(maxSize = 128) {
+    this.#maxSize = maxSize;
+  }
+
+  add({
+    cmd,
+    undo,
+    mustExec,
+    type = NaN,
+    overwriteIfSameType = false,
+    keepUndo = false
+  }) {
+    if (mustExec) {
+      cmd();
+    }
+
+    if (this.#locked) {
+      return;
+    }
+
+    const save = {
+      cmd,
+      undo,
+      type
+    };
+
+    if (this.#position === -1) {
+      if (this.#commands.length > 0) {
+        this.#commands.length = 0;
+      }
+
+      this.#position = 0;
+      this.#commands.push(save);
+      return;
+    }
+
+    if (overwriteIfSameType && this.#commands[this.#position].type === type) {
+      if (keepUndo) {
+        save.undo = this.#commands[this.#position].undo;
+      }
+
+      this.#commands[this.#position] = save;
+      return;
+    }
+
+    const next = this.#position + 1;
+
+    if (next === this.#maxSize) {
+      this.#commands.splice(0, 1);
+    } else {
+      this.#position = next;
+
+      if (next < this.#commands.length) {
+        this.#commands.splice(next);
+      }
+    }
+
+    this.#commands.push(save);
+  }
+
+  undo() {
+    if (this.#position === -1) {
+      return;
+    }
+
+    this.#locked = true;
+    this.#commands[this.#position].undo();
+    this.#locked = false;
+    this.#position -= 1;
+  }
+
+  redo() {
+    if (this.#position < this.#commands.length - 1) {
+      this.#position += 1;
+      this.#locked = true;
+      this.#commands[this.#position].cmd();
+      this.#locked = false;
+    }
+  }
+
+  hasSomethingToUndo() {
+    return this.#position !== -1;
+  }
+
+  hasSomethingToRedo() {
+    return this.#position < this.#commands.length - 1;
+  }
+
+  destroy() {
+    this.#commands = null;
+  }
+
+}
+
+exports.CommandManager = CommandManager;
+
+class KeyboardManager {
+  constructor(callbacks) {
+    this.buffer = [];
+    this.callbacks = new Map();
+    this.allKeys = new Set();
+    const isMac = KeyboardManager.platform.isMac;
+
+    for (const [keys, callback] of callbacks) {
+      for (const key of keys) {
+        const isMacKey = key.startsWith("mac+");
+
+        if (isMac && isMacKey) {
+          this.callbacks.set(key.slice(4), callback);
+          this.allKeys.add(key.split("+").at(-1));
+        } else if (!isMac && !isMacKey) {
+          this.callbacks.set(key, callback);
+          this.allKeys.add(key.split("+").at(-1));
+        }
+      }
+    }
+  }
+
+  static get platform() {
+    const platform = typeof navigator !== "undefined" ? navigator.platform : "";
+    return (0, _util.shadow)(this, "platform", {
+      isWin: platform.includes("Win"),
+      isMac: platform.includes("Mac")
+    });
+  }
+
+  #serialize(event) {
+    if (event.altKey) {
+      this.buffer.push("alt");
+    }
+
+    if (event.ctrlKey) {
+      this.buffer.push("ctrl");
+    }
+
+    if (event.metaKey) {
+      this.buffer.push("meta");
+    }
+
+    if (event.shiftKey) {
+      this.buffer.push("shift");
+    }
+
+    this.buffer.push(event.key);
+    const str = this.buffer.join("+");
+    this.buffer.length = 0;
+    return str;
+  }
+
+  exec(self, event) {
+    if (!this.allKeys.has(event.key)) {
+      return;
+    }
+
+    const callback = this.callbacks.get(this.#serialize(event));
+
+    if (!callback) {
+      return;
+    }
+
+    callback.bind(self)();
+    event.stopPropagation();
+    event.preventDefault();
+  }
+
+}
+
+exports.KeyboardManager = KeyboardManager;
+
+class ClipboardManager {
+  #elements = null;
+
+  copy(element) {
+    if (!element) {
+      return;
+    }
+
+    if (Array.isArray(element)) {
+      this.#elements = element.map(el => el.serialize());
+    } else {
+      this.#elements = [element.serialize()];
+    }
+
+    this.#elements = this.#elements.filter(el => !!el);
+
+    if (this.#elements.length === 0) {
+      this.#elements = null;
+    }
+  }
+
+  paste() {
+    return this.#elements;
+  }
+
+  isEmpty() {
+    return this.#elements === null;
+  }
+
+  destroy() {
+    this.#elements = null;
+  }
+
+}
+
+class ColorManager {
+  static _colorsMapping = new Map([["CanvasText", [0, 0, 0]], ["Canvas", [255, 255, 255]]]);
+
+  get _colors() {
+    if (typeof document === "undefined") {
+      return (0, _util.shadow)(this, "_colors", ColorManager._colorsMapping);
+    }
+
+    const colors = new Map([["CanvasText", null], ["Canvas", null]]);
+    (0, _display_utils.getColorValues)(colors);
+    return (0, _util.shadow)(this, "_colors", colors);
+  }
+
+  convert(color) {
+    const rgb = (0, _display_utils.getRGB)(color);
+
+    if (!window.matchMedia("(forced-colors: active)").matches) {
+      return rgb;
+    }
+
+    for (const [name, RGB] of this._colors) {
+      if (RGB.every((x, i) => x === rgb[i])) {
+        return ColorManager._colorsMapping.get(name);
+      }
+    }
+
+    return rgb;
+  }
+
+  getHexCode(name) {
+    const rgb = this._colors.get(name);
+
+    if (!rgb) {
+      return name;
+    }
+
+    return _util.Util.makeHexColor(...rgb);
+  }
+
+}
+
+exports.ColorManager = ColorManager;
+
+class AnnotationEditorUIManager {
+  #activeEditor = null;
+  #allEditors = new Map();
+  #allLayers = new Map();
+  #clipboardManager = new ClipboardManager();
+  #commandManager = new CommandManager();
+  #currentPageIndex = 0;
+  #editorTypes = null;
+  #eventBus = null;
+  #idManager = new IdManager();
+  #isEnabled = false;
+  #mode = _util.AnnotationEditorType.NONE;
+  #selectedEditors = new Set();
+  #boundKeydown = this.keydown.bind(this);
+  #boundOnEditingAction = this.onEditingAction.bind(this);
+  #boundOnPageChanging = this.onPageChanging.bind(this);
+  #boundOnTextLayerRendered = this.onTextLayerRendered.bind(this);
+  #previousStates = {
+    isEditing: false,
+    isEmpty: true,
+    hasEmptyClipboard: true,
+    hasSomethingToUndo: false,
+    hasSomethingToRedo: false,
+    hasSelectedEditor: false
+  };
+  #container = null;
+  static _keyboardManager = new KeyboardManager([[["ctrl+a", "mac+meta+a"], AnnotationEditorUIManager.prototype.selectAll], [["ctrl+c", "mac+meta+c"], AnnotationEditorUIManager.prototype.copy], [["ctrl+v", "mac+meta+v"], AnnotationEditorUIManager.prototype.paste], [["ctrl+x", "mac+meta+x"], AnnotationEditorUIManager.prototype.cut], [["ctrl+z", "mac+meta+z"], AnnotationEditorUIManager.prototype.undo], [["ctrl+y", "ctrl+shift+Z", "mac+meta+shift+Z"], AnnotationEditorUIManager.prototype.redo], [["Backspace", "alt+Backspace", "ctrl+Backspace", "shift+Backspace", "mac+Backspace", "mac+alt+Backspace", "mac+ctrl+Backspace", "Delete", "ctrl+Delete", "shift+Delete"], AnnotationEditorUIManager.prototype.delete], [["Escape", "mac+Escape"], AnnotationEditorUIManager.prototype.unselectAll]]);
+
+  constructor(container, eventBus) {
+    this.#container = container;
+    this.#eventBus = eventBus;
+
+    this.#eventBus._on("editingaction", this.#boundOnEditingAction);
+
+    this.#eventBus._on("pagechanging", this.#boundOnPageChanging);
+
+    this.#eventBus._on("textlayerrendered", this.#boundOnTextLayerRendered);
+  }
+
+  destroy() {
+    this.#removeKeyboardManager();
+
+    this.#eventBus._off("editingaction", this.#boundOnEditingAction);
+
+    this.#eventBus._off("pagechanging", this.#boundOnPageChanging);
+
+    this.#eventBus._off("textlayerrendered", this.#boundOnTextLayerRendered);
+
+    for (const layer of this.#allLayers.values()) {
+      layer.destroy();
+    }
+
+    this.#allLayers.clear();
+    this.#allEditors.clear();
+    this.#activeEditor = null;
+    this.#selectedEditors.clear();
+    this.#clipboardManager.destroy();
+    this.#commandManager.destroy();
+  }
+
+  onPageChanging({
+    pageNumber
+  }) {
+    this.#currentPageIndex = pageNumber - 1;
+  }
+
+  onTextLayerRendered({
+    pageNumber
+  }) {
+    const pageIndex = pageNumber - 1;
+    const layer = this.#allLayers.get(pageIndex);
+    layer?.onTextLayerRendered();
+  }
+
+  focusMainContainer() {
+    this.#container.focus();
+  }
+
+  #addKeyboardManager() {
+    this.#container.addEventListener("keydown", this.#boundKeydown);
+  }
+
+  #removeKeyboardManager() {
+    this.#container.removeEventListener("keydown", this.#boundKeydown);
+  }
+
+  keydown(event) {
+    if (!this.getActive()?.shouldGetKeyboardEvents()) {
+      AnnotationEditorUIManager._keyboardManager.exec(this, event);
+    }
+  }
+
+  onEditingAction(details) {
+    if (["undo", "redo", "cut", "copy", "paste", "delete", "selectAll"].includes(details.name)) {
+      this[details.name]();
+    }
+  }
+
+  #dispatchUpdateStates(details) {
+    const hasChanged = Object.entries(details).some(([key, value]) => this.#previousStates[key] !== value);
+
+    if (hasChanged) {
+      this.#eventBus.dispatch("annotationeditorstateschanged", {
+        source: this,
+        details: Object.assign(this.#previousStates, details)
+      });
+    }
+  }
+
+  #dispatchUpdateUI(details) {
+    this.#eventBus.dispatch("annotationeditorparamschanged", {
+      source: this,
+      details
+    });
+  }
+
+  setEditingState(isEditing) {
+    if (isEditing) {
+      this.#addKeyboardManager();
+      this.#dispatchUpdateStates({
+        isEditing: this.#mode !== _util.AnnotationEditorType.NONE,
+        isEmpty: this.#isEmpty(),
+        hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(),
+        hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(),
+        hasSelectedEditor: false,
+        hasEmptyClipboard: this.#clipboardManager.isEmpty()
+      });
+    } else {
+      this.#removeKeyboardManager();
+      this.#dispatchUpdateStates({
+        isEditing: false
+      });
+    }
+  }
+
+  registerEditorTypes(types) {
+    this.#editorTypes = types;
+
+    for (const editorType of this.#editorTypes) {
+      this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate);
+    }
+  }
+
+  getId() {
+    return this.#idManager.getId();
+  }
+
+  addLayer(layer) {
+    this.#allLayers.set(layer.pageIndex, layer);
+
+    if (this.#isEnabled) {
+      layer.enable();
+    } else {
+      layer.disable();
+    }
+  }
+
+  removeLayer(layer) {
+    this.#allLayers.delete(layer.pageIndex);
+  }
+
+  updateMode(mode) {
+    this.#mode = mode;
+
+    if (mode === _util.AnnotationEditorType.NONE) {
+      this.setEditingState(false);
+      this.#disableAll();
+    } else {
+      this.setEditingState(true);
+      this.#enableAll();
+
+      for (const layer of this.#allLayers.values()) {
+        layer.updateMode(mode);
+      }
+    }
+  }
+
+  updateToolbar(mode) {
+    if (mode === this.#mode) {
+      return;
+    }
+
+    this.#eventBus.dispatch("switchannotationeditormode", {
+      source: this,
+      mode
+    });
+  }
+
+  updateParams(type, value) {
+    for (const editor of this.#selectedEditors) {
+      editor.updateParams(type, value);
+    }
+
+    for (const editorType of this.#editorTypes) {
+      editorType.updateDefaultParams(type, value);
+    }
+  }
+
+  #enableAll() {
+    if (!this.#isEnabled) {
+      this.#isEnabled = true;
+
+      for (const layer of this.#allLayers.values()) {
+        layer.enable();
+      }
+    }
+  }
+
+  #disableAll() {
+    this.unselectAll();
+
+    if (this.#isEnabled) {
+      this.#isEnabled = false;
+
+      for (const layer of this.#allLayers.values()) {
+        layer.disable();
+      }
+    }
+  }
+
+  getEditors(pageIndex) {
+    const editors = [];
+
+    for (const editor of this.#allEditors.values()) {
+      if (editor.pageIndex === pageIndex) {
+        editors.push(editor);
+      }
+    }
+
+    return editors;
+  }
+
+  getEditor(id) {
+    return this.#allEditors.get(id);
+  }
+
+  addEditor(editor) {
+    this.#allEditors.set(editor.id, editor);
+  }
+
+  removeEditor(editor) {
+    this.#allEditors.delete(editor.id);
+    this.unselect(editor);
+  }
+
+  #addEditorToLayer(editor) {
+    const layer = this.#allLayers.get(editor.pageIndex);
+
+    if (layer) {
+      layer.addOrRebuild(editor);
+    } else {
+      this.addEditor(editor);
+    }
+  }
+
+  setActiveEditor(editor) {
+    if (this.#activeEditor === editor) {
+      return;
+    }
+
+    this.#activeEditor = editor;
+
+    if (editor) {
+      this.#dispatchUpdateUI(editor.propertiesToUpdate);
+    }
+  }
+
+  toggleSelected(editor) {
+    if (this.#selectedEditors.has(editor)) {
+      this.#selectedEditors.delete(editor);
+      editor.unselect();
+      this.#dispatchUpdateStates({
+        hasSelectedEditor: this.hasSelection
+      });
+      return;
+    }
+
+    this.#selectedEditors.add(editor);
+    editor.select();
+    this.#dispatchUpdateUI(editor.propertiesToUpdate);
+    this.#dispatchUpdateStates({
+      hasSelectedEditor: true
+    });
+  }
+
+  setSelected(editor) {
+    for (const ed of this.#selectedEditors) {
+      if (ed !== editor) {
+        ed.unselect();
+      }
+    }
+
+    this.#selectedEditors.clear();
+    this.#selectedEditors.add(editor);
+    editor.select();
+    this.#dispatchUpdateUI(editor.propertiesToUpdate);
+    this.#dispatchUpdateStates({
+      hasSelectedEditor: true
+    });
+  }
+
+  isSelected(editor) {
+    return this.#selectedEditors.has(editor);
+  }
+
+  unselect(editor) {
+    editor.unselect();
+    this.#selectedEditors.delete(editor);
+    this.#dispatchUpdateStates({
+      hasSelectedEditor: this.hasSelection
+    });
+  }
+
+  get hasSelection() {
+    return this.#selectedEditors.size !== 0;
+  }
+
+  undo() {
+    this.#commandManager.undo();
+    this.#dispatchUpdateStates({
+      hasSomethingToUndo: this.#commandManager.hasSomethingToUndo(),
+      hasSomethingToRedo: true,
+      isEmpty: this.#isEmpty()
+    });
+  }
+
+  redo() {
+    this.#commandManager.redo();
+    this.#dispatchUpdateStates({
+      hasSomethingToUndo: true,
+      hasSomethingToRedo: this.#commandManager.hasSomethingToRedo(),
+      isEmpty: this.#isEmpty()
+    });
+  }
+
+  addCommands(params) {
+    this.#commandManager.add(params);
+    this.#dispatchUpdateStates({
+      hasSomethingToUndo: true,
+      hasSomethingToRedo: false,
+      isEmpty: this.#isEmpty()
+    });
+  }
+
+  #isEmpty() {
+    if (this.#allEditors.size === 0) {
+      return true;
+    }
+
+    if (this.#allEditors.size === 1) {
+      for (const editor of this.#allEditors.values()) {
+        return editor.isEmpty();
+      }
+    }
+
+    return false;
+  }
+
+  delete() {
+    if (this.#activeEditor) {
+      this.#activeEditor.commitOrRemove();
+    }
+
+    if (!this.hasSelection) {
+      return;
+    }
+
+    const editors = [...this.#selectedEditors];
+
+    const cmd = () => {
+      for (const editor of editors) {
+        editor.remove();
+      }
+    };
+
+    const undo = () => {
+      for (const editor of editors) {
+        this.#addEditorToLayer(editor);
+      }
+    };
+
+    this.addCommands({
+      cmd,
+      undo,
+      mustExec: true
+    });
+  }
+
+  copy() {
+    if (this.#activeEditor) {
+      this.#activeEditor.commitOrRemove();
+    }
+
+    if (this.hasSelection) {
+      const editors = [];
+
+      for (const editor of this.#selectedEditors) {
+        if (!editor.isEmpty()) {
+          editors.push(editor);
+        }
+      }
+
+      if (editors.length === 0) {
+        return;
+      }
+
+      this.#clipboardManager.copy(editors);
+      this.#dispatchUpdateStates({
+        hasEmptyClipboard: false
+      });
+    }
+  }
+
+  cut() {
+    this.copy();
+    this.delete();
+  }
+
+  paste() {
+    if (this.#clipboardManager.isEmpty()) {
+      return;
+    }
+
+    this.unselectAll();
+    const layer = this.#allLayers.get(this.#currentPageIndex);
+    const newEditors = this.#clipboardManager.paste().map(data => layer.deserialize(data));
+
+    const cmd = () => {
+      for (const editor of newEditors) {
+        this.#addEditorToLayer(editor);
+      }
+
+      this.#selectEditors(newEditors);
+    };
+
+    const undo = () => {
+      for (const editor of newEditors) {
+        editor.remove();
+      }
+    };
+
+    this.addCommands({
+      cmd,
+      undo,
+      mustExec: true
+    });
+  }
+
+  #selectEditors(editors) {
+    this.#selectedEditors.clear();
+
+    for (const editor of editors) {
+      if (editor.isEmpty()) {
+        continue;
+      }
+
+      this.#selectedEditors.add(editor);
+      editor.select();
+    }
+
+    this.#dispatchUpdateStates({
+      hasSelectedEditor: true
+    });
+  }
+
+  selectAll() {
+    for (const editor of this.#selectedEditors) {
+      editor.commit();
+    }
+
+    this.#selectEditors(this.#allEditors.values());
+  }
+
+  unselectAll() {
+    if (this.#activeEditor) {
+      this.#activeEditor.commitOrRemove();
+      return;
+    }
+
+    if (this.#selectEditors.size === 0) {
+      return;
+    }
+
+    for (const editor of this.#selectedEditors) {
+      editor.unselect();
+    }
+
+    this.#selectedEditors.clear();
+    this.#dispatchUpdateStates({
+      hasSelectedEditor: false
+    });
+  }
+
+  isActive(editor) {
+    return this.#activeEditor === editor;
+  }
+
+  getActive() {
+    return this.#activeEditor;
+  }
+
+  getMode() {
+    return this.#mode;
+  }
+
+}
+
+exports.AnnotationEditorUIManager = AnnotationEditorUIManager;

+ 3 - 3
lib/display/font_loader.js

@@ -59,7 +59,7 @@ class BaseFontLoader {
       styleElement = this.styleElement = this._document.createElement("style");
       styleElement.id = `PDFJS_FONT_STYLE_TAG_${this.docId}`;
 
-      this._document.documentElement.getElementsByTagName("head")[0].appendChild(styleElement);
+      this._document.documentElement.getElementsByTagName("head")[0].append(styleElement);
     }
 
     const styleSheet = styleElement.sheet;
@@ -288,10 +288,10 @@ exports.FontLoader = FontLoader;
 
         span.textContent = "Hi";
         span.style.fontFamily = name;
-        div.appendChild(span);
+        div.append(span);
       }
 
-      this._document.body.appendChild(div);
+      this._document.body.append(div);
 
       isFontReady(loadTestFontId, () => {
         div.remove();

+ 75 - 33
lib/display/optional_content_config.js

@@ -28,21 +28,39 @@ exports.OptionalContentConfig = void 0;
 
 var _util = require("../shared/util.js");
 
+const INTERNAL = Symbol("INTERNAL");
+
 class OptionalContentGroup {
+  #visible = true;
+
   constructor(name, intent) {
-    this.visible = true;
     this.name = name;
     this.intent = intent;
   }
 
+  get visible() {
+    return this.#visible;
+  }
+
+  _setVisible(internal, visible) {
+    if (internal !== INTERNAL) {
+      (0, _util.unreachable)("Internal method `_setVisible` called.");
+    }
+
+    this.#visible = visible;
+  }
+
 }
 
 class OptionalContentConfig {
+  #cachedHasInitialVisibility = true;
+  #groups = new Map();
+  #initialVisibility = null;
+  #order = null;
+
   constructor(data) {
     this.name = null;
     this.creator = null;
-    this._order = null;
-    this._groups = new Map();
 
     if (data === null) {
       return;
@@ -50,28 +68,34 @@ class OptionalContentConfig {
 
     this.name = data.name;
     this.creator = data.creator;
-    this._order = data.order;
+    this.#order = data.order;
 
     for (const group of data.groups) {
-      this._groups.set(group.id, new OptionalContentGroup(group.name, group.intent));
+      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 group of this.#groups.values()) {
+        group._setVisible(INTERNAL, false);
       }
     }
 
     for (const on of data.on) {
-      this._groups.get(on).visible = true;
+      this.#groups.get(on)._setVisible(INTERNAL, true);
     }
 
     for (const off of data.off) {
-      this._groups.get(off).visible = false;
+      this.#groups.get(off)._setVisible(INTERNAL, false);
+    }
+
+    this.#initialVisibility = new Map();
+
+    for (const [id, group] of this.#groups) {
+      this.#initialVisibility.set(id, group.visible);
     }
   }
 
-  _evaluateVisibilityExpression(array) {
+  #evaluateVisibilityExpression(array) {
     const length = array.length;
 
     if (length < 2) {
@@ -85,9 +109,9 @@ class OptionalContentConfig {
       let state;
 
       if (Array.isArray(element)) {
-        state = this._evaluateVisibilityExpression(element);
-      } else if (this._groups.has(element)) {
-        state = this._groups.get(element).visible;
+        state = this.#evaluateVisibilityExpression(element);
+      } else if (this.#groups.has(element)) {
+        state = this.#groups.get(element).visible;
       } else {
         (0, _util.warn)(`Optional content group not found: ${element}`);
         return true;
@@ -120,7 +144,7 @@ class OptionalContentConfig {
   }
 
   isVisible(group) {
-    if (this._groups.size === 0) {
+    if (this.#groups.size === 0) {
       return true;
     }
 
@@ -130,25 +154,25 @@ class OptionalContentConfig {
     }
 
     if (group.type === "OCG") {
-      if (!this._groups.has(group.id)) {
+      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;
+      return this.#groups.get(group.id).visible;
     } else if (group.type === "OCMD") {
       if (group.expression) {
-        return this._evaluateVisibilityExpression(group.expression);
+        return this.#evaluateVisibilityExpression(group.expression);
       }
 
       if (!group.policy || group.policy === "AnyOn") {
         for (const id of group.ids) {
-          if (!this._groups.has(id)) {
+          if (!this.#groups.has(id)) {
             (0, _util.warn)(`Optional content group not found: ${id}`);
             return true;
           }
 
-          if (this._groups.get(id).visible) {
+          if (this.#groups.get(id).visible) {
             return true;
           }
         }
@@ -156,12 +180,12 @@ class OptionalContentConfig {
         return false;
       } else if (group.policy === "AllOn") {
         for (const id of group.ids) {
-          if (!this._groups.has(id)) {
+          if (!this.#groups.has(id)) {
             (0, _util.warn)(`Optional content group not found: ${id}`);
             return true;
           }
 
-          if (!this._groups.get(id).visible) {
+          if (!this.#groups.get(id).visible) {
             return false;
           }
         }
@@ -169,12 +193,12 @@ class OptionalContentConfig {
         return true;
       } else if (group.policy === "AnyOff") {
         for (const id of group.ids) {
-          if (!this._groups.has(id)) {
+          if (!this.#groups.has(id)) {
             (0, _util.warn)(`Optional content group not found: ${id}`);
             return true;
           }
 
-          if (!this._groups.get(id).visible) {
+          if (!this.#groups.get(id).visible) {
             return true;
           }
         }
@@ -182,12 +206,12 @@ class OptionalContentConfig {
         return false;
       } else if (group.policy === "AllOff") {
         for (const id of group.ids) {
-          if (!this._groups.has(id)) {
+          if (!this.#groups.has(id)) {
             (0, _util.warn)(`Optional content group not found: ${id}`);
             return true;
           }
 
-          if (this._groups.get(id).visible) {
+          if (this.#groups.get(id).visible) {
             return false;
           }
         }
@@ -204,32 +228,50 @@ class OptionalContentConfig {
   }
 
   setVisibility(id, visible = true) {
-    if (!this._groups.has(id)) {
+    if (!this.#groups.has(id)) {
       (0, _util.warn)(`Optional content group not found: ${id}`);
       return;
     }
 
-    this._groups.get(id).visible = !!visible;
+    this.#groups.get(id)._setVisible(INTERNAL, !!visible);
+
+    this.#cachedHasInitialVisibility = null;
+  }
+
+  get hasInitialVisibility() {
+    if (this.#cachedHasInitialVisibility !== null) {
+      return this.#cachedHasInitialVisibility;
+    }
+
+    for (const [id, group] of this.#groups) {
+      const visible = this.#initialVisibility.get(id);
+
+      if (group.visible !== visible) {
+        return this.#cachedHasInitialVisibility = false;
+      }
+    }
+
+    return this.#cachedHasInitialVisibility = true;
   }
 
   getOrder() {
-    if (!this._groups.size) {
+    if (!this.#groups.size) {
       return null;
     }
 
-    if (this._order) {
-      return this._order.slice();
+    if (this.#order) {
+      return this.#order.slice();
     }
 
-    return Array.from(this._groups.keys());
+    return [...this.#groups.keys()];
   }
 
   getGroups() {
-    return this._groups.size > 0 ? (0, _util.objectFromMap)(this._groups) : null;
+    return this.#groups.size > 0 ? (0, _util.objectFromMap)(this.#groups) : null;
   }
 
   getGroup(id) {
-    return this._groups.get(id) || null;
+    return this.#groups.get(id) || null;
   }
 
 }

+ 27 - 26
lib/display/svg.js

@@ -26,10 +26,10 @@ Object.defineProperty(exports, "__esModule", {
 });
 exports.SVGGraphics = void 0;
 
-var _util = require("../shared/util.js");
-
 var _display_utils = require("./display_utils.js");
 
+var _util = require("../shared/util.js");
+
 var _is_node = require("../shared/is_node.js");
 
 let SVGGraphics = class {
@@ -332,7 +332,7 @@ exports.SVGGraphics = SVGGraphics;
           items: []
         });
         tmp.push(opTree);
-        opTree = opTree[opTree.length - 1].items;
+        opTree = opTree.at(-1).items;
         continue;
       }
 
@@ -393,6 +393,7 @@ exports.SVGGraphics = SVGGraphics;
   let shadingCount = 0;
   exports.SVGGraphics = SVGGraphics = class {
     constructor(commonObjs, objs, forceDataSchema = false) {
+      (0, _display_utils.deprecated)("The SVG back-end is no longer maintained and *may* be removed in the future.");
       this.svgFactory = new _display_utils.DOMSVGFactory();
       this.current = new SVGExtraState();
       this.transformMatrix = _util.IDENTITY_MATRIX;
@@ -728,7 +729,7 @@ exports.SVGGraphics = SVGGraphics;
       current.tspan.setAttributeNS(null, "font-size", `${pf(current.fontSize)}px`);
       current.tspan.setAttributeNS(null, "y", pf(-current.y));
       current.txtElement = this.svgFactory.createElement("svg:text");
-      current.txtElement.appendChild(current.tspan);
+      current.txtElement.append(current.tspan);
     }
 
     beginText() {
@@ -882,10 +883,10 @@ exports.SVGGraphics = SVGGraphics;
 
       current.txtElement.setAttributeNS(null, "transform", `${pm(textMatrix)} scale(${pf(textHScale)}, -1)`);
       current.txtElement.setAttributeNS(XML_NS, "xml:space", "preserve");
-      current.txtElement.appendChild(current.tspan);
-      current.txtgrp.appendChild(current.txtElement);
+      current.txtElement.append(current.tspan);
+      current.txtgrp.append(current.txtElement);
 
-      this._ensureTransformGroup().appendChild(current.txtElement);
+      this._ensureTransformGroup().append(current.txtElement);
     }
 
     setLeadingMoveText(x, y) {
@@ -901,7 +902,7 @@ exports.SVGGraphics = SVGGraphics;
       if (!this.cssStyle) {
         this.cssStyle = this.svgFactory.createElement("svg:style");
         this.cssStyle.setAttributeNS(null, "type", "text/css");
-        this.defs.appendChild(this.cssStyle);
+        this.defs.append(this.cssStyle);
       }
 
       const url = createObjectURL(fontObj.data, fontObj.mimetype, this.forceDataSchema);
@@ -1031,7 +1032,7 @@ exports.SVGGraphics = SVGGraphics;
         rect.setAttributeNS(null, "fill-opacity", this.current.fillAlpha);
       }
 
-      this._ensureTransformGroup().appendChild(rect);
+      this._ensureTransformGroup().append(rect);
     }
 
     _makeColorN_Pattern(args) {
@@ -1085,8 +1086,8 @@ exports.SVGGraphics = SVGGraphics;
       this.transformMatrix = transformMatrix;
       this.current.fillColor = fillColor;
       this.current.strokeColor = strokeColor;
-      tiling.appendChild(bbox.childNodes[0]);
-      this.defs.appendChild(tiling);
+      tiling.append(bbox.childNodes[0]);
+      this.defs.append(tiling);
       return `url(#${tilingId})`;
     }
 
@@ -1138,10 +1139,10 @@ exports.SVGGraphics = SVGGraphics;
             const stop = this.svgFactory.createElement("svg:stop");
             stop.setAttributeNS(null, "offset", colorStop[0]);
             stop.setAttributeNS(null, "stop-color", colorStop[1]);
-            gradient.appendChild(stop);
+            gradient.append(stop);
           }
 
-          this.defs.appendChild(gradient);
+          this.defs.append(gradient);
           return `url(#${shadingId})`;
 
         case "Mesh":
@@ -1226,7 +1227,7 @@ exports.SVGGraphics = SVGGraphics;
       } else {
         current.path = this.svgFactory.createElement("svg:path");
 
-        this._ensureTransformGroup().appendChild(current.path);
+        this._ensureTransformGroup().append(current.path);
       }
 
       current.path.setAttributeNS(null, "d", d);
@@ -1261,8 +1262,8 @@ exports.SVGGraphics = SVGGraphics;
       }
 
       this.pendingClip = null;
-      clipPath.appendChild(clipElement);
-      this.defs.appendChild(clipPath);
+      clipPath.append(clipElement);
+      this.defs.append(clipPath);
 
       if (current.activeClipUrl) {
         current.clipGroup = null;
@@ -1446,7 +1447,7 @@ exports.SVGGraphics = SVGGraphics;
       rect.setAttributeNS(null, "height", "1px");
       rect.setAttributeNS(null, "fill", this.current.fillColor);
 
-      this._ensureTransformGroup().appendChild(rect);
+      this._ensureTransformGroup().append(rect);
     }
 
     paintImageXObject(objId) {
@@ -1480,9 +1481,9 @@ exports.SVGGraphics = SVGGraphics;
       imgEl.setAttributeNS(null, "transform", `scale(${pf(1 / width)} ${pf(-1 / height)})`);
 
       if (mask) {
-        mask.appendChild(imgEl);
+        mask.append(imgEl);
       } else {
-        this._ensureTransformGroup().appendChild(imgEl);
+        this._ensureTransformGroup().append(imgEl);
       }
     }
 
@@ -1501,9 +1502,9 @@ exports.SVGGraphics = SVGGraphics;
       rect.setAttributeNS(null, "height", pf(height));
       rect.setAttributeNS(null, "fill", fillColor);
       rect.setAttributeNS(null, "mask", `url(#${current.maskId})`);
-      this.defs.appendChild(mask);
+      this.defs.append(mask);
 
-      this._ensureTransformGroup().appendChild(rect);
+      this._ensureTransformGroup().append(rect);
 
       this.paintInlineImageXObject(imgData, mask);
     }
@@ -1532,11 +1533,11 @@ exports.SVGGraphics = SVGGraphics;
     _initialize(viewport) {
       const svg = this.svgFactory.create(viewport.width, viewport.height);
       const definitions = this.svgFactory.createElement("svg:defs");
-      svg.appendChild(definitions);
+      svg.append(definitions);
       this.defs = definitions;
       const rootGroup = this.svgFactory.createElement("svg:g");
       rootGroup.setAttributeNS(null, "transform", pm(viewport.transform));
-      svg.appendChild(rootGroup);
+      svg.append(rootGroup);
       this.svg = rootGroup;
       return svg;
     }
@@ -1545,7 +1546,7 @@ exports.SVGGraphics = SVGGraphics;
       if (!this.current.clipGroup) {
         const clipGroup = this.svgFactory.createElement("svg:g");
         clipGroup.setAttributeNS(null, "clip-path", this.current.activeClipUrl);
-        this.svg.appendChild(clipGroup);
+        this.svg.append(clipGroup);
         this.current.clipGroup = clipGroup;
       }
 
@@ -1558,9 +1559,9 @@ exports.SVGGraphics = SVGGraphics;
         this.tgrp.setAttributeNS(null, "transform", pm(this.transformMatrix));
 
         if (this.current.activeClipUrl) {
-          this._ensureClipGroup().appendChild(this.tgrp);
+          this._ensureClipGroup().append(this.tgrp);
         } else {
-          this.svg.appendChild(this.tgrp);
+          this.svg.append(this.tgrp);
         }
       }
 

+ 26 - 11
lib/display/text_layer.js

@@ -24,10 +24,13 @@
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
+exports.TextLayerRenderTask = void 0;
 exports.renderTextLayer = renderTextLayer;
 
 var _util = require("../shared/util.js");
 
+var _display_utils = require("./display_utils.js");
+
 const MAX_TEXT_DIVS_TO_RENDER = 100000;
 const DEFAULT_FONT_SIZE = 30;
 const DEFAULT_FONT_ASCENT = 0.8;
@@ -103,12 +106,14 @@ function appendText(task, geom, styles, ctx) {
     paddingLeft: 0,
     paddingRight: 0,
     paddingTop: 0,
-    scale: 1
+    scale: 1,
+    fontSize: 0
   } : {
     angle: 0,
     canvasWidth: 0,
     hasText: geom.str !== "",
-    hasEOL: geom.hasEOL
+    hasEOL: geom.hasEOL,
+    fontSize: 0
   };
 
   task._textDivs.push(textDiv);
@@ -138,6 +143,7 @@ function appendText(task, geom, styles, ctx) {
   textDiv.style.top = `${top}px`;
   textDiv.style.fontSize = `${fontHeight}px`;
   textDiv.style.fontFamily = style.fontFamily;
+  textDivProperties.fontSize = fontHeight;
   textDiv.setAttribute("role", "presentation");
   textDiv.textContent = geom.str;
   textDiv.dir = geom.dir;
@@ -438,7 +444,7 @@ function expandBoundsLTR(width, bounds) {
       const useBoundary = affectedBoundary.x2 > boundary.x2 ? affectedBoundary : boundary;
 
       if (lastBoundary === useBoundary) {
-        changedHorizon[changedHorizon.length - 1].end = horizonPart.end;
+        changedHorizon.at(-1).end = horizonPart.end;
       } else {
         changedHorizon.push({
           start: horizonPart.start,
@@ -459,7 +465,7 @@ function expandBoundsLTR(width, bounds) {
     }
 
     if (boundary.y2 < horizon[j].end) {
-      changedHorizon[changedHorizon.length - 1].end = boundary.y2;
+      changedHorizon.at(-1).end = boundary.y2;
       changedHorizon.push({
         start: boundary.y2,
         end: horizon[j].end,
@@ -494,7 +500,7 @@ function expandBoundsLTR(width, bounds) {
       }
     }
 
-    Array.prototype.splice.apply(horizon, [i, j - i + 1].concat(changedHorizon));
+    Array.prototype.splice.apply(horizon, [i, j - i + 1, ...changedHorizon]);
   }
 
   for (const horizonPart of horizon) {
@@ -516,6 +522,10 @@ class TextLayerRenderTask {
     textContentItemsStr,
     enhanceTextSelection
   }) {
+    if (enhanceTextSelection) {
+      (0, _display_utils.deprecated)("The `enhanceTextSelection` functionality will be removed in the future.");
+    }
+
     this._textContent = textContent;
     this._textContentStream = textContentStream;
     this._container = container;
@@ -535,6 +545,7 @@ class TextLayerRenderTask {
     this._capability = (0, _util.createPromiseCapability)();
     this._renderTimer = null;
     this._bounds = [];
+    this._devicePixelRatio = globalThis.devicePixelRatio || 1;
 
     this._capability.promise.finally(() => {
       if (!this._enhanceTextSelection) {
@@ -583,7 +594,7 @@ class TextLayerRenderTask {
             this._container.setAttribute("id", `${items[i].id}`);
           }
 
-          parent.appendChild(this._container);
+          parent.append(this._container);
         } else if (items[i].type === "endMarkedContent") {
           this._container = this._container.parentNode;
         }
@@ -604,12 +615,14 @@ class TextLayerRenderTask {
 
     if (textDivProperties.canvasWidth !== 0 && textDivProperties.hasText) {
       const {
-        fontSize,
         fontFamily
       } = textDiv.style;
+      const {
+        fontSize
+      } = textDivProperties;
 
       if (fontSize !== this._layoutTextLastFontSize || fontFamily !== this._layoutTextLastFontFamily) {
-        this._layoutTextCtx.font = `${fontSize} ${fontFamily}`;
+        this._layoutTextCtx.font = `${fontSize * this._devicePixelRatio}px ${fontFamily}`;
         this._layoutTextLastFontSize = fontSize;
         this._layoutTextLastFontFamily = fontFamily;
       }
@@ -619,7 +632,7 @@ class TextLayerRenderTask {
       } = this._layoutTextCtx.measureText(textDiv.textContent);
 
       if (width > 0) {
-        const scale = textDivProperties.canvasWidth / width;
+        const scale = this._devicePixelRatio * textDivProperties.canvasWidth / width;
 
         if (this._enhanceTextSelection) {
           textDivProperties.scale = scale;
@@ -642,14 +655,14 @@ class TextLayerRenderTask {
     }
 
     if (textDivProperties.hasText) {
-      this._container.appendChild(textDiv);
+      this._container.append(textDiv);
     }
 
     if (textDivProperties.hasEOL) {
       const br = document.createElement("br");
       br.setAttribute("role", "presentation");
 
-      this._container.appendChild(br);
+      this._container.append(br);
     }
   }
 
@@ -780,6 +793,8 @@ class TextLayerRenderTask {
 
 }
 
+exports.TextLayerRenderTask = TextLayerRenderTask;
+
 function renderTextLayer(renderParameters) {
   const task = new TextLayerRenderTask({
     textContent: renderParameters.textContent,

+ 30 - 18
lib/display/xfa_layer.js

@@ -123,26 +123,38 @@ class XfaLayer {
     }
 
     for (const [key, value] of Object.entries(attributes)) {
-      if (value === null || value === undefined || key === "dataId") {
+      if (value === null || value === undefined) {
         continue;
       }
 
-      if (key !== "style") {
-        if (key === "textContent") {
-          html.textContent = value;
-        } else if (key === "class") {
+      switch (key) {
+        case "class":
           if (value.length) {
             html.setAttribute(key, value.join(" "));
           }
-        } else {
-          if (isHTMLAnchorElement && (key === "href" || key === "newWindow")) {
-            continue;
+
+          break;
+
+        case "dataId":
+          break;
+
+        case "id":
+          html.setAttribute("data-element-id", value);
+          break;
+
+        case "style":
+          Object.assign(html.style, value);
+          break;
+
+        case "textContent":
+          html.textContent = value;
+          break;
+
+        default:
+          if (!isHTMLAnchorElement || key !== "href" && key !== "newWindow") {
+            html.setAttribute(key, value);
           }
 
-          html.setAttribute(key, value);
-        }
-      } else {
-        Object.assign(html.style, value);
       }
     }
 
@@ -173,7 +185,7 @@ class XfaLayer {
 
     const stack = [[root, -1, rootHtml]];
     const rootDiv = parameters.div;
-    rootDiv.appendChild(rootHtml);
+    rootDiv.append(rootHtml);
 
     if (parameters.viewport) {
       const transform = `matrix(${parameters.viewport.transform.join(",")})`;
@@ -187,14 +199,14 @@ class XfaLayer {
     const textDivs = [];
 
     while (stack.length > 0) {
-      const [parent, i, html] = stack[stack.length - 1];
+      const [parent, i, html] = stack.at(-1);
 
       if (i + 1 === parent.children.length) {
         stack.pop();
         continue;
       }
 
-      const child = parent.children[++stack[stack.length - 1][1]];
+      const child = parent.children[++stack.at(-1)[1]];
 
       if (child === null) {
         continue;
@@ -207,7 +219,7 @@ class XfaLayer {
       if (name === "#text") {
         const node = document.createTextNode(child.value);
         textDivs.push(node);
-        html.appendChild(node);
+        html.append(node);
         continue;
       }
 
@@ -219,7 +231,7 @@ class XfaLayer {
         childHtml = document.createElement(name);
       }
 
-      html.appendChild(childHtml);
+      html.append(childHtml);
 
       if (child.attributes) {
         this.setAttributes({
@@ -240,7 +252,7 @@ class XfaLayer {
           textDivs.push(node);
         }
 
-        childHtml.appendChild(node);
+        childHtml.append(node);
       }
     }
 

+ 9 - 0
lib/examples/node/domstubs.js

@@ -122,6 +122,15 @@ DOMElement.prototype = {
   setAttributeNS: function DOMElement_setAttributeNS(NS, name, value) {
     this.setAttribute(name, value);
   },
+  append: function DOMElement_append(...elements) {
+    const childNodes = this.childNodes;
+
+    for (const element of elements) {
+      if (!childNodes.includes(element)) {
+        childNodes.push(element);
+      }
+    }
+  },
   appendChild: function DOMElement_appendChild(element) {
     const childNodes = this.childNodes;
 

+ 37 - 3
lib/pdf.js

@@ -24,6 +24,30 @@
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
+Object.defineProperty(exports, "AnnotationEditorLayer", {
+  enumerable: true,
+  get: function () {
+    return _annotation_editor_layer.AnnotationEditorLayer;
+  }
+});
+Object.defineProperty(exports, "AnnotationEditorParamsType", {
+  enumerable: true,
+  get: function () {
+    return _util.AnnotationEditorParamsType;
+  }
+});
+Object.defineProperty(exports, "AnnotationEditorType", {
+  enumerable: true,
+  get: function () {
+    return _util.AnnotationEditorType;
+  }
+});
+Object.defineProperty(exports, "AnnotationEditorUIManager", {
+  enumerable: true,
+  get: function () {
+    return _tools.AnnotationEditorUIManager;
+  }
+});
 Object.defineProperty(exports, "AnnotationLayer", {
   enumerable: true,
   get: function () {
@@ -150,6 +174,12 @@ Object.defineProperty(exports, "XfaLayer", {
     return _xfa_layer.XfaLayer;
   }
 });
+Object.defineProperty(exports, "binarySearchFirstItem", {
+  enumerable: true,
+  get: function () {
+    return _display_utils.binarySearchFirstItem;
+  }
+});
 Object.defineProperty(exports, "build", {
   enumerable: true,
   get: function () {
@@ -225,9 +255,13 @@ Object.defineProperty(exports, "version", {
 
 var _util = require("./shared/util.js");
 
+var _display_utils = require("./display/display_utils.js");
+
 var _api = require("./display/api.js");
 
-var _display_utils = require("./display/display_utils.js");
+var _annotation_editor_layer = require("./display/editor/annotation_editor_layer.js");
+
+var _tools = require("./display/editor/tools.js");
 
 var _annotation_layer = require("./display/annotation_layer.js");
 
@@ -241,8 +275,8 @@ var _svg = require("./display/svg.js");
 
 var _xfa_layer = require("./display/xfa_layer.js");
 
-const pdfjsVersion = '2.14.305';
-const pdfjsBuild = 'eaaa8b4ad';
+const pdfjsVersion = '2.15.349';
+const pdfjsBuild = 'b8aa9c622';
 {
   if (_is_node.isNodeJS) {
     const {

Diferenças do arquivo suprimidas por serem muito extensas
+ 3 - 3
lib/pdf.sandbox.js


+ 2 - 2
lib/pdf.worker.js

@@ -33,5 +33,5 @@ Object.defineProperty(exports, "WorkerMessageHandler", {
 
 var _worker = require("./core/worker.js");
 
-const pdfjsVersion = '2.14.305';
-const pdfjsBuild = 'eaaa8b4ad';
+const pdfjsVersion = '2.15.349';
+const pdfjsBuild = 'b8aa9c622';

+ 2 - 1
lib/shared/scripting_utils.js

@@ -68,7 +68,8 @@ class ColorConverters {
   }
 
   static CMYK_HTML(components) {
-    return this.RGB_HTML(this.CMYK_RGB(components));
+    const rgb = this.CMYK_RGB(components).slice(1);
+    return this.RGB_HTML(rgb);
   }
 
   static RGB_CMYK([r, g, b]) {

+ 23 - 1
lib/shared/util.js

@@ -24,7 +24,7 @@
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-exports.VerbosityLevel = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.UNSUPPORTED_FEATURES = exports.TextRenderingMode = exports.StreamType = exports.RenderingIntentFlag = exports.PermissionFlag = exports.PasswordResponses = exports.PasswordException = exports.PageActionEventType = exports.OPS = exports.MissingPDFException = exports.InvalidPDFException = exports.ImageKind = exports.IDENTITY_MATRIX = exports.FormatError = exports.FontType = exports.FeatureTest = exports.FONT_IDENTITY_MATRIX = exports.DocumentActionEventType = exports.CMapCompressionType = exports.BaseException = exports.AnnotationType = exports.AnnotationStateModelType = exports.AnnotationReviewState = exports.AnnotationReplyType = exports.AnnotationMode = exports.AnnotationMarkedState = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationBorderStyleType = exports.AnnotationActionEventType = exports.AbortException = void 0;
+exports.VerbosityLevel = exports.Util = exports.UnknownErrorException = exports.UnexpectedResponseException = exports.UNSUPPORTED_FEATURES = exports.TextRenderingMode = exports.StreamType = exports.RenderingIntentFlag = exports.PermissionFlag = exports.PasswordResponses = exports.PasswordException = exports.PageActionEventType = exports.OPS = exports.MissingPDFException = exports.LINE_FACTOR = exports.LINE_DESCENT_FACTOR = exports.InvalidPDFException = exports.ImageKind = exports.IDENTITY_MATRIX = exports.FormatError = exports.FontType = exports.FeatureTest = exports.FONT_IDENTITY_MATRIX = exports.DocumentActionEventType = exports.CMapCompressionType = exports.BaseException = exports.AnnotationType = exports.AnnotationStateModelType = exports.AnnotationReviewState = exports.AnnotationReplyType = exports.AnnotationMode = exports.AnnotationMarkedState = exports.AnnotationFlag = exports.AnnotationFieldFlag = exports.AnnotationEditorType = exports.AnnotationEditorPrefix = exports.AnnotationEditorParamsType = exports.AnnotationBorderStyleType = exports.AnnotationActionEventType = exports.AbortException = void 0;
 exports.arrayByteLength = arrayByteLength;
 exports.arraysToBytes = arraysToBytes;
 exports.assert = assert;
@@ -57,6 +57,10 @@ const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
 exports.IDENTITY_MATRIX = IDENTITY_MATRIX;
 const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
 exports.FONT_IDENTITY_MATRIX = FONT_IDENTITY_MATRIX;
+const LINE_FACTOR = 1.35;
+exports.LINE_FACTOR = LINE_FACTOR;
+const LINE_DESCENT_FACTOR = 0.35;
+exports.LINE_DESCENT_FACTOR = LINE_DESCENT_FACTOR;
 const RenderingIntentFlag = {
   ANY: 0x01,
   DISPLAY: 0x02,
@@ -74,6 +78,24 @@ const AnnotationMode = {
   ENABLE_STORAGE: 3
 };
 exports.AnnotationMode = AnnotationMode;
+const AnnotationEditorPrefix = "pdfjs_internal_editor_";
+exports.AnnotationEditorPrefix = AnnotationEditorPrefix;
+const AnnotationEditorType = {
+  DISABLE: -1,
+  NONE: 0,
+  FREETEXT: 3,
+  INK: 15
+};
+exports.AnnotationEditorType = AnnotationEditorType;
+const AnnotationEditorParamsType = {
+  FREETEXT_SIZE: 1,
+  FREETEXT_COLOR: 2,
+  FREETEXT_OPACITY: 3,
+  INK_COLOR: 11,
+  INK_THICKNESS: 12,
+  INK_OPACITY: 13
+};
+exports.AnnotationEditorParamsType = AnnotationEditorParamsType;
 const PermissionFlag = {
   PRINT: 0x04,
   MODIFY_CONTENTS: 0x08,

+ 273 - 60
lib/test/unit/annotation_spec.js

@@ -1423,7 +1423,7 @@ describe("annotation", function () {
         value: "test\\print"
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" + " 2.00 3.04 Td (test\\\\print) Tj ET Q EMC");
+      expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" + " 2 3.04 Td (test\\\\print) Tj ET Q EMC");
     });
     it("should render regular text in Japanese for printing", async function () {
       textWidgetDict.get("DR").get("Font").set("Goth", gothRefObj.ref);
@@ -1444,7 +1444,7 @@ describe("annotation", function () {
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
       const utf16String = "\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e";
-      expect(appearance).toEqual("/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm" + ` 2.00 2.00 Td (${utf16String}) Tj ET Q EMC`);
+      expect(appearance).toEqual("/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm" + ` 2 2 Td (${utf16String}) Tj ET Q EMC`);
     });
     it("should render regular text for printing using normal appearance", async function () {
       const textWidgetRef = _primitives.Ref.get(271, 0);
@@ -1463,11 +1463,13 @@ describe("annotation", function () {
       partialEvaluator.xref = xref;
       const annotation = await _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock);
       const annotationStorage = new Map();
-      const operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(3);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["271R", [0, 0, 32, 10], [32, 0, 0, 10, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
+      const {
+        opList
+      } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList.argsArray.length).toEqual(3);
+      expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+      expect(opList.argsArray[0]).toEqual(["271R", [0, 0, 32, 10], [32, 0, 0, 10, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
     });
     it("should render auto-sized text for printing", async function () {
       textWidgetDict.set("DA", "/Helv 0 Tf");
@@ -1486,7 +1488,7 @@ describe("annotation", function () {
         value: "test (print)"
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      expect(appearance).toEqual("/Tx BMC q BT /Helv 5.92 Tf 0 g 1 0 0 1 0 0 Tm" + " 2.00 3.23 Td (test \\(print\\)) Tj ET Q EMC");
+      expect(appearance).toEqual("/Tx BMC q BT /Helv 5.92 Tf 0 g 1 0 0 1 0 0 Tm" + " 2 3.23 Td (test \\(print\\)) Tj ET Q EMC");
     });
     it("should render auto-sized text in Japanese for printing", async function () {
       textWidgetDict.get("DR").get("Font").set("Goth", gothRefObj.ref);
@@ -1507,7 +1509,7 @@ describe("annotation", function () {
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
       const utf16String = "\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e";
-      expect(appearance).toEqual("/Tx BMC q BT /Goth 3.5 Tf 0 g 1 0 0 1 0 0 Tm" + ` 2.00 2.00 Td (${utf16String}) Tj ET Q EMC`);
+      expect(appearance).toEqual("/Tx BMC q BT /Goth 3.5 Tf 0 g 1 0 0 1 0 0 Tm" + ` 2 2 Td (${utf16String}) Tj ET Q EMC`);
     });
     it("should not render a password for printing", async function () {
       textWidgetDict.set("Ff", _util.AnnotationFieldFlag.PASSWORD);
@@ -1545,7 +1547,7 @@ describe("annotation", function () {
         value: "a aa aaa aaaa aaaaa aaaaaa " + "pneumonoultramicroscopicsilicovolcanoconiosis"
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      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");
+      expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " + "2 -5 Td (a aa aaa ) Tj\n" + "0 -5 Td (aaaa aaaaa ) Tj\n" + "0 -5 Td (aaaaaa ) Tj\n" + "0 -5 Td (pneumonoultr) Tj\n" + "0 -5 Td (amicroscopi) Tj\n" + "0 -5 Td (csilicovolca) Tj\n" + "0 -5 Td (noconiosis) Tj ET Q EMC");
     });
     it("should render multiline text in Japanese for printing", async function () {
       textWidgetDict.set("Ff", _util.AnnotationFieldFlag.MULTILINE);
@@ -1566,7 +1568,7 @@ describe("annotation", function () {
         value: "こんにちは世界の"
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      expect(appearance).toEqual("/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 10 Tm " + "2.00 -5.00 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" + "0.00 -5.00 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC");
+      expect(appearance).toEqual("/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 10 Tm " + "2 -5 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" + "0 -5 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC");
     });
     it("should render multiline text with various EOL for printing", async function () {
       textWidgetDict.set("Ff", _util.AnnotationFieldFlag.MULTILINE);
@@ -1580,7 +1582,7 @@ describe("annotation", function () {
       }, helvRefObj]);
       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";
+      const expectedAppearance = "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " + "2 -5 Td " + "(Lorem ipsum dolor sit amet, consectetur adipiscing elit.) Tj\n" + "0 -5 Td " + "(Aliquam vitae felis ac lectus bibendum ultricies quis non) Tj\n" + "0 -5 Td " + "( diam.) Tj\n" + "0 -5 Td " + "(Morbi id porttitor quam, a iaculis dui.) Tj\n" + "0 -5 Td " + "(Pellentesque habitant morbi tristique senectus et netus ) Tj\n" + "0 -5 Td " + "(et malesuada fames ac turpis egestas.) Tj\n" + "0 -5 Td () Tj\n" + "0 -5 Td () Tj\n" + "0 -5 Td " + "(Nulla consectetur, ligula in tincidunt placerat, velit ) Tj\n" + "0 -5 Td " + "(augue consectetur orci, sed mattis libero nunc ut massa.) Tj\n" + "0 -5 Td " + "(Etiam facilisis tempus interdum.) Tj ET Q EMC";
       const annotation = await _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock);
       const annotationStorage = new Map();
       annotationStorage.set(annotation.data.id, {
@@ -1607,7 +1609,7 @@ describe("annotation", function () {
         value: "aa(aa)a\\"
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.035 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");
+      expect(appearance).toEqual("/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.035 Tm" + " (a) Tj 8 0 Td (a) Tj 8 0 Td (\\() Tj" + " 8 0 Td (a) Tj 8 0 Td (a) Tj" + " 8 0 Td (\\)) Tj 8 0 Td (a) Tj" + " 8 0 Td (\\\\) Tj ET Q EMC");
     });
     it("should render comb with Japanese text for printing", async function () {
       textWidgetDict.set("Ff", _util.AnnotationFieldFlag.COMB);
@@ -1630,7 +1632,7 @@ describe("annotation", function () {
         value: "こんにちは世界の"
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      expect(appearance).toEqual("/Tx BMC q BT /Goth 5 Tf 1 0 0 1 2 2 Tm" + " (\x30\x53) Tj 8.00 0 Td (\x30\x93) Tj 8.00 0 Td (\x30\x6b) Tj" + " 8.00 0 Td (\x30\x61) Tj 8.00 0 Td (\x30\x6f) Tj" + " 8.00 0 Td (\x4e\x16) Tj 8.00 0 Td (\x75\x4c) Tj" + " 8.00 0 Td (\x30\x6e) Tj ET Q EMC");
+      expect(appearance).toEqual("/Tx BMC q BT /Goth 5 Tf 1 0 0 1 2 2 Tm" + " (\x30\x53) Tj 8 0 Td (\x30\x93) Tj 8 0 Td (\x30\x6b) Tj" + " 8 0 Td (\x30\x61) Tj 8 0 Td (\x30\x6f) Tj" + " 8 0 Td (\x4e\x16) Tj 8 0 Td (\x75\x4c) Tj" + " 8 0 Td (\x30\x6e) Tj ET Q EMC");
     });
     it("should save text", async function () {
       const textWidgetRef = _primitives.Ref.get(123, 0);
@@ -1653,7 +1655,31 @@ describe("annotation", function () {
       expect(newData.ref).toEqual(_primitives.Ref.get(2, 0));
       oldData.data = oldData.data.replace(/\(D:\d+\)/, "(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 2 0 R>> /M (date)>>\nendobj\n");
-      expect(newData.data).toEqual("2 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 3.04 Td (hello world) Tj " + "ET Q EMC\nendstream\nendobj\n");
+      expect(newData.data).toEqual("2 0 obj\n<< /Length 74 /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 3.04 Td (hello world) Tj " + "ET Q EMC\nendstream\nendobj\n");
+    });
+    it("should save rotated text", async function () {
+      const textWidgetRef = _primitives.Ref.get(123, 0);
+
+      const xref = new _test_utils.XRefMock([{
+        ref: textWidgetRef,
+        data: textWidgetDict
+      }, helvRefObj]);
+      partialEvaluator.xref = xref;
+      const task = new _worker.WorkerTask("test save");
+      const annotation = await _annotation.AnnotationFactory.create(xref, textWidgetRef, pdfManagerMock, idFactoryMock);
+      const annotationStorage = new Map();
+      annotationStorage.set(annotation.data.id, {
+        value: "hello world",
+        rotation: 90
+      });
+      const data = await annotation.save(partialEvaluator, task, annotationStorage);
+      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(2, 0));
+      oldData.data = oldData.data.replace(/\(D:\d+\)/, "(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 2 0 R>> /M (date) /MK << /R 90>>>>\nendobj\n");
+      expect(newData.data).toEqual("2 0 obj\n<< /Length 74 /Subtype /Form /Resources " + "<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10] /Matrix [0 1 -1 0 32 0]>> stream\n" + "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " + "ET Q EMC\nendstream\nendobj\n");
     });
     it("should get field object for usage in JS sandbox", async function () {
       const textWidgetRef = _primitives.Ref.get(123, 0);
@@ -1759,7 +1785,7 @@ describe("annotation", function () {
       expect(newData.ref).toEqual(_primitives.Ref.get(2, 0));
       oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)");
       expect(oldData.data).toEqual("123 0 obj\n" + "<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Goth 5 Tf) /DR " + "<< /Font << /Helv 314 0 R /Goth 159 0 R>>>> /Rect [0 0 32 10] " + `/V (\xfe\xff${utf16String}) /AP << /N 2 0 R>> /M (date)>>\nendobj\n`);
-      expect(newData.data).toEqual("2 0 obj\n<< /Length 82 /Subtype /Form /Resources " + "<< /Font << /Helv 314 0 R /Goth 159 0 R>>>> /BBox [0 0 32 10]>> stream\n" + `/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2.00 2.00 Td (${utf16String}) Tj ` + "ET Q EMC\nendstream\nendobj\n");
+      expect(newData.data).toEqual("2 0 obj\n<< /Length 76 /Subtype /Form /Resources " + "<< /Font << /Helv 314 0 R /Goth 159 0 R>>>> /BBox [0 0 32 10]>> stream\n" + `/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 2 Td (${utf16String}) Tj ` + "ET Q EMC\nendstream\nendobj\n");
     });
   });
   describe("ButtonWidgetAnnotation", function () {
@@ -1875,11 +1901,13 @@ describe("annotation", function () {
       annotationStorage.set(annotation.data.id, {
         value: true
       });
-      const operatorList = await annotation.getOperatorList(checkboxEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(5);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.dependency, _util.OPS.setFont, _util.OPS.showText, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[3][0][0].unicode).toEqual("4");
+      const {
+        opList
+      } = await annotation.getOperatorList(checkboxEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList.argsArray.length).toEqual(5);
+      expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.dependency, _util.OPS.setFont, _util.OPS.showText, _util.OPS.endAnnotation]);
+      expect(opList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList.argsArray[3][0][0].unicode).toEqual("4");
     });
     it("should render checkboxes for printing", async function () {
       const appearanceStatesDict = new _primitives.Dict();
@@ -1910,19 +1938,23 @@ describe("annotation", function () {
       annotationStorage.set(annotation.data.id, {
         value: true
       });
-      let operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(3);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
+      const {
+        opList: opList1
+      } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList1.argsArray.length).toEqual(3);
+      expect(opList1.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+      expect(opList1.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList1.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
       annotationStorage.set(annotation.data.id, {
         value: false
       });
-      operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(3);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
+      const {
+        opList: opList2
+      } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList2.argsArray.length).toEqual(3);
+      expect(opList2.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+      expect(opList2.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList2.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
     });
     it("should render checkboxes for printing twice", async function () {
       const appearanceStatesDict = new _primitives.Dict();
@@ -1956,11 +1988,13 @@ describe("annotation", function () {
         annotationStorage.set(annotation.data.id, {
           value: true
         });
-        const operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-        expect(operatorList.argsArray.length).toEqual(3);
-        expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-        expect(operatorList.argsArray[0]).toEqual(["1249R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-        expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
+        const {
+          opList
+        } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+        expect(opList.argsArray.length).toEqual(3);
+        expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+        expect(opList.argsArray[0]).toEqual(["1249R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+        expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
       }
     });
     it("should render checkboxes for printing using normal appearance", async function () {
@@ -1990,11 +2024,13 @@ describe("annotation", function () {
       const task = new _worker.WorkerTask("test print");
       const annotation = await _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock);
       const annotationStorage = new Map();
-      const operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(3);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
+      const {
+        opList
+      } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList.argsArray.length).toEqual(3);
+      expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+      expect(opList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
     });
     it("should save checkboxes", async function () {
       const appearanceStatesDict = new _primitives.Dict();
@@ -2028,6 +2064,39 @@ describe("annotation", function () {
       const data = await annotation.save(partialEvaluator, task, annotationStorage);
       expect(data).toEqual(null);
     });
+    it("should save rotated checkboxes", async function () {
+      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");
+      const annotation = await _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock);
+      const annotationStorage = new Map();
+      annotationStorage.set(annotation.data.id, {
+        value: true,
+        rotation: 180
+      });
+      const [oldData] = await annotation.save(partialEvaluator, task, annotationStorage);
+      oldData.data = oldData.data.replace(/\(D:\d+\)/, "(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) /MK << /R 180>>>>\nendobj\n");
+      annotationStorage.set(annotation.data.id, {
+        value: false
+      });
+      const data = await annotation.save(partialEvaluator, task, annotationStorage);
+      expect(data).toEqual(null);
+    });
     it("should handle radio buttons with a field value", async function () {
       const parentDict = new _primitives.Dict();
       parentDict.set("V", _primitives.Name.get("1"));
@@ -2133,19 +2202,23 @@ describe("annotation", function () {
       annotationStorage.set(annotation.data.id, {
         value: true
       });
-      let operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(3);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
+      const {
+        opList: opList1
+      } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList1.argsArray.length).toEqual(3);
+      expect(opList1.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+      expect(opList1.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList1.argsArray[1]).toEqual(new Uint8ClampedArray([26, 51, 76]));
       annotationStorage.set(annotation.data.id, {
         value: false
       });
-      operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(3);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
+      const {
+        opList: opList2
+      } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList2.argsArray.length).toEqual(3);
+      expect(opList2.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+      expect(opList2.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList2.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
     });
     it("should render radio buttons for printing using normal appearance", async function () {
       const appearanceStatesDict = new _primitives.Dict();
@@ -2175,11 +2248,13 @@ describe("annotation", function () {
       const task = new _worker.WorkerTask("test print");
       const annotation = await _annotation.AnnotationFactory.create(xref, buttonWidgetRef, pdfManagerMock, idFactoryMock);
       const annotationStorage = new Map();
-      const operatorList = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
-      expect(operatorList.argsArray.length).toEqual(3);
-      expect(operatorList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
-      expect(operatorList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
-      expect(operatorList.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
+      const {
+        opList
+      } = await annotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, annotationStorage);
+      expect(opList.argsArray.length).toEqual(3);
+      expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setFillRGBColor, _util.OPS.endAnnotation]);
+      expect(opList.argsArray[0]).toEqual(["124R", [0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 0, 0], false]);
+      expect(opList.argsArray[1]).toEqual(new Uint8ClampedArray([76, 51, 26]));
     });
     it("should save radio buttons", async function () {
       const appearanceStatesDict = new _primitives.Dict();
@@ -2624,7 +2699,7 @@ describe("annotation", function () {
         value: ["A", "C"]
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      expect(appearance).toEqual(["/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2.00 -5.88 Td (a) Tj", "0.00 -6.75 Td (b) Tj", "ET Q EMC"].join("\n"));
+      expect(appearance).toEqual(["/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2 -5.88 Td (a) Tj", "0 -6.75 Td (b) Tj", "ET Q EMC"].join("\n"));
     });
     it("should render choice with multiple selections for printing", async function () {
       choiceWidgetDict.set("Ff", _util.AnnotationFieldFlag.MULTISELECT);
@@ -2645,7 +2720,34 @@ describe("annotation", function () {
         value: ["B", "C"]
       });
       const appearance = await annotation._getAppearance(partialEvaluator, task, annotationStorage);
-      expect(appearance).toEqual(["/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "1 -3.5 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2.00 -5.88 Td (b) Tj", "0.00 -6.75 Td (c) Tj", "ET Q EMC"].join("\n"));
+      expect(appearance).toEqual(["/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "1 -3.5 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2 -5.88 Td (b) Tj", "0 -6.75 Td (c) Tj", "ET Q EMC"].join("\n"));
+    });
+    it("should save rotated choice", async function () {
+      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
+      }, fontRefObj]);
+      partialEvaluator.xref = xref;
+      const task = new _worker.WorkerTask("test save");
+      const annotation = await _annotation.AnnotationFactory.create(xref, choiceWidgetRef, pdfManagerMock, idFactoryMock);
+      const annotationStorage = new Map();
+      annotationStorage.set(annotation.data.id, {
+        value: "C",
+        rotation: 270
+      });
+      const data = await annotation.save(partialEvaluator, task, annotationStorage);
+      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(2, 0));
+      oldData.data = oldData.data.replace(/\(D:\d+\)/, "(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 2 0 R>> /M (date) /MK << /R 270>>>>\nendobj\n");
+      expect(newData.data).toEqual(["2 0 obj", "<< /Length 170 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10] /Matrix [0 -1 1 0 0 10]>> stream", "/Tx BMC q", "1 1 10 32 re W n", "0.600006 0.756866 0.854904 rg", "1 11.75 10 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 32 Tm", "2 -5.88 Td (A) Tj", "0 -6.75 Td (B) Tj", "0 -6.75 Td (C) Tj", "ET Q EMC", "endstream", "endobj\n"].join("\n"));
     });
     it("should save choice", async function () {
       choiceWidgetDict.set("Opt", ["A", "B", "C"]);
@@ -2671,7 +2773,7 @@ describe("annotation", function () {
       expect(newData.ref).toEqual(_primitives.Ref.get(2, 0));
       oldData.data = oldData.data.replace(/\(D:\d+\)/, "(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 2 0 R>> /M (date)>>\nendobj\n");
-      expect(newData.data).toEqual(["2 0 obj", "<< /Length 136 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10]>> stream", "/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2.00 -5.88 Td (C) Tj", "ET Q EMC", "endstream", "endobj\n"].join("\n"));
+      expect(newData.data).toEqual(["2 0 obj", "<< /Length 133 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10]>> stream", "/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2 -5.88 Td (C) Tj", "ET Q EMC", "endstream", "endobj\n"].join("\n"));
     });
     it("should save choice with multiple selections", async function () {
       choiceWidgetDict.set("Ff", _util.AnnotationFieldFlag.MULTISELECT);
@@ -2698,7 +2800,7 @@ describe("annotation", function () {
       expect(newData.ref).toEqual(_primitives.Ref.get(2, 0));
       oldData.data = oldData.data.replace(/\(D:\d+\)/, "(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] /Ff 2097152 /Opt " + "[[(A) (a)] [(B) (b)] [(C) (c)] [(D) (d)]] /V [(B) (C)] /AP " + "<< /N 2 0 R>> /M (date)>>\nendobj\n");
-      expect(newData.data).toEqual(["2 0 obj", "<< /Length 177 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10]>> stream", "/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "1 -3.5 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2.00 -5.88 Td (b) Tj", "0.00 -6.75 Td (c) Tj", "ET Q EMC", "endstream", "endobj\n"].join("\n"));
+      expect(newData.data).toEqual(["2 0 obj", "<< /Length 171 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10]>> stream", "/Tx BMC q", "1 1 32 10 re W n", "0.600006 0.756866 0.854904 rg", "1 3.25 32 6.75 re f", "1 -3.5 32 6.75 re f", "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", "2 -5.88 Td (b) Tj", "0 -6.75 Td (c) Tj", "ET Q EMC", "endstream", "endobj\n"].join("\n"));
     });
   });
   describe("LineAnnotation", function () {
@@ -2929,6 +3031,43 @@ describe("annotation", function () {
       expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255]));
     });
   });
+  describe("FreeTextAnnotation", () => {
+    it("should create a new FreeText annotation", async () => {
+      partialEvaluator.xref = new _test_utils.XRefMock();
+      const task = new _worker.WorkerTask("test FreeText creation");
+      const data = await _annotation.AnnotationFactory.saveNewAnnotations(partialEvaluator, task, [{
+        annotationType: _util.AnnotationEditorType.FREETEXT,
+        rect: [12, 34, 56, 78],
+        rotation: 0,
+        fontSize: 10,
+        color: [0, 0, 0],
+        value: "Hello PDF.js World!"
+      }]);
+      const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)");
+      expect(base).toEqual("2 0 obj\n" + "<< /Type /Annot /Subtype /FreeText /CreationDate (date) " + "/Rect [12 34 56 78] /DA (/Helv 10 Tf 0 g) /Contents (Hello PDF.js World!) " + "/F 4 /Border [0 0 0] /Rotate 0 /AP << /N 3 0 R>>>>\n" + "endobj\n");
+      const font = data.dependencies[0].data;
+      expect(font).toEqual("1 0 obj\n" + "<< /BaseFont /Helvetica /Type /Font /Subtype /Type1 /Encoding " + "/WinAnsiEncoding>>\n" + "endobj\n");
+      const appearance = data.dependencies[1].data;
+      expect(appearance).toEqual("3 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] " + "/Length 101 /Resources << /Font << /Helv 1 0 R>>>>>> stream\n" + "q\n" + "0 0 44 44 re W n\n" + "BT\n" + "1 0 0 1 0 47.5 Tm 0 Tc 0 g\n" + "/Helv 10 Tf\n" + "0 -13.5 Td (Hello PDF.js World!) Tj\n" + "ET\n" + "Q\n" + "endstream\n" + "\n" + "endobj\n");
+    });
+    it("should render an added FreeText annotation for printing", async function () {
+      partialEvaluator.xref = new _test_utils.XRefMock();
+      const task = new _worker.WorkerTask("test FreeText printing");
+      const freetextAnnotation = (await _annotation.AnnotationFactory.printNewAnnotations(partialEvaluator, task, [{
+        annotationType: _util.AnnotationEditorType.FREETEXT,
+        rect: [12, 34, 56, 78],
+        rotation: 0,
+        fontSize: 10,
+        color: [0, 0, 0],
+        value: "A"
+      }]))[0];
+      const {
+        opList
+      } = await freetextAnnotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, null);
+      expect(opList.fnArray.length).toEqual(16);
+      expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.save, _util.OPS.constructPath, _util.OPS.clip, _util.OPS.endPath, _util.OPS.beginText, _util.OPS.setTextMatrix, _util.OPS.setCharSpacing, _util.OPS.setFillRGBColor, _util.OPS.dependency, _util.OPS.setFont, _util.OPS.moveText, _util.OPS.showText, _util.OPS.endText, _util.OPS.restore, _util.OPS.endAnnotation]);
+    });
+  });
   describe("InkAnnotation", function () {
     it("should handle a single ink list", async function () {
       const inkDict = new _primitives.Dict();
@@ -2993,6 +3132,80 @@ describe("annotation", function () {
         y: 5
       }]);
     });
+    it("should create a new Ink annotation", async function () {
+      partialEvaluator.xref = new _test_utils.XRefMock();
+      const task = new _worker.WorkerTask("test Ink creation");
+      const data = await _annotation.AnnotationFactory.saveNewAnnotations(partialEvaluator, task, [{
+        annotationType: _util.AnnotationEditorType.INK,
+        rect: [12, 34, 56, 78],
+        rotation: 0,
+        thickness: 1,
+        opacity: 1,
+        color: [0, 0, 0],
+        paths: [{
+          bezier: [10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27],
+          points: [1, 2, 3, 4, 5, 6, 7, 8]
+        }, {
+          bezier: [910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925, 926, 927],
+          points: [91, 92, 93, 94, 95, 96, 97, 98]
+        }]
+      }]);
+      const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)");
+      expect(base).toEqual("1 0 obj\n" + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + "/InkList [[1 2 3 4 5 6 7 8] [91 92 93 94 95 96 97 98]] /F 4 /Border [0 0 0] " + "/Rotate 0 /AP << /N 2 0 R>>>>\n" + "endobj\n");
+      const appearance = data.dependencies[0].data;
+      expect(appearance).toEqual("2 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] /Length 129>> stream\n" + "1 w 1 J 1 j\n" + "0 G\n" + "10 11 m\n" + "12 13 14 15 16 17 c\n" + "22 23 24 25 26 27 c\n" + "S\n" + "910 911 m\n" + "912 913 914 915 916 917 c\n" + "922 923 924 925 926 927 c\n" + "S\n" + "endstream\n" + "\n" + "endobj\n");
+    });
+    it("should create a new Ink annotation with some transparency", async function () {
+      partialEvaluator.xref = new _test_utils.XRefMock();
+      const task = new _worker.WorkerTask("test Ink creation");
+      const data = await _annotation.AnnotationFactory.saveNewAnnotations(partialEvaluator, task, [{
+        annotationType: _util.AnnotationEditorType.INK,
+        rect: [12, 34, 56, 78],
+        rotation: 0,
+        thickness: 1,
+        opacity: 0.12,
+        color: [0, 0, 0],
+        paths: [{
+          bezier: [10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27],
+          points: [1, 2, 3, 4, 5, 6, 7, 8]
+        }, {
+          bezier: [910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925, 926, 927],
+          points: [91, 92, 93, 94, 95, 96, 97, 98]
+        }]
+      }]);
+      const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)");
+      expect(base).toEqual("1 0 obj\n" + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + "/InkList [[1 2 3 4 5 6 7 8] [91 92 93 94 95 96 97 98]] /F 4 /Border [0 0 0] " + "/Rotate 0 /AP << /N 2 0 R>>>>\n" + "endobj\n");
+      const appearance = data.dependencies[0].data;
+      expect(appearance).toEqual("2 0 obj\n" + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] /Length 136 /Resources " + "<< /ExtGState << /R0 << /CA 0.12 /Type /ExtGState>>>>>>>> stream\n" + "1 w 1 J 1 j\n" + "0 G\n" + "/R0 gs\n" + "10 11 m\n" + "12 13 14 15 16 17 c\n" + "22 23 24 25 26 27 c\n" + "S\n" + "910 911 m\n" + "912 913 914 915 916 917 c\n" + "922 923 924 925 926 927 c\n" + "S\n" + "endstream\n" + "\n" + "endobj\n");
+    });
+    it("should render an added Ink annotation for printing", async function () {
+      partialEvaluator.xref = new _test_utils.XRefMock();
+      const task = new _worker.WorkerTask("test Ink printing");
+      const inkAnnotation = (await _annotation.AnnotationFactory.printNewAnnotations(partialEvaluator, task, [{
+        annotationType: _util.AnnotationEditorType.INK,
+        rect: [12, 34, 56, 78],
+        rotation: 0,
+        thickness: 3,
+        opacity: 1,
+        color: [0, 255, 0],
+        paths: [{
+          bezier: [1, 2, 3, 4, 5, 6, 7, 8],
+          points: [1, 2, 3, 4, 5, 6, 7, 8]
+        }]
+      }]))[0];
+      const {
+        opList
+      } = await inkAnnotation.getOperatorList(partialEvaluator, task, _util.RenderingIntentFlag.PRINT, false, null);
+      expect(opList.argsArray.length).toEqual(8);
+      expect(opList.fnArray).toEqual([_util.OPS.beginAnnotation, _util.OPS.setLineWidth, _util.OPS.setLineCap, _util.OPS.setLineJoin, _util.OPS.setStrokeRGBColor, _util.OPS.constructPath, _util.OPS.stroke, _util.OPS.endAnnotation]);
+      expect(opList.argsArray[1]).toEqual([3]);
+      expect(opList.argsArray[2]).toEqual([1]);
+      expect(opList.argsArray[3]).toEqual([1]);
+      expect(opList.argsArray[4]).toEqual(new Uint8ClampedArray([0, 255, 0]));
+      expect(opList.argsArray[5][0]).toEqual([_util.OPS.moveTo, _util.OPS.curveTo]);
+      expect(opList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
+      expect(opList.argsArray[5][2]).toEqual([1, 1, 2, 2]);
+    });
   });
   describe("HightlightAnnotation", function () {
     it("should set quadpoints to null if not defined", async function () {

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

@@ -29,6 +29,8 @@ var _api = require("../../display/api.js");
 
 var _display_utils = require("../../display/display_utils.js");
 
+var _annotation_storage = require("../../display/annotation_storage.js");
+
 var _ui_utils = require("../../web/ui_utils.js");
 
 var _image_utils = require("../../core/image_utils.js");
@@ -59,7 +61,7 @@ describe("api", function () {
   }
 
   function mergeText(items) {
-    return items.map(chunk => chunk.str + (chunk.hasEOL ? "\n" : "")).join("");
+    return items.map(chunk => (chunk.str ?? "") + (chunk.hasEOL ? "\n" : "")).join("");
   }
 
   describe("getDocument", function () {
@@ -346,6 +348,7 @@ describe("api", function () {
       expect(opList.fnArray.length).toEqual(0);
       expect(opList.argsArray.length).toEqual(0);
       expect(opList.lastChunk).toEqual(true);
+      expect(opList.separateAnnots).toEqual(null);
       await loadingTask.destroy();
     });
     it("creates pdf doc from PDF file with bad XRef header", async function () {
@@ -359,6 +362,7 @@ describe("api", function () {
       expect(opList.fnArray.length).toEqual(0);
       expect(opList.argsArray.length).toEqual(0);
       expect(opList.lastChunk).toEqual(true);
+      expect(opList.separateAnnots).toEqual(null);
       await loadingTask.destroy();
     });
     it("creates pdf doc from PDF file with bad XRef byteWidths", async function () {
@@ -404,6 +408,7 @@ describe("api", function () {
       expect(opList.fnArray.length).toBeGreaterThan(5);
       expect(opList.argsArray.length).toBeGreaterThan(5);
       expect(opList.lastChunk).toEqual(true);
+      expect(opList.separateAnnots).toEqual(null);
 
       try {
         await pdfDocument2.getPage(1);
@@ -433,6 +438,7 @@ describe("api", function () {
         expect(opList.fnArray.length).toBeGreaterThan(5);
         expect(opList.argsArray.length).toBeGreaterThan(5);
         expect(opList.lastChunk).toEqual(true);
+        expect(opList.separateAnnots).toEqual(null);
       }
 
       await Promise.all([loadingTask1.destroy(), loadingTask2.destroy()]);
@@ -465,6 +471,19 @@ describe("api", function () {
 
       await Promise.all([loadingTask1.destroy(), loadingTask2.destroy()]);
     });
+    it("creates pdf doc from PDF file with bad /Resources entry", async function () {
+      const loadingTask = (0, _api.getDocument)((0, _test_utils.buildGetDocumentParams)("issue15150.pdf"));
+      expect(loadingTask instanceof _api.PDFDocumentLoadingTask).toEqual(true);
+      const pdfDocument = await loadingTask.promise;
+      expect(pdfDocument.numPages).toEqual(1);
+      const page = await pdfDocument.getPage(1);
+      expect(page instanceof _api.PDFPageProxy).toEqual(true);
+      const opList = await page.getOperatorList();
+      expect(opList.fnArray).toEqual([_util.OPS.setLineWidth, _util.OPS.setStrokeRGBColor, _util.OPS.constructPath, _util.OPS.closeStroke]);
+      expect(opList.argsArray).toEqual([[0.5], new Uint8ClampedArray([255, 0, 0]), [[_util.OPS.moveTo, _util.OPS.lineTo], [0, 9.75, 0.5, 9.75], [0, 0.5, 9.75, 9.75]], null]);
+      expect(opList.lastChunk).toEqual(true);
+      await loadingTask.destroy();
+    });
   });
   describe("PDFWorker", function () {
     it("worker created or destroyed", async function () {
@@ -981,6 +1000,7 @@ describe("api", function () {
           page: 0,
           strokeColor: null,
           fillColor: null,
+          rotation: 0,
           type: "text"
         }],
         Button1: [{
@@ -998,6 +1018,7 @@ describe("api", function () {
           page: 0,
           strokeColor: null,
           fillColor: new Uint8ClampedArray([192, 192, 192]),
+          rotation: 0,
           type: "button"
         }]
       });
@@ -1689,6 +1710,29 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       expect(/win aisle/.test(text)).toEqual(false);
       await loadingTask.destroy();
     });
+    it("gets text content with or without includeMarkedContent, and compare (issue 15094)", async function () {
+      if (_is_node.isNodeJS) {
+        pending("Linked test-cases are not supported in Node.js.");
+      }
+
+      const loadingTask = (0, _api.getDocument)((0, _test_utils.buildGetDocumentParams)("pdf.pdf"));
+      const pdfDoc = await loadingTask.promise;
+      const pdfPage = await pdfDoc.getPage(568);
+      let {
+        items
+      } = await pdfPage.getTextContent({
+        includeMarkedContent: false
+      });
+      const textWithoutMC = mergeText(items);
+      ({
+        items
+      } = await pdfPage.getTextContent({
+        includeMarkedContent: true
+      }));
+      const textWithMC = mergeText(items);
+      expect(textWithoutMC).toEqual(textWithMC);
+      await loadingTask.destroy();
+    });
     it("gets empty structure tree", async function () {
       const tree = await page.getStructTree();
       expect(tree).toEqual(null);
@@ -1749,6 +1793,10 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       expect(operatorList.fnArray.length).toBeGreaterThan(100);
       expect(operatorList.argsArray.length).toBeGreaterThan(100);
       expect(operatorList.lastChunk).toEqual(true);
+      expect(operatorList.separateAnnots).toEqual({
+        form: false,
+        canvas: false
+      });
     });
     it("gets operatorList with JPEG image (issue 4888)", async function () {
       const loadingTask = (0, _api.getDocument)((0, _test_utils.buildGetDocumentParams)("cmykjpeg.pdf"));
@@ -1777,6 +1825,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
             expect(opList.fnArray.length).toBeGreaterThan(100);
             expect(opList.argsArray.length).toBeGreaterThan(100);
             expect(opList.lastChunk).toEqual(true);
+            expect(opList.separateAnnots).toEqual(null);
             return loadingTask1.destroy();
           });
         });
@@ -1787,6 +1836,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
             expect(opList.fnArray.length).toEqual(0);
             expect(opList.argsArray.length).toEqual(0);
             expect(opList.lastChunk).toEqual(true);
+            expect(opList.separateAnnots).toEqual(null);
             return loadingTask2.destroy();
           });
         });
@@ -1801,6 +1851,10 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       expect(operatorList.fnArray.length).toBeGreaterThan(20);
       expect(operatorList.argsArray.length).toBeGreaterThan(20);
       expect(operatorList.lastChunk).toEqual(true);
+      expect(operatorList.separateAnnots).toEqual({
+        form: false,
+        canvas: false
+      });
       expect(operatorList.fnArray.includes(_util.OPS.beginAnnotation)).toEqual(true);
       expect(operatorList.fnArray.includes(_util.OPS.endAnnotation)).toEqual(true);
       await loadingTask.destroy();
@@ -1821,24 +1875,46 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       expect(opListAnnotDisable.fnArray.length).toEqual(0);
       expect(opListAnnotDisable.argsArray.length).toEqual(0);
       expect(opListAnnotDisable.lastChunk).toEqual(true);
+      expect(opListAnnotDisable.separateAnnots).toEqual(null);
       const opListAnnotEnable = await pdfPage.getOperatorList({
         annotationMode: _util.AnnotationMode.ENABLE
       });
-      expect(opListAnnotEnable.fnArray.length).toBeGreaterThan(150);
-      expect(opListAnnotEnable.argsArray.length).toBeGreaterThan(150);
+      expect(opListAnnotEnable.fnArray.length).toBeGreaterThan(140);
+      expect(opListAnnotEnable.argsArray.length).toBeGreaterThan(140);
       expect(opListAnnotEnable.lastChunk).toEqual(true);
+      expect(opListAnnotEnable.separateAnnots).toEqual({
+        form: false,
+        canvas: true
+      });
+      let firstAnnotIndex = opListAnnotEnable.fnArray.indexOf(_util.OPS.beginAnnotation);
+      let isUsingOwnCanvas = opListAnnotEnable.argsArray[firstAnnotIndex][4];
+      expect(isUsingOwnCanvas).toEqual(false);
       const opListAnnotEnableForms = await pdfPage.getOperatorList({
         annotationMode: _util.AnnotationMode.ENABLE_FORMS
       });
-      expect(opListAnnotEnableForms.fnArray.length).toBeGreaterThan(40);
-      expect(opListAnnotEnableForms.argsArray.length).toBeGreaterThan(40);
+      expect(opListAnnotEnableForms.fnArray.length).toBeGreaterThan(30);
+      expect(opListAnnotEnableForms.argsArray.length).toBeGreaterThan(30);
       expect(opListAnnotEnableForms.lastChunk).toEqual(true);
+      expect(opListAnnotEnableForms.separateAnnots).toEqual({
+        form: true,
+        canvas: true
+      });
+      firstAnnotIndex = opListAnnotEnableForms.fnArray.indexOf(_util.OPS.beginAnnotation);
+      isUsingOwnCanvas = opListAnnotEnableForms.argsArray[firstAnnotIndex][4];
+      expect(isUsingOwnCanvas).toEqual(true);
       const opListAnnotEnableStorage = await pdfPage.getOperatorList({
         annotationMode: _util.AnnotationMode.ENABLE_STORAGE
       });
       expect(opListAnnotEnableStorage.fnArray.length).toBeGreaterThan(170);
       expect(opListAnnotEnableStorage.argsArray.length).toBeGreaterThan(170);
       expect(opListAnnotEnableStorage.lastChunk).toEqual(true);
+      expect(opListAnnotEnableStorage.separateAnnots).toEqual({
+        form: false,
+        canvas: true
+      });
+      firstAnnotIndex = opListAnnotEnableStorage.fnArray.indexOf(_util.OPS.beginAnnotation);
+      isUsingOwnCanvas = opListAnnotEnableStorage.argsArray[firstAnnotIndex][4];
+      expect(isUsingOwnCanvas).toEqual(false);
       expect(opListAnnotDisable.fnArray.length).toBeLessThan(opListAnnotEnableForms.fnArray.length);
       expect(opListAnnotEnableForms.fnArray.length).toBeLessThan(opListAnnotEnable.fnArray.length);
       expect(opListAnnotEnable.fnArray.length).toBeLessThan(opListAnnotEnableStorage.fnArray.length);
@@ -1909,7 +1985,10 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       });
       expect(renderTask instanceof _api.RenderTask).toEqual(true);
       await renderTask.promise;
-      const stats = pdfPage.stats;
+      expect(renderTask.separateAnnots).toEqual(false);
+      const {
+        stats
+      } = pdfPage;
       expect(stats instanceof _display_utils.StatTimer).toEqual(true);
       expect(stats.times.length).toEqual(3);
       const [statEntryOne, statEntryTwo, statEntryThree] = stats.times;
@@ -1975,6 +2054,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       });
       expect(reRenderTask instanceof _api.RenderTask).toEqual(true);
       await reRenderTask.promise;
+      expect(reRenderTask.separateAnnots).toEqual(false);
       CanvasFactory.destroy(canvasAndCtx);
     });
     it("multiple render() on the same canvas", async function () {
@@ -2020,6 +2100,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       });
       expect(renderTask instanceof _api.RenderTask).toEqual(true);
       await renderTask.promise;
+      expect(renderTask.separateAnnots).toEqual(false);
       await pdfDoc.cleanup();
       expect(true).toEqual(true);
       CanvasFactory.destroy(canvasAndCtx);
@@ -2054,6 +2135,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       }
 
       await renderTask.promise;
+      expect(renderTask.separateAnnots).toEqual(false);
       CanvasFactory.destroy(canvasAndCtx);
       await loadingTask.destroy();
     });
@@ -2113,6 +2195,53 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
       await loadingTask.destroy();
       firstImgData = null;
     });
+    it("render for printing, with `printAnnotationStorage` set", async function () {
+      async function getPrintData(printAnnotationStorage = null) {
+        const canvasAndCtx = CanvasFactory.create(viewport.width, viewport.height);
+        const renderTask = pdfPage.render({
+          canvasContext: canvasAndCtx.context,
+          canvasFactory: CanvasFactory,
+          viewport,
+          intent: "print",
+          annotationMode: _util.AnnotationMode.ENABLE_STORAGE,
+          printAnnotationStorage
+        });
+        await renderTask.promise;
+        expect(renderTask.separateAnnots).toEqual(false);
+        const printData = canvasAndCtx.canvas.toDataURL();
+        CanvasFactory.destroy(canvasAndCtx);
+        return printData;
+      }
+
+      const loadingTask = (0, _api.getDocument)((0, _test_utils.buildGetDocumentParams)("annotation-tx.pdf"));
+      const pdfDoc = await loadingTask.promise;
+      const pdfPage = await pdfDoc.getPage(1);
+      const viewport = pdfPage.getViewport({
+        scale: 1
+      });
+      const {
+        annotationStorage
+      } = pdfDoc;
+      annotationStorage.setValue("22R", {
+        value: "Hello World"
+      });
+      const printOriginalData = await getPrintData();
+      const printAnnotationStorage = annotationStorage.print;
+      annotationStorage.setValue("22R", {
+        value: "Printing again..."
+      });
+
+      const annotationHash = _annotation_storage.AnnotationStorage.getHash(annotationStorage.serializable);
+
+      const printAnnotationHash = _annotation_storage.AnnotationStorage.getHash(printAnnotationStorage.serializable);
+
+      expect(printAnnotationHash).not.toEqual(annotationHash);
+      const printAgainData = await getPrintData();
+      const printStorageData = await getPrintData(printAnnotationStorage);
+      expect(printAgainData).not.toEqual(printOriginalData);
+      expect(printStorageData).toEqual(printOriginalData);
+      await loadingTask.destroy();
+    });
   });
   describe("Multiple `getDocument` instances", function () {
     const pdf1 = (0, _test_utils.buildGetDocumentParams)("tracemonkey.pdf");
@@ -2136,6 +2265,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)).toEqual(true);
         viewport
       });
       await renderTask.promise;
+      expect(renderTask.separateAnnots).toEqual(false);
       const data = canvasAndCtx.canvas.toDataURL();
       CanvasFactory.destroy(canvasAndCtx);
       return data;

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

@@ -141,7 +141,7 @@ describe("custom ownerDocument", function () {
       createElement,
       documentElement: {
         getElementsByTagName: () => [{
-          appendChild: () => {}
+          append: () => {}
         }]
       }
     };

+ 2 - 2
lib/test/unit/default_appearance_spec.js

@@ -26,7 +26,7 @@ var _default_appearance = require("../../core/default_appearance.js");
 describe("Default appearance", function () {
   describe("parseDefaultAppearance and createDefaultAppearance", function () {
     it("should parse and create default appearance", function () {
-      const da = "/F1 12 Tf 0.10 0.20 0.30 rg";
+      const da = "/F1 12 Tf 0.1 0.2 0.3 rg";
       const result = {
         fontSize: 12,
         fontName: "F1",
@@ -41,7 +41,7 @@ describe("Default appearance", function () {
       });
     });
     it("should parse default appearance with save/restore", function () {
-      const da = "q Q 0.10 0.20 0.30 rg /F1 12 Tf q 0.30 0.20 0.10 rg /F2 13 Tf Q";
+      const da = "q Q 0.1 0.2 0.3 rg /F1 12 Tf q 0.3 0.2 0.1 rg /F2 13 Tf Q";
       expect((0, _default_appearance.parseDefaultAppearance)(da)).toEqual({
         fontSize: 12,
         fontName: "F1",

+ 2 - 2
lib/test/unit/display_svg_spec.js

@@ -85,8 +85,8 @@ describe("SVGGraphics", function () {
       }).then(function () {
         let svgImg;
         const elementContainer = {
-          appendChild(element) {
-            svgImg = element;
+          append(...elements) {
+            svgImg = elements.at(-1);
           }
 
         };

+ 33 - 0
lib/test/unit/display_utils_spec.js

@@ -28,6 +28,39 @@ var _util = require("../../shared/util.js");
 var _is_node = require("../../shared/is_node.js");
 
 describe("display_utils", function () {
+  describe("binary search", function () {
+    function isTrue(boolean) {
+      return boolean;
+    }
+
+    function isGreater3(number) {
+      return number > 3;
+    }
+
+    it("empty array", function () {
+      expect((0, _display_utils.binarySearchFirstItem)([], isTrue)).toEqual(0);
+    });
+    it("single boolean entry", function () {
+      expect((0, _display_utils.binarySearchFirstItem)([false], isTrue)).toEqual(1);
+      expect((0, _display_utils.binarySearchFirstItem)([true], isTrue)).toEqual(0);
+    });
+    it("three boolean entries", function () {
+      expect((0, _display_utils.binarySearchFirstItem)([true, true, true], isTrue)).toEqual(0);
+      expect((0, _display_utils.binarySearchFirstItem)([false, true, true], isTrue)).toEqual(1);
+      expect((0, _display_utils.binarySearchFirstItem)([false, false, true], isTrue)).toEqual(2);
+      expect((0, _display_utils.binarySearchFirstItem)([false, false, false], isTrue)).toEqual(3);
+    });
+    it("three numeric entries", function () {
+      expect((0, _display_utils.binarySearchFirstItem)([0, 1, 2], isGreater3)).toEqual(3);
+      expect((0, _display_utils.binarySearchFirstItem)([2, 3, 4], isGreater3)).toEqual(2);
+      expect((0, _display_utils.binarySearchFirstItem)([4, 5, 6], isGreater3)).toEqual(0);
+    });
+    it("three numeric entries and a start index", function () {
+      expect((0, _display_utils.binarySearchFirstItem)([0, 1, 2, 3, 4], isGreater3, 2)).toEqual(4);
+      expect((0, _display_utils.binarySearchFirstItem)([2, 3, 4], isGreater3, 2)).toEqual(2);
+      expect((0, _display_utils.binarySearchFirstItem)([4, 5, 6], isGreater3, 1)).toEqual(1);
+    });
+  });
   describe("DOMCanvasFactory", function () {
     let canvasFactory;
     beforeAll(function () {

+ 119 - 0
lib/test/unit/editor_spec.js

@@ -0,0 +1,119 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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 _tools = require("../../display/editor/tools.js");
+
+var _ink = require("../../display/editor/ink.js");
+
+describe("editor", function () {
+  describe("Command Manager", function () {
+    it("should check undo/redo", function () {
+      const manager = new _tools.CommandManager(4);
+      let x = 0;
+
+      const makeDoUndo = n => ({
+        cmd: () => x += n,
+        undo: () => x -= n
+      });
+
+      manager.add({ ...makeDoUndo(1),
+        mustExec: true
+      });
+      expect(x).toEqual(1);
+      manager.add({ ...makeDoUndo(2),
+        mustExec: true
+      });
+      expect(x).toEqual(3);
+      manager.add({ ...makeDoUndo(3),
+        mustExec: true
+      });
+      expect(x).toEqual(6);
+      manager.undo();
+      expect(x).toEqual(3);
+      manager.undo();
+      expect(x).toEqual(1);
+      manager.undo();
+      expect(x).toEqual(0);
+      manager.undo();
+      expect(x).toEqual(0);
+      manager.redo();
+      expect(x).toEqual(1);
+      manager.redo();
+      expect(x).toEqual(3);
+      manager.redo();
+      expect(x).toEqual(6);
+      manager.redo();
+      expect(x).toEqual(6);
+      manager.undo();
+      expect(x).toEqual(3);
+      manager.redo();
+      expect(x).toEqual(6);
+    });
+  });
+  it("should hit the limit of the manager", function () {
+    const manager = new _tools.CommandManager(3);
+    let x = 0;
+
+    const makeDoUndo = n => ({
+      cmd: () => x += n,
+      undo: () => x -= n
+    });
+
+    manager.add({ ...makeDoUndo(1),
+      mustExec: true
+    });
+    manager.add({ ...makeDoUndo(2),
+      mustExec: true
+    });
+    manager.add({ ...makeDoUndo(3),
+      mustExec: true
+    });
+    manager.add({ ...makeDoUndo(4),
+      mustExec: true
+    });
+    expect(x).toEqual(10);
+    manager.undo();
+    manager.undo();
+    expect(x).toEqual(3);
+    manager.undo();
+    expect(x).toEqual(1);
+    manager.undo();
+    expect(x).toEqual(1);
+    manager.redo();
+    manager.redo();
+    expect(x).toEqual(6);
+    manager.add({ ...makeDoUndo(5),
+      mustExec: true
+    });
+    expect(x).toEqual(11);
+  });
+  describe("fitCurve", function () {
+    it("should return a function", function () {
+      expect(typeof _ink.fitCurve).toEqual("function");
+    });
+    it("should compute an Array of bezier curves", function () {
+      const bezier = (0, _ink.fitCurve)([[1, 2], [4, 5]], 30, null);
+      expect(bezier).toEqual([[[1, 2], [2, 3], [3, 4], [4, 5]]]);
+    });
+  });
+});

+ 7 - 4
lib/test/unit/function_spec.js

@@ -416,10 +416,13 @@ describe("function", function () {
         expect(compiledCode).not.toBeNull();
         const fn = new Function("src", "srcOffset", "dest", "destOffset", compiledCode);
 
-        for (let i = 0; i < samples.length; i++) {
-          const out = new Float32Array(samples[i].output.length);
-          fn(samples[i].input, 0, out, 0);
-          expect(Array.prototype.slice.call(out, 0)).toEqual(samples[i].output);
+        for (const {
+          input,
+          output
+        } of samples) {
+          const out = new Float32Array(output.length);
+          fn(input, 0, out, 0);
+          expect(Array.prototype.slice.call(out, 0)).toEqual(output);
         }
       }
     }

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

@@ -36,7 +36,7 @@ var _api = require("pdfjs/display/api.js");
 var _testreporter = require("./testreporter.js");
 
 async function initializePDFJS(callback) {
-  await Promise.all(["pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/annotation_storage_spec.js", "pdfjs-test/unit/api_spec.js", "pdfjs-test/unit/base_viewer_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/default_appearance_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/event_utils_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/scripting_spec.js", "pdfjs-test/unit/stream_spec.js", "pdfjs-test/unit/struct_tree_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", "pdfjs-test/unit/xfa_formcalc_spec.js", "pdfjs-test/unit/xfa_parser_spec.js", "pdfjs-test/unit/xfa_serialize_data_spec.js", "pdfjs-test/unit/xfa_tohtml_spec.js", "pdfjs-test/unit/xml_spec.js"].map(function (moduleName) {
+  await Promise.all(["pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/annotation_storage_spec.js", "pdfjs-test/unit/api_spec.js", "pdfjs-test/unit/base_viewer_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/default_appearance_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/editor_spec.js", "pdfjs-test/unit/encodings_spec.js", "pdfjs-test/unit/evaluator_spec.js", "pdfjs-test/unit/event_utils_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/scripting_spec.js", "pdfjs-test/unit/stream_spec.js", "pdfjs-test/unit/struct_tree_spec.js", "pdfjs-test/unit/text_layer_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", "pdfjs-test/unit/xfa_formcalc_spec.js", "pdfjs-test/unit/xfa_parser_spec.js", "pdfjs-test/unit/xfa_serialize_data_spec.js", "pdfjs-test/unit/xfa_tohtml_spec.js", "pdfjs-test/unit/xml_spec.js"].map(function (moduleName) {
     return import(moduleName);
   }));
 

+ 26 - 1
lib/test/unit/pdf_find_controller_spec.js

@@ -34,6 +34,10 @@ var _pdf_find_controller = require("../../web/pdf_find_controller.js");
 var _pdf_link_service = require("../../web/pdf_link_service.js");
 
 const tracemonkeyFileName = "tracemonkey.pdf";
+const CMAP_PARAMS = {
+  cMapUrl: _is_node.isNodeJS ? "./external/bcmaps/" : "../../../external/bcmaps/",
+  cMapPacked: true
+};
 
 class MockLinkService extends _pdf_link_service.SimpleLinkService {
   constructor() {
@@ -61,7 +65,8 @@ class MockLinkService extends _pdf_link_service.SimpleLinkService {
 }
 
 async function initPdfFindController(filename) {
-  const loadingTask = (0, _api.getDocument)((0, _test_utils.buildGetDocumentParams)(filename || tracemonkeyFileName));
+  const loadingTask = (0, _api.getDocument)((0, _test_utils.buildGetDocumentParams)(filename || tracemonkeyFileName, { ...CMAP_PARAMS
+  }));
   const pdfDocument = await loadingTask.promise;
   const eventBus = new _event_utils.EventBus();
   const linkService = new MockLinkService();
@@ -533,4 +538,24 @@ describe("pdf_find_controller", function () {
       pageMatchesLength: [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
     });
   });
+  it("performs a search in a text containing some Hangul syllables", async function () {
+    const {
+      eventBus,
+      pdfFindController
+    } = await initPdfFindController("bug1771477.pdf");
+    await testSearch({
+      eventBus,
+      pdfFindController,
+      state: {
+        query: "안녕하세요 세계"
+      },
+      matchesPerPage: [1],
+      selectedMatch: {
+        pageIndex: 0,
+        matchIndex: 0
+      },
+      pageMatches: [[139]],
+      pageMatchesLength: [[8]]
+    });
+  });
 });

+ 14 - 0
lib/test/unit/scripting_spec.js

@@ -137,6 +137,7 @@ describe("Scripting", function () {
       expect(send_queue.has(refId)).toEqual(true);
       expect(send_queue.get(refId)).toEqual({
         id: refId,
+        siblings: null,
         value: expected,
         formattedValue: null
       });
@@ -344,6 +345,7 @@ describe("Scripting", function () {
       expect(send_queue.has(refId)).toEqual(true);
       expect(send_queue.get(refId)).toEqual({
         id: refId,
+        siblings: null,
         value: "hell",
         selRange: [4, 4]
       });
@@ -380,6 +382,7 @@ describe("Scripting", function () {
       expect(send_queue.has(refId)).toEqual(true);
       expect(send_queue.get(refId)).toEqual({
         id: refId,
+        siblings: null,
         value: "hella",
         selRange: [5, 5]
       });
@@ -450,6 +453,7 @@ describe("Scripting", function () {
       expect(send_queue.has(refId1)).toEqual(true);
       expect(send_queue.get(refId1)).toEqual({
         id: refId1,
+        siblings: null,
         value: "world",
         formattedValue: null
       });
@@ -710,6 +714,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refId)).toEqual(true);
         expect(send_queue.get(refId)).toEqual({
           id: refId,
+          siblings: null,
           value: "123456.789",
           formattedValue: null
         });
@@ -872,6 +877,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refId)).toEqual(true);
         expect(send_queue.get(refId)).toEqual({
           id: refId,
+          siblings: null,
           value: "321",
           formattedValue: null
         });
@@ -959,6 +965,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refIds[3])).toEqual(true);
         expect(send_queue.get(refIds[3])).toEqual({
           id: refIds[3],
+          siblings: null,
           value: 1,
           formattedValue: null
         });
@@ -971,6 +978,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refIds[3])).toEqual(true);
         expect(send_queue.get(refIds[3])).toEqual({
           id: refIds[3],
+          siblings: null,
           value: 3,
           formattedValue: null
         });
@@ -983,6 +991,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refIds[3])).toEqual(true);
         expect(send_queue.get(refIds[3])).toEqual({
           id: refIds[3],
+          siblings: null,
           value: 6,
           formattedValue: null
         });
@@ -1055,6 +1064,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refId)).toEqual(true);
         expect(send_queue.get(refId)).toEqual({
           id: refId,
+          siblings: null,
           value: "3F?",
           selRange: [3, 3]
         });
@@ -1081,6 +1091,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refId)).toEqual(true);
         expect(send_queue.get(refId)).toEqual({
           id: refId,
+          siblings: null,
           value: "3F?0",
           formattedValue: null
         });
@@ -1140,6 +1151,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refId)).toEqual(true);
         expect(send_queue.get(refId)).toEqual({
           id: refId,
+          siblings: null,
           value,
           selRange: [i, i]
         });
@@ -1198,6 +1210,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refId)).toEqual(true);
         expect(send_queue.get(refId)).toEqual({
           id: refId,
+          siblings: null,
           value,
           selRange: [i, i]
         });
@@ -1256,6 +1269,7 @@ describe("Scripting", function () {
         expect(send_queue.has(refId)).toEqual(true);
         expect(send_queue.get(refId)).toEqual({
           id: refId,
+          siblings: null,
           value,
           selRange: [i, i]
         });

+ 4 - 3
lib/test/unit/test_utils.js

@@ -29,6 +29,8 @@ exports.buildGetDocumentParams = buildGetDocumentParams;
 exports.createIdFactory = createIdFactory;
 exports.isEmptyObj = isEmptyObj;
 
+var _stream = require("../../core/stream.js");
+
 var _document = require("../../core/document.js");
 
 var _util = require("../../shared/util.js");
@@ -39,8 +41,6 @@ var _is_node = require("../../shared/is_node.js");
 
 var _primitives = require("../../core/primitives.js");
 
-var _stream = require("../../core/stream.js");
-
 const TEST_PDFS_PATH = _is_node.isNodeJS ? "./test/pdfs/" : "../pdfs/";
 exports.TEST_PDFS_PATH = TEST_PDFS_PATH;
 const CMAP_PARAMS = {
@@ -104,6 +104,7 @@ class XRefMock {
       send: () => {}
     });
     this._newRefNum = null;
+    this.stream = new _stream.NullStream();
 
     for (const key in array) {
       const obj = array[key];
@@ -113,7 +114,7 @@ class XRefMock {
 
   getNewRef() {
     if (this._newRefNum === null) {
-      this._newRefNum = Object.keys(this._map).length;
+      this._newRefNum = Object.keys(this._map).length || 1;
     }
 
     return _primitives.Ref.get(this._newRefNum++, 0);

+ 52 - 0
lib/test/unit/text_layer_spec.js

@@ -0,0 +1,52 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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 _text_layer = require("../../display/text_layer.js");
+
+var _test_utils = require("./test_utils.js");
+
+var _api = require("../../display/api.js");
+
+var _is_node = require("../../shared/is_node.js");
+
+describe("textLayer", function () {
+  it("creates textLayer from ReadableStream", async function () {
+    if (_is_node.isNodeJS) {
+      pending("document.createDocumentFragment is not supported in Node.js.");
+    }
+
+    const loadingTask = (0, _api.getDocument)((0, _test_utils.buildGetDocumentParams)("basicapi.pdf"));
+    const pdfDocument = await loadingTask.promise;
+    const page = await pdfDocument.getPage(1);
+    const textContentItemsStr = [];
+    const textLayerRenderTask = (0, _text_layer.renderTextLayer)({
+      textContentStream: page.streamTextContent(),
+      container: document.createDocumentFragment(),
+      viewport: page.getViewport(),
+      textContentItemsStr
+    });
+    expect(textLayerRenderTask instanceof _text_layer.TextLayerRenderTask).toEqual(true);
+    await textLayerRenderTask.promise;
+    expect(textContentItemsStr).toEqual(["Table Of Content", "", "Chapter 1", " ", "..........................................................", " ", "2", "", "Paragraph 1.1", " ", "......................................................", " ", "3", "", "page 1 / 3"]);
+  });
+});

+ 1 - 34
lib/test/unit/ui_utils_spec.js

@@ -24,39 +24,6 @@
 var _ui_utils = require("../../web/ui_utils.js");
 
 describe("ui_utils", function () {
-  describe("binary search", function () {
-    function isTrue(boolean) {
-      return boolean;
-    }
-
-    function isGreater3(number) {
-      return number > 3;
-    }
-
-    it("empty array", function () {
-      expect((0, _ui_utils.binarySearchFirstItem)([], isTrue)).toEqual(0);
-    });
-    it("single boolean entry", function () {
-      expect((0, _ui_utils.binarySearchFirstItem)([false], isTrue)).toEqual(1);
-      expect((0, _ui_utils.binarySearchFirstItem)([true], isTrue)).toEqual(0);
-    });
-    it("three boolean entries", function () {
-      expect((0, _ui_utils.binarySearchFirstItem)([true, true, true], isTrue)).toEqual(0);
-      expect((0, _ui_utils.binarySearchFirstItem)([false, true, true], isTrue)).toEqual(1);
-      expect((0, _ui_utils.binarySearchFirstItem)([false, false, true], isTrue)).toEqual(2);
-      expect((0, _ui_utils.binarySearchFirstItem)([false, false, false], isTrue)).toEqual(3);
-    });
-    it("three numeric entries", function () {
-      expect((0, _ui_utils.binarySearchFirstItem)([0, 1, 2], isGreater3)).toEqual(3);
-      expect((0, _ui_utils.binarySearchFirstItem)([2, 3, 4], isGreater3)).toEqual(2);
-      expect((0, _ui_utils.binarySearchFirstItem)([4, 5, 6], isGreater3)).toEqual(0);
-    });
-    it("three numeric entries and a start index", function () {
-      expect((0, _ui_utils.binarySearchFirstItem)([0, 1, 2, 3, 4], isGreater3, 2)).toEqual(4);
-      expect((0, _ui_utils.binarySearchFirstItem)([2, 3, 4], isGreater3, 2)).toEqual(2);
-      expect((0, _ui_utils.binarySearchFirstItem)([4, 5, 6], isGreater3, 1)).toEqual(1);
-    });
-  });
   describe("isValidRotation", function () {
     it("should reject non-integer angles", function () {
       expect((0, _ui_utils.isValidRotation)()).toEqual(false);
@@ -257,7 +224,7 @@ describe("ui_utils", function () {
 
       return {
         first: views[0],
-        last: views[views.length - 1],
+        last: views.at(-1),
         views,
         ids
       };

+ 122 - 0
lib/web/annotation_editor_layer_builder.js

@@ -0,0 +1,122 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.AnnotationEditorLayerBuilder = void 0;
+
+var _pdf = require("../pdf");
+
+var _l10n_utils = require("./l10n_utils.js");
+
+class AnnotationEditorLayerBuilder {
+  #uiManager;
+
+  constructor(options) {
+    this.pageDiv = options.pageDiv;
+    this.pdfPage = options.pdfPage;
+    this.annotationStorage = options.annotationStorage || null;
+    this.l10n = options.l10n || _l10n_utils.NullL10n;
+    this.annotationEditorLayer = null;
+    this.div = null;
+    this._cancelled = false;
+    this.#uiManager = options.uiManager;
+  }
+
+  async render(viewport, intent = "display") {
+    if (intent !== "display") {
+      return;
+    }
+
+    if (this._cancelled) {
+      return;
+    }
+
+    const clonedViewport = viewport.clone({
+      dontFlip: true
+    });
+
+    if (this.div) {
+      this.annotationEditorLayer.update({
+        viewport: clonedViewport
+      });
+      this.show();
+      return;
+    }
+
+    this.div = document.createElement("div");
+    this.div.className = "annotationEditorLayer";
+    this.div.tabIndex = 0;
+    this.pageDiv.append(this.div);
+    this.annotationEditorLayer = new _pdf.AnnotationEditorLayer({
+      uiManager: this.#uiManager,
+      div: this.div,
+      annotationStorage: this.annotationStorage,
+      pageIndex: this.pdfPage._pageIndex,
+      l10n: this.l10n,
+      viewport: clonedViewport
+    });
+    const parameters = {
+      viewport: clonedViewport,
+      div: this.div,
+      annotations: null,
+      intent
+    };
+    this.annotationEditorLayer.render(parameters);
+  }
+
+  cancel() {
+    this._cancelled = true;
+    this.destroy();
+  }
+
+  hide() {
+    if (!this.div) {
+      return;
+    }
+
+    this.div.hidden = true;
+  }
+
+  show() {
+    if (!this.div) {
+      return;
+    }
+
+    this.div.hidden = false;
+  }
+
+  destroy() {
+    if (!this.div) {
+      return;
+    }
+
+    this.pageDiv = null;
+    this.annotationEditorLayer.destroy();
+    this.div.remove();
+  }
+
+}
+
+exports.AnnotationEditorLayerBuilder = AnnotationEditorLayerBuilder;

+ 109 - 0
lib/web/annotation_editor_params.js

@@ -0,0 +1,109 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2022 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.AnnotationEditorParams = void 0;
+
+var _pdf = require("../pdf");
+
+class AnnotationEditorParams {
+  constructor(options, eventBus) {
+    this.eventBus = eventBus;
+    this.#bindListeners(options);
+  }
+
+  #bindListeners({
+    editorFreeTextFontSize,
+    editorFreeTextColor,
+    editorInkColor,
+    editorInkThickness,
+    editorInkOpacity
+  }) {
+    editorFreeTextFontSize.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: _pdf.AnnotationEditorParamsType.FREETEXT_SIZE,
+        value: editorFreeTextFontSize.valueAsNumber
+      });
+    });
+    editorFreeTextColor.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: _pdf.AnnotationEditorParamsType.FREETEXT_COLOR,
+        value: editorFreeTextColor.value
+      });
+    });
+    editorInkColor.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: _pdf.AnnotationEditorParamsType.INK_COLOR,
+        value: editorInkColor.value
+      });
+    });
+    editorInkThickness.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: _pdf.AnnotationEditorParamsType.INK_THICKNESS,
+        value: editorInkThickness.valueAsNumber
+      });
+    });
+    editorInkOpacity.addEventListener("input", evt => {
+      this.eventBus.dispatch("switchannotationeditorparams", {
+        source: this,
+        type: _pdf.AnnotationEditorParamsType.INK_OPACITY,
+        value: editorInkOpacity.valueAsNumber
+      });
+    });
+
+    this.eventBus._on("annotationeditorparamschanged", evt => {
+      for (const [type, value] of evt.details) {
+        switch (type) {
+          case _pdf.AnnotationEditorParamsType.FREETEXT_SIZE:
+            editorFreeTextFontSize.value = value;
+            break;
+
+          case _pdf.AnnotationEditorParamsType.FREETEXT_COLOR:
+            editorFreeTextColor.value = value;
+            break;
+
+          case _pdf.AnnotationEditorParamsType.INK_COLOR:
+            editorInkColor.value = value;
+            break;
+
+          case _pdf.AnnotationEditorParamsType.INK_THICKNESS:
+            editorInkThickness.value = value;
+            break;
+
+          case _pdf.AnnotationEditorParamsType.INK_OPACITY:
+            editorInkOpacity.value = value;
+            break;
+        }
+      }
+    });
+  }
+
+}
+
+exports.AnnotationEditorParams = AnnotationEditorParams;

+ 1 - 1
lib/web/annotation_layer_builder.js

@@ -96,7 +96,7 @@ class AnnotationLayerBuilder {
     } else {
       this.div = document.createElement("div");
       this.div.className = "annotationLayer";
-      this.pageDiv.appendChild(this.div);
+      this.pageDiv.append(this.div);
       parameters.div = this.div;
 
       _pdf.AnnotationLayer.render(parameters);

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff