Browse Source

:arrow_up: https://github.com/siyuan-note/siyuan/issues/7699

Vanessa 2 years ago
parent
commit
23f2af3501
57 changed files with 2104 additions and 1649 deletions
  1. 1 1
      app/src/asset/anno.ts
  2. 2 0
      app/src/asset/index.ts
  3. 20 28
      app/src/asset/pdf/annotation_editor_layer_builder.js
  4. 87 31
      app/src/asset/pdf/annotation_layer_builder.js
  5. 407 378
      app/src/asset/pdf/app.js
  6. 92 87
      app/src/asset/pdf/app_options.js
  7. 4 6
      app/src/asset/pdf/event_utils.js
  8. 31 27
      app/src/asset/pdf/genericcom.js
  9. 4 14
      app/src/asset/pdf/l10n_utils.js
  10. 1 3
      app/src/asset/pdf/password_prompt.js
  11. 4 1
      app/src/asset/pdf/pdf_attachment_viewer.js
  12. 2 8
      app/src/asset/pdf/pdf_cursor_tools.js
  13. 5 11
      app/src/asset/pdf/pdf_document_properties.js
  14. 3 4
      app/src/asset/pdf/pdf_find_bar.js
  15. 111 29
      app/src/asset/pdf/pdf_find_controller.js
  16. 14 0
      app/src/asset/pdf/pdf_link_service.js
  17. 19 2
      app/src/asset/pdf/pdf_outline_viewer.js
  18. 357 269
      app/src/asset/pdf/pdf_page_view.js
  19. 17 14
      app/src/asset/pdf/pdf_presentation_mode.js
  20. 10 30
      app/src/asset/pdf/pdf_scripting_manager.js
  21. 1 10
      app/src/asset/pdf/pdf_thumbnail_viewer.js
  22. 222 301
      app/src/asset/pdf/pdf_viewer.js
  23. 1 1
      app/src/asset/pdf/pdfjs.js
  24. 1 2
      app/src/asset/pdf/secondary_toolbar.js
  25. 27 18
      app/src/asset/pdf/struct_tree_layer_builder.js
  26. 5 5
      app/src/asset/pdf/text_highlighter.js
  27. 89 57
      app/src/asset/pdf/text_layer_builder.js
  28. 13 14
      app/src/asset/pdf/toolbar.js
  29. 35 11
      app/src/asset/pdf/ui_utils.js
  30. 0 2
      app/src/asset/pdf/view_history.js
  31. 145 190
      app/src/asset/pdf/viewer.js
  32. 24 30
      app/src/asset/pdf/xfa_layer_builder.js
  33. 12 6
      app/src/assets/scss/pdf/_pdf.scss
  34. 142 34
      app/src/assets/scss/pdf/annotation_layer_builder.scss
  35. 65 23
      app/src/assets/scss/pdf/pdf_viewer.scss
  36. 1 1
      app/stage/protyle/js/pdf/pdf.js
  37. 1 1
      app/stage/protyle/js/pdf/pdf.worker.js
  38. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitDingbats.pfb
  39. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitFixed.pfb
  40. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitFixedBold.pfb
  41. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitFixedBoldItalic.pfb
  42. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitFixedItalic.pfb
  43. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSans.pfb
  44. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSansBold.pfb
  45. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSansBoldItalic.pfb
  46. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSansItalic.pfb
  47. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSerif.pfb
  48. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSerifBold.pfb
  49. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSerifBoldItalic.pfb
  50. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSerifItalic.pfb
  51. BIN
      app/stage/protyle/js/pdf/standard_fonts/FoxitSymbol.pfb
  52. 27 0
      app/stage/protyle/js/pdf/standard_fonts/LICENSE_FOXIT
  53. 102 0
      app/stage/protyle/js/pdf/standard_fonts/LICENSE_LIBERATION
  54. BIN
      app/stage/protyle/js/pdf/standard_fonts/LiberationSans-Bold.ttf
  55. BIN
      app/stage/protyle/js/pdf/standard_fonts/LiberationSans-BoldItalic.ttf
  56. BIN
      app/stage/protyle/js/pdf/standard_fonts/LiberationSans-Italic.ttf
  57. BIN
      app/stage/protyle/js/pdf/standard_fonts/LiberationSans-Regular.ttf

+ 1 - 1
app/src/asset/anno.ts

@@ -531,7 +531,7 @@ export const getHighlight = (element: HTMLElement) => {
 const showHighlight = (selected: IPdfAnno, pdf: any, hl?: boolean) => {
     const pageIndex = selected.index;
     const page = pdf.pdfViewer.getPageView(pageIndex);
-    let textLayerElement = page.textLayer.textLayerDiv;
+    let textLayerElement = page.textLayer.div;
     if (!textLayerElement.lastElementChild) {
         return;
     }

+ 2 - 0
app/src/asset/index.ts

@@ -284,6 +284,8 @@ export class Asset extends Model {
                   <svg><use xlink:href="#iconPlay"></use></svg>
                 </button>
                 <span id="scrollPage" class="fn__none"></span>
+                <span id="print" class="fn__none"></span>
+                <span id="secondaryPrint" class="fn__none"></span>
                 <span id="viewBookmark" class="fn__none"></span>
                 <span id="secondaryViewBookmark" class="fn__none"></span>
                 <button id="secondaryToolbarToggle" class="toolbarButton b3-tooltips b3-tooltips__sw" aria-label="${window.siyuan.languages.more}" tabindex="36" aria-expanded="false" aria-controls="secondaryToolbar">

+ 20 - 28
app/src/asset/pdf/annotation_editor_layer_builder.js

@@ -16,12 +16,9 @@
 /** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
 // eslint-disable-next-line max-len
 /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
-/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
 // eslint-disable-next-line max-len
 /** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
 // eslint-disable-next-line max-len
-/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
-// eslint-disable-next-line max-len
 /** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
 /** @typedef {import("./interfaces").IL10n} IL10n */
 
@@ -30,13 +27,11 @@ import { NullL10n } from "./l10n_utils.js";
 
 /**
  * @typedef {Object} AnnotationEditorLayerBuilderOptions
- * @property {number} mode - Editor mode
+ * @property {AnnotationEditorUIManager} [uiManager]
  * @property {HTMLDivElement} pageDiv
  * @property {PDFPageProxy} pdfPage
- * @property {TextAccessibilityManager} accessibilityManager
- * @property {AnnotationStorage} annotationStorage
- * @property {IL10n} l10n - Localization service.
- * @property {AnnotationEditorUIManager} uiManager
+ * @property {IL10n} [l10n]
+ * @property {TextAccessibilityManager} [accessibilityManager]
  */
 
 class AnnotationEditorLayerBuilder {
@@ -48,7 +43,6 @@ class AnnotationEditorLayerBuilder {
   constructor(options) {
     this.pageDiv = options.pageDiv;
     this.pdfPage = options.pdfPage;
-    this.annotationStorage = options.annotationStorage || null;
     this.accessibilityManager = options.accessibilityManager;
     this.l10n = options.l10n || NullL10n;
     this.annotationEditorLayer = null;
@@ -78,57 +72,55 @@ class AnnotationEditorLayerBuilder {
     }
 
     // Create an AnnotationEditor layer div
-    this.div = document.createElement("div");
-    this.div.className = "annotationEditorLayer";
-    this.div.tabIndex = 0;
-    this.pageDiv.append(this.div);
+    const div = (this.div = document.createElement("div"));
+    div.className = "annotationEditorLayer";
+    div.tabIndex = 0;
+    div.hidden = true;
+    this.pageDiv.append(div);
 
     this.annotationEditorLayer = new AnnotationEditorLayer({
       uiManager: this.#uiManager,
-      div: this.div,
-      annotationStorage: this.annotationStorage,
+      div,
       accessibilityManager: this.accessibilityManager,
-      pageIndex: this.pdfPage._pageIndex,
+      pageIndex: this.pdfPage.pageNumber - 1,
       l10n: this.l10n,
       viewport: clonedViewport,
     });
 
     const parameters = {
       viewport: clonedViewport,
-      div: this.div,
+      div,
       annotations: null,
       intent,
     };
 
     this.annotationEditorLayer.render(parameters);
+    this.show();
   }
 
   cancel() {
     this._cancelled = true;
-    this.destroy();
-  }
 
-  hide() {
     if (!this.div) {
       return;
     }
-    this.div.hidden = true;
+    this.pageDiv = null;
+    this.annotationEditorLayer.destroy();
+    this.div.remove();
   }
 
-  show() {
+  hide() {
     if (!this.div) {
       return;
     }
-    this.div.hidden = false;
+    this.div.hidden = true;
   }
 
-  destroy() {
-    if (!this.div) {
+  show() {
+    if (!this.div || this.annotationEditorLayer.isEmpty) {
       return;
     }
-    this.pageDiv = null;
-    this.annotationEditorLayer.destroy();
-    this.div.remove();
+    this.div.hidden = false;
   }
 }
 

+ 87 - 31
app/src/asset/pdf/annotation_layer_builder.js

@@ -24,6 +24,7 @@
 
 import { AnnotationLayer } from "./pdfjs";
 import { NullL10n } from "./l10n_utils.js";
+import { PresentationModeState } from "./ui_utils.js";
 
 /**
  * @typedef {Object} AnnotationLayerBuilderOptions
@@ -40,12 +41,15 @@ import { NullL10n } from "./l10n_utils.js";
  * @property {Promise<boolean>} [hasJSActionsPromise]
  * @property {Promise<Object<string, Array<Object>> | null>}
  *   [fieldObjectsPromise]
- * @property {Object} [mouseState]
  * @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
- * @property {TextAccessibilityManager} accessibilityManager
+ * @property {TextAccessibilityManager} [accessibilityManager]
  */
 
 class AnnotationLayerBuilder {
+  #numAnnotations = 0;
+
+  #onPresentationModeChanged = null;
+
   /**
    * @param {AnnotationLayerBuilderOptions} options
    */
@@ -61,7 +65,6 @@ class AnnotationLayerBuilder {
     enableScripting = false,
     hasJSActionsPromise = null,
     fieldObjectsPromise = null,
-    mouseState = null,
     annotationCanvasMap = null,
     accessibilityManager = null,
   }) {
@@ -74,14 +77,14 @@ class AnnotationLayerBuilder {
     this.l10n = l10n;
     this.annotationStorage = annotationStorage;
     this.enableScripting = enableScripting;
-    this._hasJSActionsPromise = hasJSActionsPromise;
-    this._fieldObjectsPromise = fieldObjectsPromise;
-    this._mouseState = mouseState;
+    this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false);
+    this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
     this._annotationCanvasMap = annotationCanvasMap;
     this._accessibilityManager = accessibilityManager;
 
     this.div = null;
     this._cancelled = false;
+    this._eventBus = linkService.eventBus;
   }
 
   /**
@@ -91,18 +94,41 @@ class AnnotationLayerBuilder {
    *   annotations is complete.
    */
   async render(viewport, intent = "display") {
-    const [annotations, hasJSActions = false, fieldObjects = null] =
-      await Promise.all([
-        this.pdfPage.getAnnotations({ intent }),
-        this._hasJSActionsPromise,
-        this._fieldObjectsPromise,
-      ]);
-
-    if (this._cancelled || annotations.length === 0) {
+    if (this.div) {
+      if (this._cancelled || this.#numAnnotations === 0) {
+        return;
+      }
+      // If an annotationLayer already exists, refresh its children's
+      // transformation matrices.
+      AnnotationLayer.update({
+        viewport: viewport.clone({ dontFlip: true }),
+        div: this.div,
+        annotationCanvasMap: this._annotationCanvasMap,
+      });
       return;
     }
 
-    const parameters = {
+    const [annotations, hasJSActions, fieldObjects] = await Promise.all([
+      this.pdfPage.getAnnotations({ intent }),
+      this._hasJSActionsPromise,
+      this._fieldObjectsPromise,
+    ]);
+    if (this._cancelled) {
+      return;
+    }
+    this.#numAnnotations = annotations.length;
+
+    // Create an annotation layer div and render the annotations
+    // if there is at least one annotation.
+    this.div = document.createElement("div");
+    this.div.className = "annotationLayer";
+    this.pageDiv.append(this.div);
+
+    if (this.#numAnnotations === 0) {
+      this.hide();
+      return;
+    }
+    AnnotationLayer.render({
       viewport: viewport.clone({ dontFlip: true }),
       div: this.div,
       annotations,
@@ -115,30 +141,37 @@ class AnnotationLayerBuilder {
       enableScripting: this.enableScripting,
       hasJSActions,
       fieldObjects,
-      mouseState: this._mouseState,
       annotationCanvasMap: this._annotationCanvasMap,
       accessibilityManager: this._accessibilityManager,
-    };
+    });
+    // NOTE this.l10n.translate(this.div);
 
-    if (this.div) {
-      // If an annotationLayer already exists, refresh its children's
-      // transformation matrices.
-      AnnotationLayer.update(parameters);
-    } else {
-      // Create an annotation layer div and render the annotations
-      // if there is at least one annotation.
-      this.div = document.createElement("div");
-      this.div.className = "annotationLayer";
-      this.pageDiv.append(this.div);
-      parameters.div = this.div;
-
-      AnnotationLayer.render(parameters);
-      this.l10n.translate(this.div);
+    // Ensure that interactive form elements in the annotationLayer are
+    // disabled while PresentationMode is active (see issue 12232).
+    if (this.linkService.isInPresentationMode) {
+      this.#updatePresentationModeState(PresentationModeState.FULLSCREEN);
+    }
+    if (!this.#onPresentationModeChanged) {
+      this.#onPresentationModeChanged = evt => {
+        this.#updatePresentationModeState(evt.state);
+      };
+      this._eventBus?._on(
+        "presentationmodechanged",
+        this.#onPresentationModeChanged
+      );
     }
   }
 
   cancel() {
     this._cancelled = true;
+
+    if (this.#onPresentationModeChanged) {
+      this._eventBus?._off(
+        "presentationmodechanged",
+        this.#onPresentationModeChanged
+      );
+      this.#onPresentationModeChanged = null;
+    }
   }
 
   hide() {
@@ -147,6 +180,29 @@ class AnnotationLayerBuilder {
     }
     this.div.hidden = true;
   }
+
+  #updatePresentationModeState(state) {
+    if (!this.div) {
+      return;
+    }
+    let disableFormElements = false;
+
+    switch (state) {
+      case PresentationModeState.FULLSCREEN:
+        disableFormElements = true;
+        break;
+      case PresentationModeState.NORMAL:
+        break;
+      default:
+        return;
+    }
+    for (const section of this.div.childNodes) {
+      if (section.hasAttribute("data-internal-link")) {
+        continue;
+      }
+      section.inert = disableFormElements;
+    }
+  }
 }
 
 export { AnnotationLayerBuilder };

File diff suppressed because it is too large
+ 407 - 378
app/src/asset/pdf/app.js


+ 92 - 87
app/src/asset/pdf/app_options.js

@@ -12,34 +12,33 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 import { Constants } from '../../constants'
 
-const compatibilityParams = Object.create(null)
-if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
+const compatibilityParams = Object.create(null);
+if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
   if (
-    typeof PDFJSDev !== 'undefined' &&
-    PDFJSDev.test('LIB') &&
-    typeof navigator === 'undefined'
+    typeof PDFJSDev !== "undefined" &&
+    PDFJSDev.test("LIB") &&
+    typeof navigator === "undefined"
   ) {
-    globalThis.navigator = Object.create(null)
+    globalThis.navigator = Object.create(null);
   }
-  const userAgent = navigator.userAgent || ''
-  const platform = navigator.platform || ''
-  const maxTouchPoints = navigator.maxTouchPoints || 1
+  const userAgent = navigator.userAgent || "";
+  const platform = navigator.platform || "";
+  const maxTouchPoints = navigator.maxTouchPoints || 1;
 
-  const isAndroid = /Android/.test(userAgent)
+  const isAndroid = /Android/.test(userAgent);
   const isIOS =
     /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) ||
-    (platform === 'MacIntel' && maxTouchPoints > 1);
+    (platform === "MacIntel" && maxTouchPoints > 1);
 
   // Limit canvas size to 5 mega-pixels on mobile.
   // Support: Android, iOS
-  (function checkCanvasSizeLimitation () {
+  (function checkCanvasSizeLimitation() {
     if (isIOS || isAndroid) {
-      compatibilityParams.maxCanvasPixels = 5242880
+      compatibilityParams.maxCanvasPixels = 5242880;
     }
-  })()
+  })();
 }
 
 const OptionKind = {
@@ -47,7 +46,7 @@ const OptionKind = {
   API: 0x04,
   WORKER: 0x08,
   PREFERENCE: 0x80,
-}
+};
 
 /**
  * NOTE: These options are used to generate the `default_preferences.json` file,
@@ -70,9 +69,14 @@ const defaultOptions = {
     value: 0,
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
   },
+  defaultZoomDelay: {
+    /** @type {number} */
+    value: 400,
+    kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
+  },
   defaultZoomValue: {
     /** @type {string} */
-    value: '',
+    value: "",
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
   },
   disableHistory: {
@@ -97,12 +101,12 @@ const defaultOptions = {
   },
   enableScripting: {
     /** @type {boolean} */
-    value: typeof PDFJSDev === 'undefined' || !PDFJSDev.test('CHROME'),
+    value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"),
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
   },
   externalLinkRel: {
     /** @type {string} */
-    value: 'noopener noreferrer nofollow',
+    value: "noopener noreferrer nofollow",
     kind: OptionKind.VIEWER,
   },
   externalLinkTarget: {
@@ -122,7 +126,7 @@ const defaultOptions = {
   },
   imageResourcesPath: {
     /** @type {string} */
-    value: './images/',
+    value: "./images/",
     kind: OptionKind.VIEWER,
   },
   maxCanvasPixels: {
@@ -137,17 +141,17 @@ const defaultOptions = {
   },
   pageColorsBackground: {
     /** @type {string} */
-    value: 'Canvas',
+    value: "Canvas",
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
   },
   pageColorsForeground: {
     /** @type {string} */
-    value: 'CanvasText',
+    value: "CanvasText",
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
   },
   pdfBugEnabled: {
     /** @type {boolean} */
-    value: typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION'),
+    value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION"),
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
   },
   printResolution: {
@@ -182,7 +186,7 @@ const defaultOptions = {
   },
   viewerCssTheme: {
     /** @type {number} */
-    value: typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') ? 2 : 0,
+    value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME") ? 2 : 0,
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
   },
   viewOnLoad: {
@@ -223,7 +227,7 @@ const defaultOptions = {
   },
   docBaseUrl: {
     /** @type {string} */
-    value: '',
+    value: "",
     kind: OptionKind.API,
   },
   enableXfa: {
@@ -241,6 +245,11 @@ const defaultOptions = {
     value: true,
     kind: OptionKind.API,
   },
+  isOffscreenCanvasSupported: {
+    /** @type {boolean} */
+    value: true,
+    kind: OptionKind.API,
+  },
   maxImageSize: {
     /** @type {number} */
     value: -1,
@@ -253,10 +262,7 @@ const defaultOptions = {
   },
   standardFontDataUrl: {
     /** @type {string} */
-    value:
-      typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')
-        ? '../external/standard_fonts/'
-        : '../web/standard_fonts/',
+    value: "standard_fonts/", // NOTE
     kind: OptionKind.API,
   },
   verbosity: {
@@ -273,131 +279,130 @@ const defaultOptions = {
   workerSrc: {
     /** @type {string} */
     // NOTE
-    value: `${Constants.PROTYLE_CDN}/js/pdf/pdf.worker.js?v=3.0.150`,
+    value: `${Constants.PROTYLE_CDN}/js/pdf/pdf.worker.js?v=3.4.120`,
     kind: OptionKind.WORKER,
   },
-}
+};
 if (
-  typeof PDFJSDev === 'undefined' ||
-  PDFJSDev.test('!PRODUCTION || GENERIC')
+  typeof PDFJSDev === "undefined" ||
+  PDFJSDev.test("!PRODUCTION || GENERIC")
 ) {
   defaultOptions.defaultUrl = {
     /** @type {string} */
-    value: 'compressed.tracemonkey-pldi-09.pdf',
+    value: "compressed.tracemonkey-pldi-09.pdf",
     kind: OptionKind.VIEWER,
-  }
+  };
   defaultOptions.disablePreferences = {
     /** @type {boolean} */
-    value: typeof PDFJSDev !== 'undefined' && PDFJSDev.test('TESTING'),
+    value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
     kind: OptionKind.VIEWER,
-  }
+  };
   defaultOptions.locale = {
     /** @type {string} */
-    value: navigator.language || 'en-US',
+    value: navigator.language || "en-US",
     kind: OptionKind.VIEWER,
-  }
+  };
   defaultOptions.renderer = {
     /** @type {string} */
-    value: 'canvas',
+    value: "canvas",
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
-  }
+  };
   defaultOptions.sandboxBundleSrc = {
     /** @type {string} */
     value:
-      typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')
-        ? '../build/dev-sandbox/pdf.sandbox.js'
-        : '../build/pdf.sandbox.js',
+      typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")
+        ? "../build/dev-sandbox/pdf.sandbox.js"
+        : "../build/pdf.sandbox.js",
     kind: OptionKind.VIEWER,
-  }
-} else if (PDFJSDev.test('CHROME')) {
+  };
+} else if (PDFJSDev.test("CHROME")) {
   defaultOptions.defaultUrl = {
     /** @type {string} */
-    value: '',
+    value: "",
     kind: OptionKind.VIEWER,
-  }
+  };
   defaultOptions.disableTelemetry = {
     /** @type {boolean} */
     value: false,
     kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
-  }
+  };
   defaultOptions.sandboxBundleSrc = {
     /** @type {string} */
-    value: '../build/pdf.sandbox.js',
+    value: "../build/pdf.sandbox.js",
     kind: OptionKind.VIEWER,
-  }
+  };
 }
 
-const userOptions = Object.create(null)
+const userOptions = Object.create(null);
 
 class AppOptions {
-  constructor () {
-    throw new Error('Cannot initialize AppOptions.')
+  constructor() {
+    throw new Error("Cannot initialize AppOptions.");
   }
 
-  static get (name) {
-    const userOption = userOptions[name]
+  static get(name) {
+    const userOption = userOptions[name];
     if (userOption !== undefined) {
-      return userOption
+      return userOption;
     }
-    const defaultOption = defaultOptions[name]
+    const defaultOption = defaultOptions[name];
     if (defaultOption !== undefined) {
-      return compatibilityParams[name] ?? defaultOption.value
+      return compatibilityParams[name] ?? defaultOption.value;
     }
-    return undefined
+    return undefined;
   }
 
-  static getAll (kind = null) {
-    const options = Object.create(null)
+  static getAll(kind = null) {
+    const options = Object.create(null);
     for (const name in defaultOptions) {
-      const defaultOption = defaultOptions[name]
+      const defaultOption = defaultOptions[name];
       if (kind) {
         if ((kind & defaultOption.kind) === 0) {
-          continue
+          continue;
         }
         if (kind === OptionKind.PREFERENCE) {
           const value = defaultOption.value,
-            valueType = typeof value
+            valueType = typeof value;
 
           if (
-            valueType === 'boolean' ||
-            valueType === 'string' ||
-            (valueType === 'number' && Number.isInteger(value))
+            valueType === "boolean" ||
+            valueType === "string" ||
+            (valueType === "number" && Number.isInteger(value))
           ) {
-            options[name] = value
-            continue
+            options[name] = value;
+            continue;
           }
-          throw new Error(`Invalid type for preference: ${name}`)
+          throw new Error(`Invalid type for preference: ${name}`);
         }
       }
-      const userOption = userOptions[name]
+      const userOption = userOptions[name];
       options[name] =
         userOption !== undefined
           ? userOption
-          : compatibilityParams[name] ?? defaultOption.value
+          : compatibilityParams[name] ?? defaultOption.value;
     }
-    return options
+    return options;
   }
 
-  static set (name, value) {
-    userOptions[name] = value
+  static set(name, value) {
+    userOptions[name] = value;
   }
 
-  static setAll (options) {
+  static setAll(options) {
     for (const name in options) {
-      userOptions[name] = options[name]
+      userOptions[name] = options[name];
     }
   }
 
-  static remove (name) {
-    delete userOptions[name]
+  static remove(name) {
+    delete userOptions[name];
   }
+}
 
-  /**
-   * @ignore
-   */
-  static _hasUserOptions () {
-    return Object.keys(userOptions).length > 0
-  }
+if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
+  AppOptions._hasUserOptions = function () {
+    return Object.keys(userOptions).length > 0;
+  };
 }
 
-export { AppOptions, compatibilityParams, OptionKind }
+export { AppOptions, compatibilityParams, OptionKind };

+ 4 - 6
app/src/asset/pdf/event_utils.js

@@ -75,9 +75,7 @@ function waitOnEventOrTimeout({ target, name, delay = 0 }) {
  * and `off` methods. To raise an event, the `dispatch` method shall be used.
  */
 class EventBus {
-  constructor() {
-    this._listeners = Object.create(null);
-  }
+  #listeners = Object.create(null);
 
   /**
    * @param {string} eventName
@@ -108,7 +106,7 @@ class EventBus {
    * @param {Object} data
    */
   dispatch(eventName, data) {
-    const eventListeners = this._listeners[eventName];
+    const eventListeners = this.#listeners[eventName];
     if (!eventListeners || eventListeners.length === 0) {
       return;
     }
@@ -139,7 +137,7 @@ class EventBus {
    * @ignore
    */
   _on(eventName, listener, options = null) {
-    const eventListeners = (this._listeners[eventName] ||= []);
+    const eventListeners = (this.#listeners[eventName] ||= []);
     eventListeners.push({
       listener,
       external: options?.external === true,
@@ -151,7 +149,7 @@ class EventBus {
    * @ignore
    */
   _off(eventName, listener, options = null) {
-    const eventListeners = this._listeners[eventName];
+    const eventListeners = this.#listeners[eventName];
     if (!eventListeners) {
       return;
     }

+ 31 - 27
app/src/asset/pdf/genericcom.js

@@ -14,10 +14,8 @@
  */
 
 import { BasePreferences } from "./preferences.js";
-// NOTE
-// import { DownloadManager } from "./download_manager.js";
 import { GenericScripting } from "./generic_scripting.js";
-import { shadow } from './pdfjs'
+import {shadow} from "./pdfjs";
 
 if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
   throw new Error(
@@ -37,44 +35,48 @@ class GenericPreferences extends BasePreferences {
   }
 }
 
-// NOTE
-class GenericExternalServices {
-  constructor () {
-    throw new Error('Cannot initialize DefaultExternalServices.')
+class DefaultExternalServices {
+  constructor() {
+    throw new Error("Cannot initialize DefaultExternalServices.");
   }
 
-  static updateFindControlState (data) {}
+  static updateFindControlState(data) {}
 
-  static updateFindMatchesCount (data) {}
+  static updateFindMatchesCount(data) {}
 
-  static initPassiveLoading (callbacks) {}
+  static initPassiveLoading(callbacks) {}
 
-  static async fallback (data) {}
+  static reportTelemetry(data) {}
 
-  static reportTelemetry (data) {}
+  static get supportsPinchToZoom() {
+    return shadow(this, "supportsPinchToZoom", true);
+  }
 
-  static get supportsIntegratedFind () {
-    return shadow(this, 'supportsIntegratedFind', false)
+  static get supportsIntegratedFind() {
+    return shadow(this, "supportsIntegratedFind", false);
   }
 
-  static get supportsDocumentFonts () {
-    return shadow(this, 'supportsDocumentFonts', true)
+  static get supportsDocumentFonts() {
+    return shadow(this, "supportsDocumentFonts", true);
   }
 
-  static get supportedMouseWheelZoomModifierKeys () {
-    return shadow(this, 'supportedMouseWheelZoomModifierKeys', {
+  static get supportedMouseWheelZoomModifierKeys() {
+    return shadow(this, "supportedMouseWheelZoomModifierKeys", {
       ctrlKey: true,
       metaKey: true,
-    })
+    });
   }
 
-  static get isInAutomation () {
-    return shadow(this, 'isInAutomation', false)
+  static get isInAutomation() {
+    return shadow(this, "isInAutomation", false);
   }
 
-  static createDownloadManager(options) {
-    // NOTE
-    // return new DownloadManager();
+  static updateEditorStates(data) {
+    throw new Error("Not implemented: updateEditorStates");
+  }
+
+  static createDownloadManager() {
+    // NOTE return new DownloadManager();
   }
 
   static createPreferences() {
@@ -82,13 +84,15 @@ class GenericExternalServices {
   }
 
   static createL10n({ locale = "en-US" }) {
-    // NOTE
-    // return new GenericL10n(locale);
+    // NOTE return new GenericL10n(locale);
   }
 
   static createScripting({ sandboxBundleSrc }) {
     return new GenericScripting(sandboxBundleSrc);
   }
 }
+
 // NOTE
-export { GenericCom, GenericExternalServices };
+// PDFViewerApplication.externalServices = GenericExternalServices;
+
+export { GenericCom, DefaultExternalServices };

+ 4 - 14
app/src/asset/pdf/l10n_utils.js

@@ -38,12 +38,6 @@ const DEFAULT_L10N_STRINGS = {
   document_properties_linearized_yes: "Yes",
   document_properties_linearized_no: "No",
 
-  print_progress_percent: "{{progress}}%",
-
-  "toggle_sidebar.title": "Toggle Sidebar",
-  "toggle_sidebar_notification2.title":
-    "Toggle Sidebar (document contains outline/attachments/layers)",
-
   additional_layers: "Additional Layers",
   page_landmark: "Page {{page}}",
   thumb_page_title: "Page {{page}}",
@@ -57,24 +51,17 @@ const DEFAULT_L10N_STRINGS = {
   "find_match_count_limit[other]": "More than {{limit}} matches",
   find_not_found: "Phrase not found",
 
-  error_version_info: "PDF.js v{{version}} (build: {{build}})",
-  error_message: "Message: {{message}}",
-  error_stack: "Stack: {{stack}}",
-  error_file: "File: {{file}}",
-  error_line: "Line: {{line}}",
-  rendering_error: "An error occurred while rendering the page.",
-
   page_scale_width: "Page Width",
   page_scale_fit: "Page Fit",
   page_scale_auto: "Automatic Zoom",
   page_scale_actual: "Actual Size",
   page_scale_percent: "{{scale}}%",
 
-  loading: "Loading…",
   loading_error: "An error occurred while loading the PDF.",
   invalid_file_error: "Invalid or corrupted PDF file.",
   missing_file_error: "Missing PDF file.",
   unexpected_response_error: "Unexpected server response.",
+  rendering_error: "An error occurred while rendering the page.",
 
   printing_not_supported:
     "Warning: Printing is not fully supported by this browser.",
@@ -87,6 +74,9 @@ const DEFAULT_L10N_STRINGS = {
   editor_ink2_aria_label: "Draw Editor",
   editor_ink_canvas_aria_label: "User-created image",
 };
+if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
+  DEFAULT_L10N_STRINGS.print_progress_percent = "{{progress}}%";
+}
 
 function getL10nFallback(key, args) {
   switch (key) {

+ 1 - 3
app/src/asset/pdf/password_prompt.js

@@ -85,9 +85,7 @@ class PasswordPrompt {
       this.input.focus();
     }
     // NOTE
-    this.label.textContent = window.siyuan.languages[`password_${passwordIncorrect
-      ? 'invalid'
-      : 'label'}`]
+    this.label.textContent = window.siyuan.languages[`password_${passwordIncorrect ? 'invalid' : 'label'}`]
   }
 
   async close() {

+ 4 - 1
app/src/asset/pdf/pdf_attachment_viewer.js

@@ -118,7 +118,10 @@ class PDFAttachmentViewer extends BaseTreeViewer {
     for (const name of names) {
       const item = attachments[name];
       const content = item.content,
-        filename = getFilenameFromUrl(item.filename);
+        filename = getFilenameFromUrl(
+          item.filename,
+          /* onlyStripPath = */ true
+        );
 
       const div = document.createElement("div");
       div.className = "treeItem";

+ 2 - 8
app/src/asset/pdf/pdf_cursor_tools.js

@@ -13,15 +13,9 @@
  * limitations under the License.
  */
 
+import { CursorTool, PresentationModeState } from "./ui_utils.js";
 import { AnnotationEditorType } from "./pdfjs";
 import { GrabToPan } from "./grab_to_pan.js";
-import { PresentationModeState } from "./ui_utils.js";
-
-const CursorTool = {
-  SELECT: 0, // The default value.
-  HAND: 1,
-  ZOOM: 2,
-};
 
 /**
  * @typedef {Object} PDFCursorToolsOptions
@@ -175,4 +169,4 @@ class PDFCursorTools {
   }
 }
 
-export { CursorTool, PDFCursorTools };
+export { PDFCursorTools };

+ 5 - 11
app/src/asset/pdf/pdf_document_properties.js

@@ -239,11 +239,9 @@ class PDFDocumentProperties {
     }
     // NOTE
     if (mb >= 1) {
-      return `${mb >= 1 && (+mb.toPrecision(
-        3)).toLocaleString()} MB ${fileSize.toLocaleString()} bytes`
+      return `${mb >= 1 && (+mb.toPrecision(3)).toLocaleString()} MB ${fileSize.toLocaleString()} bytes`
     }
-    return `${mb < 1 && (+kb.toPrecision(
-      3)).toLocaleString()} KB (${fileSize.toLocaleString()} bytes`
+    return `${mb < 1 && (+kb.toPrecision(3)).toLocaleString()} KB (${fileSize.toLocaleString()} bytes`
   }
 
   async #parsePageSize(pageSizeInches, pagesRotation) {
@@ -313,15 +311,11 @@ class PDFDocumentProperties {
     // NOTE
     const [{ width, height }, unit, name, orientation] = await Promise.all([
       this._isNonMetricLocale ? sizeInches : sizeMillimeters,
-      this._isNonMetricLocale
-        ? window.siyuan.languages.unitInches
-        : window.siyuan.languages.unitMillimeters,
+      this._isNonMetricLocale ? window.siyuan.languages.unitInches : window.siyuan.languages.unitMillimeters,
       rawName &&
       window.siyuan.languages[`document_properties_page_size_name_${rawName.toLowerCase()}`],
-      window.siyuan.languages[`document_properties_page_size_orientation_${isPortrait
-        ? 'portrait'
-        : 'landscape'}`],
-    ])
+      window.siyuan.languages[`document_properties_page_size_orientation_${isPortrait ? 'portrait' : 'landscape'}`],
+    ]);
     if (name) {
       return `${width.toLocaleString()} × ${height.toLocaleString()} ${unit} (${name}, ${orientation})`
     }

+ 3 - 4
app/src/asset/pdf/pdf_find_bar.js

@@ -149,9 +149,8 @@ class PDFFindBar {
         status = "notFound";
         break;
       case FindState.WRAPPED:
-        findMsg = window.siyuan.languages.find_not_found[`find_reached_${previous
-          ? 'top'
-          : 'bottom'}`]
+        // NOTE
+        findMsg = window.siyuan.languages.find_not_found[`find_reached_${previous ? 'top' : 'bottom'}`]
         break;
     }
     this.findField.setAttribute("data-status", status);
@@ -165,7 +164,7 @@ class PDFFindBar {
 
   updateResultsCount({ current = 0, total = 0 } = {}) {
     const limit = MATCHES_COUNT_LIMIT;
-    // // NOTE
+    // NOTE
     let matchCountMsg = "";
 
     if (total > 0) {

+ 111 - 29
app/src/asset/pdf/pdf_find_controller.js

@@ -76,9 +76,7 @@ const DIACRITICS_EXCEPTION = new Set([
   // https://www.compart.com/fr/unicode/combining/132
   0x0f74,
 ]);
-const DIACRITICS_EXCEPTION_STR = [...DIACRITICS_EXCEPTION.values()]
-  .map(x => String.fromCharCode(x))
-  .join("");
+let DIACRITICS_EXCEPTION_STR; // Lazily initialized, see below.
 
 const DIACRITICS_REG_EXP = /\p{M}+/gu;
 const SPECIAL_CHARS_REG_EXP =
@@ -95,6 +93,8 @@ const SYLLABLES_LENGTHS = new Map();
 const FIRST_CHAR_SYLLABLES_REG_EXP =
   "[\\u1100-\\u1112\\ud7a4-\\ud7af\\ud84a\\ud84c\\ud850\\ud854\\ud857\\ud85f]";
 
+const NFKC_CHARS_TO_NORMALIZE = new Map();
+
 let noSyllablesRegExp = null;
 let withSyllablesRegExp = null;
 
@@ -126,7 +126,18 @@ function normalize(text) {
   } else {
     // Compile the regular expression for text normalization once.
     const replace = Object.keys(CHARACTERS_TO_NORMALIZE).join("");
-    const regexp = `([${replace}])|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(\\p{Ideographic}\\n)|(\\n)`;
+    const toNormalizeWithNFKC =
+      "\u2460-\u2473" + // Circled numbers.
+      "\u24b6-\u24ff" + // Circled letters/numbers.
+      "\u3244-\u32bf" + // Circled ideograms/numbers.
+      "\u32d0-\u32fe" + // Circled ideograms.
+      "\uff00-\uffef"; // Halfwidth, fullwidth forms.
+
+    // 3040-309F: Hiragana
+    // 30A0-30FF: Katakana
+    const CJK = "(?:\\p{Ideographic}|[\u3040-\u30FF])";
+    const HKDiacritics = "(?:\u3099|\u309A)";
+    const regexp = `([${replace}])|([${toNormalizeWithNFKC}])|(${HKDiacritics}\\n)|(\\p{M}+(?:-\\n)?)|(\\S-\\n)|(${CJK}\\n)|(\\n)`;
 
     if (syllablePositions.length === 0) {
       // Most of the syllables belong to Hangul so there are no need
@@ -188,11 +199,11 @@ function normalize(text) {
 
   normalized = normalized.replace(
     normalizationRegex,
-    (match, p1, p2, p3, p4, p5, p6, i) => {
+    (match, p1, p2, p3, p4, p5, p6, p7, p8, i) => {
       i -= shiftOrigin;
       if (p1) {
         // Maybe fractions or quotations mark...
-        const replacement = CHARACTERS_TO_NORMALIZE[match];
+        const replacement = CHARACTERS_TO_NORMALIZE[p1];
         const jj = replacement.length;
         for (let j = 1; j < jj; j++) {
           positions.push([i - shift + j, shift - j]);
@@ -202,8 +213,47 @@ function normalize(text) {
       }
 
       if (p2) {
-        const hasTrailingDashEOL = p2.endsWith("\n");
-        const len = hasTrailingDashEOL ? p2.length - 2 : p2.length;
+        // Use the NFKC representation to normalize the char.
+        let replacement = NFKC_CHARS_TO_NORMALIZE.get(p2);
+        if (!replacement) {
+          replacement = p2.normalize("NFKC");
+          NFKC_CHARS_TO_NORMALIZE.set(p2, replacement);
+        }
+        const jj = replacement.length;
+        for (let j = 1; j < jj; j++) {
+          positions.push([i - shift + j, shift - j]);
+        }
+        shift -= jj - 1;
+        return replacement;
+      }
+
+      if (p3) {
+        // We've a Katakana-Hiragana diacritic followed by a \n so don't replace
+        // the \n by a whitespace.
+        hasDiacritics = true;
+
+        // Diacritic.
+        if (i + eol === rawDiacriticsPositions[rawDiacriticsIndex]?.[1]) {
+          ++rawDiacriticsIndex;
+        } else {
+          // i is the position of the first diacritic
+          // so (i - 1) is the position for the letter before.
+          positions.push([i - 1 - shift + 1, shift - 1]);
+          shift -= 1;
+          shiftOrigin += 1;
+        }
+
+        // End-of-line.
+        positions.push([i - shift + 1, shift]);
+        shiftOrigin += 1;
+        eol += 1;
+
+        return p3.charAt(0);
+      }
+
+      if (p4) {
+        const hasTrailingDashEOL = p4.endsWith("\n");
+        const len = hasTrailingDashEOL ? p4.length - 2 : p4.length;
 
         // Diacritics.
         hasDiacritics = true;
@@ -223,19 +273,19 @@ function normalize(text) {
 
         if (hasTrailingDashEOL) {
           // Diacritics are followed by a -\n.
-          // See comments in `if (p3)` block.
+          // See comments in `if (p5)` block.
           i += len - 1;
           positions.push([i - shift + 1, 1 + shift]);
           shift += 1;
           shiftOrigin += 1;
           eol += 1;
-          return p2.slice(0, len);
+          return p4.slice(0, len);
         }
 
-        return p2;
+        return p4;
       }
 
-      if (p3) {
+      if (p5) {
         // "X-\n" is removed because an hyphen at the end of a line
         // with not a space before is likely here to mark a break
         // in a word.
@@ -244,19 +294,19 @@ function normalize(text) {
         shift += 1;
         shiftOrigin += 1;
         eol += 1;
-        return p3.charAt(0);
+        return p5.charAt(0);
       }
 
-      if (p4) {
+      if (p6) {
         // An ideographic at the end of a line doesn't imply adding an extra
         // white space.
         positions.push([i - shift + 1, shift]);
         shiftOrigin += 1;
         eol += 1;
-        return p4.charAt(0);
+        return p6.charAt(0);
       }
 
-      if (p5) {
+      if (p7) {
         // eol is replaced by space: "foo\nbar" is likely equivalent to
         // "foo bar".
         positions.push([i - shift + 1, shift - 1]);
@@ -266,7 +316,7 @@ function normalize(text) {
         return " ";
       }
 
-      // p6
+      // p8
       if (i + eol === syllablePositions[syllableIndex]?.[1]) {
         // A syllable (1 char) is replaced with several chars (n) so
         // newCharsLen = n - 1.
@@ -278,7 +328,7 @@ function normalize(text) {
         shift -= newCharLen;
         shiftOrigin += newCharLen;
       }
-      return p6;
+      return p8;
     }
   );
 
@@ -314,18 +364,26 @@ function getOriginalIndex(diffs, pos, len) {
  * @typedef {Object} PDFFindControllerOptions
  * @property {IPDFLinkService} linkService - The navigation/linking service.
  * @property {EventBus} eventBus - The application event bus.
+ * @property {boolean} updateMatchesCountOnProgress - True if the matches
+ *   count must be updated on progress or only when the last page is reached.
+ *   The default value is `true`.
  */
 
 /**
  * Provides search functionality to find a given string in a PDF document.
  */
 class PDFFindController {
+  #updateMatchesCountOnProgress = true;
+
+  #visitedPagesCount = 0;
+
   /**
    * @param {PDFFindControllerOptions} options
    */
-  constructor({ linkService, eventBus }) {
+  constructor({ linkService, eventBus, updateMatchesCountOnProgress = true }) {
     this._linkService = linkService;
     this._eventBus = eventBus;
+    this.#updateMatchesCountOnProgress = updateMatchesCountOnProgress;
 
     this.#reset();
     eventBus._on("find", this.#onFind.bind(this));
@@ -464,6 +522,7 @@ class PDFFindController {
     this._pdfDocument = null;
     this._pageMatches = [];
     this._pageMatchesLength = [];
+    this.#visitedPagesCount = 0;
     this._state = null;
     // Currently selected match.
     this._selected = {
@@ -566,9 +625,13 @@ class PDFFindController {
   }
 
   #calculateRegExpMatch(query, entireWord, pageIndex, pageContent) {
-    const matches = [],
-      matchesLength = [];
-
+    const matches = (this._pageMatches[pageIndex] = []);
+    const matchesLength = (this._pageMatchesLength[pageIndex] = []);
+    if (!query) {
+      // The query can be empty because some chars like diacritics could have
+      // been stripped out.
+      return;
+    }
     const diffs = this._pageDiffs[pageIndex];
     let match;
     while ((match = query.exec(pageContent)) !== null) {
@@ -590,8 +653,6 @@ class PDFFindController {
         matchesLength.push(matchLen);
       }
     }
-    this._pageMatches[pageIndex] = matches;
-    this._pageMatchesLength[pageIndex] = matchesLength;
   }
 
   #convertToRegExpString(query, hasDiacritics) {
@@ -652,6 +713,10 @@ class PDFFindController {
     if (matchDiacritics) {
       // aX must not match aXY.
       if (hasDiacritics) {
+        DIACRITICS_EXCEPTION_STR ||= String.fromCharCode(
+          ...DIACRITICS_EXCEPTION
+        );
+
         isUnicode = true;
         query = `${query}(?=[${DIACRITICS_EXCEPTION_STR}]|[^\\p{M}]|$)`;
       }
@@ -662,7 +727,7 @@ class PDFFindController {
 
   #calculateMatch(pageIndex) {
     let query = this.#query;
-    if (query.length === 0) {
+    if (!query) {
       // Do nothing: the matches should be wiped out already.
       return;
     }
@@ -695,7 +760,7 @@ class PDFFindController {
     }
 
     const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`;
-    query = new RegExp(query, flags);
+    query = query ? new RegExp(query, flags) : null;
 
     this.#calculateRegExpMatch(query, entireWord, pageIndex, pageContent);
 
@@ -711,8 +776,14 @@ class PDFFindController {
 
     // Update the match count.
     const pageMatchesCount = this._pageMatches[pageIndex].length;
-    if (pageMatchesCount > 0) {
-      this._matchesCountTotal += pageMatchesCount;
+    this._matchesCountTotal += pageMatchesCount;
+    if (this.#updateMatchesCountOnProgress) {
+      if (pageMatchesCount > 0) {
+        this.#updateUIResultsCount();
+      }
+    } else if (++this.#visitedPagesCount === this._linkService.pagesCount) {
+      // For example, in GeckoView we want to have only the final update because
+      // the Java side provides only one object to update the counts.
       this.#updateUIResultsCount();
     }
   }
@@ -807,6 +878,7 @@ class PDFFindController {
       this._resumePageIdx = null;
       this._pageMatches.length = 0;
       this._pageMatchesLength.length = 0;
+      this.#visitedPagesCount = 0;
       this._matchesCountTotal = 0;
 
       this.#updateAllPages(); // Wipe out any previously highlighted matches.
@@ -825,7 +897,7 @@ class PDFFindController {
     }
 
     // If there's no query there's no point in searching.
-    if (this.#query === "") {
+    if (!this.#query) {
       this.#updateUIState(FindState.FOUND);
       return;
     }
@@ -1005,6 +1077,16 @@ class PDFFindController {
   }
 
   #updateUIState(state, previous = false) {
+    if (
+      !this.#updateMatchesCountOnProgress &&
+      (this.#visitedPagesCount !== this._linkService.pagesCount ||
+        state === FindState.PENDING)
+    ) {
+      // When this.#updateMatchesCountOnProgress is false we only send an update
+      // when everything is ready.
+      return;
+    }
+
     this._eventBus.dispatch("updatefindcontrolstate", {
       source: this,
       state,

+ 14 - 0
app/src/asset/pdf/pdf_link_service.js

@@ -173,6 +173,13 @@ class PDFLinkService {
     this.pdfViewer.pagesRotation = value;
   }
 
+  /**
+   * @type {boolean}
+   */
+  get isInPresentationMode() {
+    return this.pdfViewer.isInPresentationMode;
+  }
+
   #goToDestinationHelper(rawDest, namedDest = null, explicitDest) {
     // Dest array looks like that: <page-ref> </XYZ|/FitXXX> <args..>
     const destRef = explicitDest[0];
@@ -673,6 +680,13 @@ class SimpleLinkService {
    */
   set rotation(value) {}
 
+  /**
+   * @type {boolean}
+   */
+  get isInPresentationMode() {
+    return false;
+  }
+
   /**
    * @param {string|Array} dest - The named, or explicit, PDF destination.
    */

+ 19 - 2
app/src/asset/pdf/pdf_outline_viewer.js

@@ -20,8 +20,9 @@ import { SidebarView } from "./ui_utils.js";
 /**
  * @typedef {Object} PDFOutlineViewerOptions
  * @property {HTMLDivElement} container - The viewer element.
- * @property {IPDFLinkService} linkService - The navigation/linking service.
  * @property {EventBus} eventBus - The application event bus.
+ * @property {IPDFLinkService} linkService - The navigation/linking service.
+ * @property {DownloadManager} downloadManager - The download manager.
  */
 
 /**
@@ -37,6 +38,7 @@ class PDFOutlineViewer extends BaseTreeViewer {
   constructor(options) {
     super(options);
     this.linkService = options.linkService;
+    this.downloadManager = options.downloadManager;
 
     this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this));
     this.eventBus._on(
@@ -109,7 +111,10 @@ class PDFOutlineViewer extends BaseTreeViewer {
   /**
    * @private
    */
-  _bindLink(element, { url, newWindow, action, dest, setOCGState }) {
+  _bindLink(
+    element,
+    { url, newWindow, action, attachment, dest, setOCGState }
+  ) {
     const { linkService } = this;
 
     if (url) {
@@ -124,6 +129,18 @@ class PDFOutlineViewer extends BaseTreeViewer {
       };
       return;
     }
+    if (attachment) {
+      element.href = linkService.getAnchorUrl("");
+      element.onclick = () => {
+        this.downloadManager.openOrDownloadData(
+          element,
+          attachment.content,
+          attachment.filename
+        );
+        return false;
+      };
+      return;
+    }
     if (setOCGState) {
       element.href = linkService.getAnchorUrl("");
       element.onclick = () => {

File diff suppressed because it is too large
+ 357 - 269
app/src/asset/pdf/pdf_page_view.js


+ 17 - 14
app/src/asset/pdf/pdf_presentation_mode.js

@@ -224,21 +224,24 @@ class PDFPresentationMode {
       evt.preventDefault();
       return;
     }
-    if (evt.button === 0) {
-      // Enable clicking of links in presentation mode. Note: only links
-      // pointing to destinations in the current PDF document work.
-      const isInternalLink =
-        evt.target.href && evt.target.classList.contains("internalLink");
-      if (!isInternalLink) {
-        // Unless an internal link was clicked, advance one page.
-        evt.preventDefault();
+    if (evt.button !== 0) {
+      return;
+    }
+    // Enable clicking of links in presentation mode. Note: only links
+    // pointing to destinations in the current PDF document work.
+    if (
+      evt.target.href &&
+      evt.target.parentNode?.hasAttribute("data-internal-link")
+    ) {
+      return;
+    }
+    // Unless an internal link was clicked, advance one page.
+    evt.preventDefault();
 
-        if (evt.shiftKey) {
-          this.pdfViewer.previousPage();
-        } else {
-          this.pdfViewer.nextPage();
-        }
-      }
+    if (evt.shiftKey) {
+      this.pdfViewer.previousPage();
+    } else {
+      this.pdfViewer.nextPage();
     }
   }
 

+ 10 - 30
app/src/asset/pdf/pdf_scripting_manager.js

@@ -46,7 +46,6 @@ class PDFScriptingManager {
     this._destroyCapability = null;
 
     this._scripting = null;
-    this._mouseState = Object.create(null);
     this._ready = false;
 
     this._eventBus = eventBus;
@@ -143,19 +142,9 @@ class PDFScriptingManager {
       this._closeCapability?.resolve();
     });
 
-    this._domEvents.set("mousedown", event => {
-      this._mouseState.isDown = true;
-    });
-    this._domEvents.set("mouseup", event => {
-      this._mouseState.isDown = false;
-    });
-
     for (const [name, listener] of this._internalEvents) {
       this._eventBus._on(name, listener);
     }
-    for (const [name, listener] of this._domEvents) {
-      window.addEventListener(name, listener, true);
-    }
 
     try {
       const docProperties = await this._getDocProperties();
@@ -229,10 +218,6 @@ class PDFScriptingManager {
     });
   }
 
-  get mouseState() {
-    return this._mouseState;
-  }
-
   get destroyPromise() {
     return this._destroyCapability?.promise || null;
   }
@@ -248,13 +233,6 @@ class PDFScriptingManager {
     return shadow(this, "_internalEvents", new Map());
   }
 
-  /**
-   * @private
-   */
-  get _domEvents() {
-    return shadow(this, "_domEvents", new Map());
-  }
-
   /**
    * @private
    */
@@ -287,13 +265,21 @@ class PDFScriptingManager {
         case "error":
           console.error(value);
           break;
-        case "layout":
-          if (isInPresentationMode) {
+        case "layout": {
+          // NOTE: Always ignore the pageLayout in GeckoView since there's
+          // no UI available to change Scroll/Spread modes for the user.
+          if (
+            (typeof PDFJSDev === "undefined"
+              ? window.isGECKOVIEW
+              : PDFJSDev.test("GECKOVIEW")) ||
+            isInPresentationMode
+          ) {
             return;
           }
           const modes = apiPageLayoutToViewerModes(value);
           this._pdfViewer.spreadMode = modes.spreadMode;
           break;
+        }
         case "page-num":
           this._pdfViewer.currentPageNumber = value + 1;
           break;
@@ -508,16 +494,10 @@ class PDFScriptingManager {
     }
     this._internalEvents.clear();
 
-    for (const [name, listener] of this._domEvents) {
-      window.removeEventListener(name, listener, true);
-    }
-    this._domEvents.clear();
-
     this._pageOpenPending.clear();
     this._visitedPages.clear();
 
     this._scripting = null;
-    delete this._mouseState.isDown;
     this._ready = false;
 
     this._destroyCapability?.resolve();

+ 1 - 10
app/src/asset/pdf/pdf_thumbnail_viewer.js

@@ -14,7 +14,6 @@
  */
 
 /** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
-/** @typedef {import("./event_utils").EventBus} EventBus */
 /** @typedef {import("./interfaces").IL10n} IL10n */
 /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
 // eslint-disable-next-line max-len
@@ -36,7 +35,6 @@ const THUMBNAIL_SELECTED_CLASS = "selected";
  * @typedef {Object} PDFThumbnailViewerOptions
  * @property {HTMLDivElement} container - The container for the thumbnail
  *   elements.
- * @property {EventBus} eventBus - The application event bus.
  * @property {IPDFLinkService} linkService - The navigation/linking service.
  * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
  * @property {IL10n} l10n - Localization service.
@@ -52,14 +50,7 @@ class PDFThumbnailViewer {
   /**
    * @param {PDFThumbnailViewerOptions} options
    */
-  constructor({
-    container,
-    eventBus,
-    linkService,
-    renderingQueue,
-    l10n,
-    pageColors,
-  }) {
+  constructor({ container, linkService, renderingQueue, l10n, pageColors }) {
     this.container = container;
     this.linkService = linkService;
     this.renderingQueue = renderingQueue;

+ 222 - 301
app/src/asset/pdf/pdf_viewer.js

@@ -22,18 +22,7 @@
 /** @typedef {import("./event_utils").EventBus} EventBus */
 /** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
 /** @typedef {import("./interfaces").IL10n} IL10n */
-// eslint-disable-next-line max-len
-/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
-// eslint-disable-next-line max-len
-/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
 /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
-// eslint-disable-next-line max-len
-/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
-// eslint-disable-next-line max-len
-/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
-/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
-// eslint-disable-next-line max-len
-/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
 
 import {
   AnnotationEditorType,
@@ -69,16 +58,10 @@ import {
   VERTICAL_PADDING,
   watchScroll,
 } from "./ui_utils.js";
-import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
-import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
 import { NullL10n } from "./l10n_utils.js";
 import { PDFPageView } from "./pdf_page_view.js";
 import { PDFRenderingQueue } from "./pdf_rendering_queue.js";
 import { SimpleLinkService } from "./pdf_link_service.js";
-import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
-import { TextHighlighter } from "./text_highlighter.js";
-import { TextLayerBuilder } from "./text_layer_builder.js";
-import { XfaLayerBuilder } from "./xfa_layer_builder.js";
 
 const DEFAULT_CACHE_SIZE = 10;
 const ENABLE_PERMISSIONS_CLASS = "enablePermissions";
@@ -128,6 +111,8 @@ function isValidAnnotationEditorMode(mode) {
  *   landscape pages upon printing. The default is `false`.
  * @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default
  *   value is `false`.
+ * @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
+ *   OffscreenCanvas if needed.
  * @property {number} [maxCanvasPixels] - The maximum supported canvas size in
  *   total pixels, i.e. width * height. Use -1 for no limit. The default value
  *   is 4096 * 4096 (16 mega-pixels).
@@ -209,12 +194,6 @@ class PDFPageViewBuffer {
 
 /**
  * Simple viewer control to display PDF content/pages.
- *
- * @implements {IPDFAnnotationLayerFactory}
- * @implements {IPDFAnnotationEditorLayerFactory}
- * @implements {IPDFStructTreeLayerFactory}
- * @implements {IPDFTextLayerFactory}
- * @implements {IPDFXfaLayerFactory}
  */
 class PDFViewer {
   #buffer = null;
@@ -225,14 +204,20 @@ class PDFViewer {
 
   #annotationMode = AnnotationMode.ENABLE_FORMS;
 
+  #containerTopLeft = null;
+
   #enablePermissions = false;
 
   #previousContainerHeight = 0;
 
+  #resizeObserver = new ResizeObserver(this.#resizeObserverCallback.bind(this));
+
   #scrollModePageState = null;
 
   #onVisibilityChange = null;
 
+  #scaleTimeoutId = null;
+
   /**
    * @param {PDFViewerOptions} options
    */
@@ -252,12 +237,7 @@ class PDFViewer {
       typeof PDFJSDev === "undefined" ||
       PDFJSDev.test("!PRODUCTION || GENERIC")
     ) {
-      if (
-        !(
-          this.container?.tagName.toUpperCase() === "DIV" &&
-          this.viewer?.tagName.toUpperCase() === "DIV"
-        )
-      ) {
+      if (this.container?.tagName !== "DIV" || this.viewer?.tagName !== "DIV") {
         throw new Error("Invalid `container` and/or `viewer` option.");
       }
 
@@ -268,12 +248,13 @@ class PDFViewer {
         throw new Error("The `container` must be absolutely positioned.");
       }
     }
+    this.#resizeObserver.observe(this.container);
+
     this.eventBus = options.eventBus;
     this.linkService = options.linkService || new SimpleLinkService();
     this.downloadManager = options.downloadManager || null;
     this.findController = options.findController || null;
     this._scriptingManager = options.scriptingManager || null;
-    this.removePageBorders = options.removePageBorders || false;
     this.textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE;
     this.#annotationMode =
       options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
@@ -285,9 +266,12 @@ class PDFViewer {
       typeof PDFJSDev === "undefined" ||
       PDFJSDev.test("!PRODUCTION || GENERIC")
     ) {
+      this.removePageBorders = options.removePageBorders || false;
       this.renderer = options.renderer || RendererType.CANVAS;
     }
     this.useOnlyCssZoom = options.useOnlyCssZoom || false;
+    this.isOffscreenCanvasSupported =
+      options.isOffscreenCanvasSupported ?? true;
     this.maxCanvasPixels = options.maxCanvasPixels;
     this.l10n = options.l10n || NullL10n;
     this.#enablePermissions = options.enablePermissions || false;
@@ -324,10 +308,14 @@ class PDFViewer {
     this._onBeforeDraw = this._onAfterDraw = null;
     this._resetView();
 
-    if (this.removePageBorders) {
+    if (
+      (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
+      this.removePageBorders
+    ) {
       this.viewer.classList.add("removePageBorders");
     }
-    this.updateContainerHeightCss();
+
+    this.#updateContainerHeightCss();
   }
 
   get pagesCount() {
@@ -467,7 +455,7 @@ class PDFViewer {
     if (!this.pdfDocument) {
       return;
     }
-    this._setScale(val, false);
+    this._setScale(val, { noScroll: false });
   }
 
   /**
@@ -484,7 +472,7 @@ class PDFViewer {
     if (!this.pdfDocument) {
       return;
     }
-    this._setScale(val, false);
+    this._setScale(val, { noScroll: false });
   }
 
   /**
@@ -516,14 +504,12 @@ class PDFViewer {
 
     const pageNumber = this._currentPageNumber;
 
-    const updateArgs = { rotation };
-    for (const pageView of this._pages) {
-      pageView.update(updateArgs);
-    }
+    this.refresh(true, { rotation });
+
     // Prevent errors in case the rotation changes *before* the scale has been
     // set to a non-default value.
     if (this._currentScaleValue) {
-      this._setScale(this._currentScaleValue, true);
+      this._setScale(this._currentScaleValue, { noScroll: true });
     }
 
     this.eventBus.dispatch("rotationchanging", {
@@ -549,6 +535,36 @@ class PDFViewer {
     return this.pdfDocument ? this._pagesCapability.promise : null;
   }
 
+  #layerProperties() {
+    const self = this;
+    return {
+      get annotationEditorUIManager() {
+        return self.#annotationEditorUIManager;
+      },
+      get annotationStorage() {
+        return self.pdfDocument?.annotationStorage;
+      },
+      get downloadManager() {
+        return self.downloadManager;
+      },
+      get enableScripting() {
+        return !!self._scriptingManager;
+      },
+      get fieldObjectsPromise() {
+        return self.pdfDocument?.getFieldObjects();
+      },
+      get findController() {
+        return self.findController;
+      },
+      get hasJSActionsPromise() {
+        return self.pdfDocument?.hasJSActions();
+      },
+      get linkService() {
+        return self.linkService;
+      },
+    };
+  }
+
   /**
    * Currently only *some* permissions are supported.
    * @returns {Object}
@@ -648,7 +664,6 @@ class PDFViewer {
     if (!pdfDocument) {
       return;
     }
-    const isPureXfa = pdfDocument.isPureXfa;
     const pagesCount = pdfDocument.numPages;
     const firstPagePromise = pdfDocument.getPage(1);
     // Rendering (potentially) depends on this, hence fetching it immediately.
@@ -722,12 +737,13 @@ class PDFViewer {
         if (annotationEditorMode !== AnnotationEditorType.DISABLE) {
           const mode = annotationEditorMode;
 
-          if (isPureXfa) {
+          if (pdfDocument.isPureXfa) {
             console.warn("Warning: XFA-editing is not implemented.");
           } else if (isValidAnnotationEditorMode(mode)) {
             this.#annotationEditorUIManager = new AnnotationEditorUIManager(
               this.container,
-              this.eventBus
+              this.eventBus,
+              pdfDocument?.annotationStorage
             );
             if (mode !== AnnotationEditorType.NONE) {
               this.#annotationEditorUIManager.updateMode(mode);
@@ -737,20 +753,16 @@ class PDFViewer {
           }
         }
 
+        const layerProperties = this.#layerProperties.bind(this);
         const viewerElement =
           this._scrollMode === ScrollMode.PAGE ? null : this.viewer;
         const scale = this.currentScale;
         const viewport = firstPdfPage.getViewport({
           scale: scale * PixelsPerInch.PDF_TO_CSS_UNITS,
         });
-        const textLayerFactory =
-          textLayerMode !== TextLayerMode.DISABLE && !isPureXfa ? this : null;
-        const annotationLayerFactory =
-          annotationMode !== AnnotationMode.DISABLE ? this : null;
-        const xfaLayerFactory = isPureXfa ? this : null;
-        const annotationEditorLayerFactory = this.#annotationEditorUIManager
-          ? this
-          : null;
+        // Ensure that the various layers always get the correct initial size,
+        // see issue 15795.
+        this.viewer.style.setProperty("--scale-factor", viewport.scale);
 
         for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
           const pageView = new PDFPageView({
@@ -761,14 +773,8 @@ class PDFViewer {
             defaultViewport: viewport.clone(),
             optionalContentConfigPromise,
             renderingQueue: this.renderingQueue,
-            textLayerFactory,
             textLayerMode,
-            annotationLayerFactory,
             annotationMode,
-            xfaLayerFactory,
-            annotationEditorLayerFactory,
-            textHighlighterFactory: this,
-            structTreeLayerFactory: this,
             imageResourcesPath: this.imageResourcesPath,
             renderer:
               typeof PDFJSDev === "undefined" ||
@@ -776,9 +782,11 @@ class PDFViewer {
                 ? this.renderer
                 : null,
             useOnlyCssZoom: this.useOnlyCssZoom,
+            isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
             maxCanvasPixels: this.maxCanvasPixels,
             pageColors: this.pageColors,
             l10n: this.l10n,
+            layerProperties,
           });
           this._pages.push(pageView);
         }
@@ -1024,10 +1032,12 @@ class PDFViewer {
   #scrollIntoView(pageView, pageSpot = null) {
     const { div, id } = pageView;
 
-    if (this._scrollMode === ScrollMode.PAGE) {
-      // Ensure that `this._currentPageNumber` is correct.
+    // Ensure that `this._currentPageNumber` is correct, when `#scrollIntoView`
+    // is called directly (and not from `#resetCurrentPageView`).
+    if (this._currentPageNumber !== id) {
       this._setCurrentPageNumber(id);
-
+    }
+    if (this._scrollMode === ScrollMode.PAGE) {
       this.#ensurePageViewVisible();
       // Ensure that rendering always occurs, to avoid showing a blank page,
       // even if the current position doesn't change when the page is scrolled.
@@ -1047,6 +1057,15 @@ class PDFViewer {
       }
     }
     scrollIntoView(div, pageSpot);
+
+    // Ensure that the correct *initial* document position is set, when any
+    // OpenParameters are used, for documents with non-default Scroll/Spread
+    // modes (fixes issue 15695). This is necessary since the scroll-handler
+    // invokes the `update`-method asynchronously, and `this._location` could
+    // thus be wrong when the initial zooming occurs in the default viewer.
+    if (!this._currentScaleValue && this._location) {
+      this._location = null;
+    }
   }
 
   /**
@@ -1060,7 +1079,11 @@ class PDFViewer {
     );
   }
 
-  _setScaleUpdatePages(newScale, newValue, noScroll = false, preset = false) {
+  _setScaleUpdatePages(
+    newScale,
+    newValue,
+    { noScroll = false, preset = false, drawingDelay = -1 }
+  ) {
     this._currentScaleValue = newValue.toString();
 
     if (this.#isSameScale(newScale)) {
@@ -1074,15 +1097,24 @@ class PDFViewer {
       return;
     }
 
-    docStyle.setProperty(
+    this.viewer.style.setProperty(
       "--scale-factor",
       newScale * PixelsPerInch.PDF_TO_CSS_UNITS
     );
 
-    const updateArgs = { scale: newScale };
-    for (const pageView of this._pages) {
-      pageView.update(updateArgs);
+    const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
+    this.refresh(true, {
+      scale: newScale,
+      drawingDelay: postponeDrawing ? drawingDelay : -1,
+    });
+
+    if (postponeDrawing) {
+      this.#scaleTimeoutId = setTimeout(() => {
+        this.#scaleTimeoutId = null;
+        this.refresh();
+      }, drawingDelay);
     }
+
     this._currentScale = newScale;
 
     if (!noScroll) {
@@ -1117,7 +1149,6 @@ class PDFViewer {
     if (this.defaultRenderingQueue) {
       this.update();
     }
-    this.updateContainerHeightCss();
   }
 
   /**
@@ -1133,11 +1164,12 @@ class PDFViewer {
     return 1;
   }
 
-  _setScale(value, noScroll = false) {
+  _setScale(value, options) {
     let scale = parseFloat(value);
 
     if (scale > 0) {
-      this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ false);
+      options.preset = false;
+      this._setScaleUpdatePages(scale, value, options);
     } else {
       const currentPage = this._pages[this._currentPageNumber - 1];
       if (!currentPage) {
@@ -1147,8 +1179,18 @@ class PDFViewer {
         vPadding = VERTICAL_PADDING;
 
       if (this.isInPresentationMode) {
-        hPadding = vPadding = 4;
-      } else if (this.removePageBorders) {
+        // Pages have a 2px (transparent) border in PresentationMode, see
+        // the `web/pdf_viewer.css` file.
+        hPadding = vPadding = 4; // 2 * 2px
+        if (this._spreadMode !== SpreadMode.NONE) {
+          // Account for two pages being visible in PresentationMode, thus
+          // "doubling" the total border width.
+          hPadding *= 2;
+        }
+      } else if (
+        (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
+        this.removePageBorders
+      ) {
         hPadding = vPadding = 0;
       } else if (this._scrollMode === ScrollMode.HORIZONTAL) {
         [hPadding, vPadding] = [vPadding, hPadding]; // Swap the padding values.
@@ -1185,7 +1227,8 @@ class PDFViewer {
           console.error(`_setScale: "${value}" is an unknown zoom value.`);
           return;
       }
-      this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ true);
+      options.preset = true;
+      this._setScaleUpdatePages(scale, value, options);
     }
   }
 
@@ -1197,7 +1240,7 @@ class PDFViewer {
 
     if (this.isInPresentationMode) {
       // Fixes the case when PDF has different page sizes.
-      this._setScale(this._currentScaleValue, true);
+      this._setScale(this._currentScaleValue, { noScroll: true });
     }
     this.#scrollIntoView(pageView);
   }
@@ -1314,9 +1357,15 @@ class PDFViewer {
         y = destArray[3];
         width = destArray[4] - x;
         height = destArray[5] - y;
-        const hPadding = this.removePageBorders ? 0 : SCROLLBAR_PADDING;
-        const vPadding = this.removePageBorders ? 0 : VERTICAL_PADDING;
-
+        let hPadding = SCROLLBAR_PADDING,
+          vPadding = VERTICAL_PADDING;
+
+        if (
+          (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
+          this.removePageBorders
+        ) {
+          hPadding = vPadding = 0;
+        }
         widthScale =
           (this.container.clientWidth - hPadding) /
           width /
@@ -1582,23 +1631,6 @@ class PDFViewer {
     return this.scroll.down;
   }
 
-  /**
-   * Only show the `loadingIcon`-spinner on visible pages (see issue 14242).
-   */
-  #toggleLoadingIconSpinner(visibleIds) {
-    for (const id of visibleIds) {
-      const pageView = this._pages[id - 1];
-      pageView?.toggleLoadingIconSpinner(/* viewVisible = */ true);
-    }
-    for (const pageView of this.#buffer) {
-      if (visibleIds.has(pageView.id)) {
-        // Handled above, since the "buffer" may not contain all visible pages.
-        continue;
-      }
-      pageView.toggleLoadingIconSpinner(/* viewVisible = */ false);
-    }
-  }
-
   forceRendering(currentlyVisiblePages) {
     const visiblePages = currentlyVisiblePages || this._getVisiblePages();
     const scrollAhead = this.#getScrollAhead(visiblePages);
@@ -1612,7 +1644,6 @@ class PDFViewer {
       scrollAhead,
       preRenderExtra
     );
-    this.#toggleLoadingIconSpinner(visiblePages.ids);
 
     if (pageView) {
       this.#ensurePdfPageLoaded(pageView).then(() => {
@@ -1623,185 +1654,6 @@ class PDFViewer {
     return false;
   }
 
-  /**
-   * @typedef {Object} CreateTextLayerBuilderParameters
-   * @property {HTMLDivElement} textLayerDiv
-   * @property {number} pageIndex
-   * @property {PageViewport} viewport
-   * @property {EventBus} eventBus
-   * @property {TextHighlighter} highlighter
-   * @property {TextAccessibilityManager} [accessibilityManager]
-   */
-
-  /**
-   * @param {CreateTextLayerBuilderParameters}
-   * @returns {TextLayerBuilder}
-   */
-  createTextLayerBuilder({
-    textLayerDiv,
-    pageIndex,
-    viewport,
-    eventBus,
-    highlighter,
-    accessibilityManager = null,
-  }) {
-    return new TextLayerBuilder({
-      textLayerDiv,
-      eventBus,
-      pageIndex,
-      viewport,
-      highlighter,
-      accessibilityManager,
-    });
-  }
-
-  /**
-   * @typedef {Object} CreateTextHighlighterParameters
-   * @property {number} pageIndex
-   * @property {EventBus} eventBus
-   */
-
-  /**
-   * @param {CreateTextHighlighterParameters}
-   * @returns {TextHighlighter}
-   */
-  createTextHighlighter({ pageIndex, eventBus }) {
-    return new TextHighlighter({
-      eventBus,
-      pageIndex,
-      findController: this.isInPresentationMode ? null : this.findController,
-    });
-  }
-
-  /**
-   * @typedef {Object} CreateAnnotationLayerBuilderParameters
-   * @property {HTMLDivElement} pageDiv
-   * @property {PDFPageProxy} pdfPage
-   * @property {AnnotationStorage} [annotationStorage] - Storage for annotation
-   *   data in forms.
-   * @property {string} [imageResourcesPath] - Path for image resources, mainly
-   *   for annotation icons. Include trailing slash.
-   * @property {boolean} renderForms
-   * @property {IL10n} l10n
-   * @property {boolean} [enableScripting]
-   * @property {Promise<boolean>} [hasJSActionsPromise]
-   * @property {Object} [mouseState]
-   * @property {Promise<Object<string, Array<Object>> | null>}
-   *   [fieldObjectsPromise]
-   * @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
-   *   annotation ids with canvases used to render them.
-   * @property {TextAccessibilityManager} [accessibilityManager]
-   */
-
-  /**
-   * @param {CreateAnnotationLayerBuilderParameters}
-   * @returns {AnnotationLayerBuilder}
-   */
-  createAnnotationLayerBuilder({
-    pageDiv,
-    pdfPage,
-    annotationStorage = this.pdfDocument?.annotationStorage,
-    imageResourcesPath = "",
-    renderForms = true,
-    l10n = NullL10n,
-    enableScripting = this.enableScripting,
-    hasJSActionsPromise = this.pdfDocument?.hasJSActions(),
-    mouseState = this._scriptingManager?.mouseState,
-    fieldObjectsPromise = this.pdfDocument?.getFieldObjects(),
-    annotationCanvasMap = null,
-    accessibilityManager = null,
-  }) {
-    return new AnnotationLayerBuilder({
-      pageDiv,
-      pdfPage,
-      annotationStorage,
-      imageResourcesPath,
-      renderForms,
-      linkService: this.linkService,
-      downloadManager: this.downloadManager,
-      l10n,
-      enableScripting,
-      hasJSActionsPromise,
-      mouseState,
-      fieldObjectsPromise,
-      annotationCanvasMap,
-      accessibilityManager,
-    });
-  }
-
-  /**
-   * @typedef {Object} CreateAnnotationEditorLayerBuilderParameters
-   * @property {AnnotationEditorUIManager} [uiManager]
-   * @property {HTMLDivElement} pageDiv
-   * @property {PDFPageProxy} pdfPage
-   * @property {IL10n} l10n
-   * @property {AnnotationStorage} [annotationStorage] - Storage for annotation
-   * @property {TextAccessibilityManager} [accessibilityManager]
-   *   data in forms.
-   */
-
-  /**
-   * @param {CreateAnnotationEditorLayerBuilderParameters}
-   * @returns {AnnotationEditorLayerBuilder}
-   */
-  createAnnotationEditorLayerBuilder({
-    uiManager = this.#annotationEditorUIManager,
-    pageDiv,
-    pdfPage,
-    accessibilityManager = null,
-    l10n,
-    annotationStorage = this.pdfDocument?.annotationStorage,
-  }) {
-    return new AnnotationEditorLayerBuilder({
-      uiManager,
-      pageDiv,
-      pdfPage,
-      annotationStorage,
-      accessibilityManager,
-      l10n,
-    });
-  }
-
-  /**
-   * @typedef {Object} CreateXfaLayerBuilderParameters
-   * @property {HTMLDivElement} pageDiv
-   * @property {PDFPageProxy} pdfPage
-   * @property {AnnotationStorage} [annotationStorage] - Storage for annotation
-   *   data in forms.
-   */
-
-  /**
-   * @param {CreateXfaLayerBuilderParameters}
-   * @returns {XfaLayerBuilder}
-   */
-  createXfaLayerBuilder({
-    pageDiv,
-    pdfPage,
-    annotationStorage = this.pdfDocument?.annotationStorage,
-  }) {
-    return new XfaLayerBuilder({
-      pageDiv,
-      pdfPage,
-      annotationStorage,
-      linkService: this.linkService,
-    });
-  }
-
-  /**
-   * @typedef {Object} CreateStructTreeLayerBuilderParameters
-   * @property {PDFPageProxy} pdfPage
-   */
-
-  /**
-   * @param {CreateStructTreeLayerBuilderParameters}
-   * @returns {StructTreeLayerBuilder}
-   */
-  createStructTreeLayerBuilder({ pdfPage }) {
-    return new StructTreeLayerBuilder({
-      pdfPage,
-    });
-  }
-
   /**
    * @type {boolean} Whether all pages of the PDF document have identical
    *   widths and heights.
@@ -1878,11 +1730,7 @@ class PDFViewer {
     }
     this._optionalContentConfigPromise = promise;
 
-    const updateArgs = { optionalContentConfigPromise: promise };
-    for (const pageView of this._pages) {
-      pageView.update(updateArgs);
-    }
-    this.update();
+    this.refresh(false, { optionalContentConfigPromise: promise });
 
     this.eventBus.dispatch("optionalcontentconfigchanged", {
       source: this,
@@ -1945,7 +1793,7 @@ class PDFViewer {
     // Call this before re-scrolling to the current page, to ensure that any
     // changes in scale don't move the current page.
     if (this._currentScaleValue && isNaN(this._currentScaleValue)) {
-      this._setScale(this._currentScaleValue, true);
+      this._setScale(this._currentScaleValue, { noScroll: true });
     }
     this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true);
     this.update();
@@ -2017,7 +1865,7 @@ class PDFViewer {
     // Call this before re-scrolling to the current page, to ensure that any
     // changes in scale don't move the current page.
     if (this._currentScaleValue && isNaN(this._currentScaleValue)) {
-      this._setScale(this._currentScaleValue, true);
+      this._setScale(this._currentScaleValue, { noScroll: true });
     }
     this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true);
     this.update();
@@ -2157,42 +2005,110 @@ class PDFViewer {
 
   /**
    * Increase the current zoom level one, or more, times.
-   * @param {number} [steps] - Defaults to zooming once.
+   * @param {Object|null} [options]
    */
-  increaseScale(steps = 1) {
+  increaseScale(options = null) {
+    if (
+      (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
+      typeof options === "number"
+    ) {
+      console.error(
+        "The `increaseScale` method-signature was updated, please use an object instead."
+      );
+      options = { steps: options };
+    }
+
+    if (!this.pdfDocument) {
+      return;
+    }
+
+    options ||= Object.create(null);
+
     let newScale = this._currentScale;
-    do {
-      newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
-      newScale = Math.ceil(newScale * 10) / 10;
-      newScale = Math.min(MAX_SCALE, newScale);
-    } while (--steps > 0 && newScale < MAX_SCALE);
-    this.currentScaleValue = newScale;
+    if (options.scaleFactor > 1) {
+      newScale = Math.min(
+        MAX_SCALE,
+        Math.round(newScale * options.scaleFactor * 100) / 100
+      );
+    } else {
+      let steps = options.steps ?? 1;
+      do {
+        newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
+        newScale = Math.ceil(newScale * 10) / 10;
+        newScale = Math.min(MAX_SCALE, newScale);
+      } while (--steps > 0 && newScale < MAX_SCALE);
+    }
+
+    options.noScroll = false;
+    this._setScale(newScale, options);
   }
 
   /**
    * Decrease the current zoom level one, or more, times.
-   * @param {number} [steps] - Defaults to zooming once.
+   * @param {Object|null} [options]
    */
-  decreaseScale(steps = 1) {
+  decreaseScale(options = null) {
+    if (
+      (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
+      typeof options === "number"
+    ) {
+      console.error(
+        "The `decreaseScale` method-signature was updated, please use an object instead."
+      );
+      options = { steps: options };
+    }
+
+    if (!this.pdfDocument) {
+      return;
+    }
+
+    options ||= Object.create(null);
+
     let newScale = this._currentScale;
-    do {
-      newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
-      newScale = Math.floor(newScale * 10) / 10;
-      newScale = Math.max(MIN_SCALE, newScale);
-    } while (--steps > 0 && newScale > MIN_SCALE);
-    this.currentScaleValue = newScale;
-  }
+    if (options.scaleFactor > 0 && options.scaleFactor < 1) {
+      newScale = Math.max(
+        MIN_SCALE,
+        Math.round(newScale * options.scaleFactor * 100) / 100
+      );
+    } else {
+      let steps = options.steps ?? 1;
+      do {
+        newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
+        newScale = Math.floor(newScale * 10) / 10;
+        newScale = Math.max(MIN_SCALE, newScale);
+      } while (--steps > 0 && newScale > MIN_SCALE);
+    }
 
-  updateContainerHeightCss() {
-    const height = this.container.clientHeight;
+    options.noScroll = false;
+    this._setScale(newScale, options);
+  }
 
+  #updateContainerHeightCss(height = this.container.clientHeight) {
     if (height !== this.#previousContainerHeight) {
       this.#previousContainerHeight = height;
-
       docStyle.setProperty("--viewer-container-height", `${height}px`);
     }
   }
 
+  #resizeObserverCallback(entries) {
+    for (const entry of entries) {
+      if (entry.target === this.container) {
+        this.#updateContainerHeightCss(
+          Math.floor(entry.borderBoxSize[0].blockSize)
+        );
+        this.#containerTopLeft = null;
+        break;
+      }
+    }
+  }
+
+  get containerTopLeft() {
+    return (this.#containerTopLeft ||= [
+      this.container.offsetTop,
+      this.container.offsetLeft,
+    ]);
+  }
+
   /**
    * @type {number}
    */
@@ -2235,15 +2151,20 @@ class PDFViewer {
     this.#annotationEditorUIManager.updateParams(type, value);
   }
 
-  refresh() {
+  refresh(noUpdate = false, updateArgs = Object.create(null)) {
     if (!this.pdfDocument) {
       return;
     }
-    const updateArgs = {};
     for (const pageView of this._pages) {
       pageView.update(updateArgs);
     }
-    this.update();
+    if (this.#scaleTimeoutId !== null) {
+      clearTimeout(this.#scaleTimeoutId);
+      this.#scaleTimeoutId = null;
+    }
+    if (!noUpdate) {
+      this.update();
+    }
   }
 }
 

+ 1 - 1
app/src/asset/pdf/pdfjs.js

@@ -18,5 +18,5 @@
 
 const {addScriptSync} = require('../../protyle/util/addScript')
 const {Constants} = require('../../constants')
-addScriptSync(`${Constants.PROTYLE_CDN}/js/pdf/pdf.js?v=3.0.150`, 'pdfjsScript')
+addScriptSync(`${Constants.PROTYLE_CDN}/js/pdf/pdf.js?v=3.4.120`, 'pdfjsScript')
 module.exports = window["pdfjs-dist/build/pdf"];

+ 1 - 2
app/src/asset/pdf/secondary_toolbar.js

@@ -13,8 +13,7 @@
  * limitations under the License.
  */
 
-import { ScrollMode, SpreadMode } from "./ui_utils.js";
-import { CursorTool } from "./pdf_cursor_tools.js";
+import { CursorTool, ScrollMode, SpreadMode } from "./ui_utils.js";
 import { PagesCountLimit } from "./pdf_viewer.js";
 
 /**

+ 27 - 18
app/src/asset/pdf/struct_tree_layer_builder.js

@@ -13,8 +13,6 @@
  * limitations under the License.
  */
 
-/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
-
 const PDF_ROLE_TO_HTML_ROLE = {
   // Document level structure types
   Document: null, // There's a "document" role, but it doesn't make sense here.
@@ -73,24 +71,35 @@ const PDF_ROLE_TO_HTML_ROLE = {
 
 const HEADING_PATTERN = /^H(\d+)$/;
 
-/**
- * @typedef {Object} StructTreeLayerBuilderOptions
- * @property {PDFPageProxy} pdfPage
- */
-
 class StructTreeLayerBuilder {
-  /**
-   * @param {StructTreeLayerBuilderOptions} options
-   */
-  constructor({ pdfPage }) {
-    this.pdfPage = pdfPage;
+  #treeDom = undefined;
+
+  get renderingDone() {
+    return this.#treeDom !== undefined;
   }
 
   render(structTree) {
-    return this._walk(structTree);
+    if (this.#treeDom !== undefined) {
+      return this.#treeDom;
+    }
+    const treeDom = this.#walk(structTree);
+    treeDom?.classList.add("structTree");
+    return (this.#treeDom = treeDom);
+  }
+
+  hide() {
+    if (this.#treeDom && !this.#treeDom.hidden) {
+      this.#treeDom.hidden = true;
+    }
+  }
+
+  show() {
+    if (this.#treeDom?.hidden) {
+      this.#treeDom.hidden = false;
+    }
   }
 
-  _setAttributes(structElement, htmlElement) {
+  #setAttributes(structElement, htmlElement) {
     if (structElement.alt !== undefined) {
       htmlElement.setAttribute("aria-label", structElement.alt);
     }
@@ -102,7 +111,7 @@ class StructTreeLayerBuilder {
     }
   }
 
-  _walk(node) {
+  #walk(node) {
     if (!node) {
       return null;
     }
@@ -119,16 +128,16 @@ class StructTreeLayerBuilder {
       }
     }
 
-    this._setAttributes(node, element);
+    this.#setAttributes(node, element);
 
     if (node.children) {
       if (node.children.length === 1 && "id" in node.children[0]) {
         // Often there is only one content node so just set the values on the
         // parent node to avoid creating an extra span.
-        this._setAttributes(node.children[0], element);
+        this.#setAttributes(node.children[0], element);
       } else {
         for (const kid of node.children) {
-          element.append(this._walk(kid));
+          element.append(this.#walk(kid));
         }
       }
     }

+ 5 - 5
app/src/asset/pdf/text_highlighter.js

@@ -95,6 +95,7 @@ class TextHighlighter {
       );
       this._onUpdateTextLayerMatches = null;
     }
+    this._updateMatches(/* reset = */ true);
   }
 
   _convertMatches(matches, matchesLength) {
@@ -264,8 +265,8 @@ class TextHighlighter {
     }
   }
 
-  _updateMatches() {
-    if (!this.enabled) {
+  _updateMatches(reset = false) {
+    if (!this.enabled && !reset) {
       return;
     }
     const { findController, matches, pageIdx } = this;
@@ -273,8 +274,7 @@ class TextHighlighter {
     let clearedUntilDivIdx = -1;
 
     // Clear all current matches.
-    for (let i = 0, ii = matches.length; i < ii; i++) {
-      const match = matches[i];
+    for (const match of matches) {
       const begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
       for (let n = begin, end = match.end.divIdx; n <= end; n++) {
         const div = textDivs[n];
@@ -284,7 +284,7 @@ class TextHighlighter {
       clearedUntilDivIdx = match.end.divIdx + 1;
     }
 
-    if (!findController?.highlightMatches) {
+    if (!findController?.highlightMatches || reset) {
       return;
     }
     // Convert the matches on the `findController` into the match format

+ 89 - 57
app/src/asset/pdf/text_layer_builder.js

@@ -15,23 +15,21 @@
 
 // eslint-disable-next-line max-len
 /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
-/** @typedef {import("./event_utils").EventBus} EventBus */
+/** @typedef {import("../src/display/api").TextContent} TextContent */
 /** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
 // eslint-disable-next-line max-len
 /** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
 
-import { renderTextLayer } from "./pdfjs";
-import { getHighlight } from '../anno'
+import { renderTextLayer, updateTextLayer } from "./pdfjs";
+import {getHighlight} from "../anno";
 
 /**
  * @typedef {Object} TextLayerBuilderOptions
- * @property {HTMLDivElement} textLayerDiv - The text layer container.
- * @property {EventBus} eventBus - The application event bus.
- * @property {number} pageIndex - The page index.
- * @property {PageViewport} viewport - The viewport of the text layer.
  * @property {TextHighlighter} highlighter - Optional object that will handle
  *   highlighting text from the find controller.
  * @property {TextAccessibilityManager} [accessibilityManager]
+ * @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
+ *   OffscreenCanvas if needed.
  */
 
 /**
@@ -40,28 +38,29 @@ import { getHighlight } from '../anno'
  * contain text that matches the PDF text they are overlaying.
  */
 class TextLayerBuilder {
+  #rotation = 0;
+
+  #scale = 0;
+
+  #textContentSource = null;
+
   constructor({
-    textLayerDiv,
-    eventBus,
-    pageIndex,
-    viewport,
     highlighter = null,
     accessibilityManager = null,
+    isOffscreenCanvasSupported = true,
   }) {
-    this.textLayerDiv = textLayerDiv;
-    this.eventBus = eventBus;
-    this.textContent = null;
     this.textContentItemsStr = [];
-    this.textContentStream = null;
     this.renderingDone = false;
-    this.pageNumber = pageIndex + 1;
-    this.viewport = viewport;
     this.textDivs = [];
+    this.textDivProperties = new WeakMap();
     this.textLayerRenderTask = null;
     this.highlighter = highlighter;
     this.accessibilityManager = accessibilityManager;
+    this.isOffscreenCanvasSupported = isOffscreenCanvasSupported;
 
-    this.#bindMouse();
+    this.div = document.createElement("div");
+    this.div.className = "textLayer";
+    this.hide();
   }
 
   #finishRendering() {
@@ -69,54 +68,86 @@ class TextLayerBuilder {
 
     const endOfContent = document.createElement("div");
     endOfContent.className = "endOfContent";
-    this.textLayerDiv.append(endOfContent);
+    this.div.append(endOfContent);
+
+    this.#bindMouse();
 
-    this.eventBus.dispatch("textlayerrendered", {
-      source: this,
-      pageNumber: this.pageNumber,
-      numTextDivs: this.textDivs.length,
-    });
     // NOTE
-    getHighlight(this.textLayerDiv)
+    getHighlight(this.div)
+  }
+
+  get numTextDivs() {
+    return this.textDivs.length;
   }
 
   /**
    * Renders the text layer.
-   *
-   * @param {number} [timeout] - Wait for a specified amount of milliseconds
-   *                             before rendering.
+   * @param {PageViewport} viewport
    */
-  render(timeout = 0) {
-    if (!(this.textContent || this.textContentStream) || this.renderingDone) {
+  async render(viewport) {
+    if (!this.#textContentSource) {
+      throw new Error('No "textContentSource" parameter specified.');
+    }
+
+    const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
+    const { rotation } = viewport;
+    if (this.renderingDone) {
+      const mustRotate = rotation !== this.#rotation;
+      const mustRescale = scale !== this.#scale;
+      if (mustRotate || mustRescale) {
+        this.hide();
+        updateTextLayer({
+          container: this.div,
+          viewport,
+          textDivs: this.textDivs,
+          textDivProperties: this.textDivProperties,
+          isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
+          mustRescale,
+          mustRotate,
+        });
+        this.#scale = scale;
+        this.#rotation = rotation;
+      }
+      this.show();
       return;
     }
-    this.cancel();
 
-    this.textDivs.length = 0;
+    this.cancel();
     this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
     this.accessibilityManager?.setTextMapping(this.textDivs);
 
-    const textLayerFrag = document.createDocumentFragment();
     this.textLayerRenderTask = renderTextLayer({
-      textContent: this.textContent,
-      textContentStream: this.textContentStream,
-      container: textLayerFrag,
-      viewport: this.viewport,
+      textContentSource: this.#textContentSource,
+      container: this.div,
+      viewport,
       textDivs: this.textDivs,
+      textDivProperties: this.textDivProperties,
       textContentItemsStr: this.textContentItemsStr,
-      timeout,
+      isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
     });
-    this.textLayerRenderTask.promise.then(
-      () => {
-        this.textLayerDiv.append(textLayerFrag);
-        this.#finishRendering();
-        this.highlighter?.enable();
-        this.accessibilityManager?.enable();
-      },
-      function (reason) {
-        // Cancelled or failed to render text layer; skipping errors.
-      }
-    );
+
+    await this.textLayerRenderTask.promise;
+    this.#finishRendering();
+    this.#scale = scale;
+    this.#rotation = rotation;
+    this.show();
+    this.accessibilityManager?.enable();
+  }
+
+  hide() {
+    if (!this.div.hidden) {
+      // We turn off the highlighter in order to avoid to scroll into view an
+      // element of the text layer which could be hidden.
+      this.highlighter?.disable();
+      this.div.hidden = true;
+    }
+  }
+
+  show() {
+    if (this.div.hidden && this.renderingDone) {
+      this.div.hidden = false;
+      this.highlighter?.enable();
+    }
   }
 
   /**
@@ -129,16 +160,17 @@ class TextLayerBuilder {
     }
     this.highlighter?.disable();
     this.accessibilityManager?.disable();
+    this.textContentItemsStr.length = 0;
+    this.textDivs.length = 0;
+    this.textDivProperties = new WeakMap();
   }
 
-  setTextContentStream(readableStream) {
-    this.cancel();
-    this.textContentStream = readableStream;
-  }
-
-  setTextContent(textContent) {
+  /**
+   * @param {ReadableStream | TextContent} source
+   */
+  setTextContentSource(source) {
     this.cancel();
-    this.textContent = textContent;
+    this.#textContentSource = source;
   }
 
   /**
@@ -147,7 +179,7 @@ class TextLayerBuilder {
    * dragged up or down.
    */
   #bindMouse() {
-    const div = this.textLayerDiv;
+    const { div } = this;
 
     div.addEventListener("mousedown", evt => {
       const end = div.querySelector(".endOfContent");

+ 13 - 14
app/src/asset/pdf/toolbar.js

@@ -17,7 +17,6 @@ import {
   animationStarted,
   DEFAULT_SCALE,
   DEFAULT_SCALE_VALUE,
-  docStyle,
   MAX_SCALE,
   MIN_SCALE,
   noContextMenuHandler,
@@ -262,6 +261,7 @@ class Toolbar {
         items.pageNumber.type = "text";
       } else {
         items.pageNumber.type = "number";
+        // NOTE
         items.numPages.textContent =  "/ " + pagesCount;
       }
       items.pageNumber.max = pagesCount;
@@ -269,6 +269,7 @@ class Toolbar {
 
     if (this.hasPageLabels) {
       items.pageNumber.value = this.pageLabel;
+      // NOTE
       items.numPages.textContent = `(${pageNumber} / ${pagesCount})`
     } else {
       items.pageNumber.value = pageNumber;
@@ -318,15 +319,10 @@ class Toolbar {
 
     await animationStarted;
 
-    const style = getComputedStyle(items.scaleSelect),
-      scaleSelectContainerWidth = parseInt(
-        style.getPropertyValue("--scale-select-container-width"),
-        10
-      ),
-      scaleSelectOverflow = parseInt(
-        style.getPropertyValue("--scale-select-overflow"),
-        10
-      );
+    const style = getComputedStyle(items.scaleSelect);
+    const scaleSelectWidth = parseFloat(
+      style.getPropertyValue("--scale-select-width")
+    );
 
     // The temporary canvas is used to measure text length in the DOM.
     const canvas = document.createElement("canvas");
@@ -334,16 +330,19 @@ class Toolbar {
     ctx.font = `${style.fontSize} ${style.fontFamily}`;
 
     let maxWidth = 0;
-    for (const predefinedValue of predefinedValuesPromise) {
+    for (const predefinedValue of await predefinedValuesPromise) {
       const { width } = ctx.measureText(predefinedValue);
       if (width > maxWidth) {
         maxWidth = width;
       }
     }
-    maxWidth += 2 * scaleSelectOverflow;
+    // Account for the icon width, and ensure that there's always some spacing
+    // between the text and the icon.
+    maxWidth += 0.3 * scaleSelectWidth;
 
-    if (maxWidth > scaleSelectContainerWidth) {
-      docStyle.setProperty("--scale-select-container-width", `${maxWidth}px`);
+    if (maxWidth > scaleSelectWidth) {
+      const container = items.scaleSelect.parentNode;
+      container.style.setProperty("--scale-select-width", `${maxWidth}px`);
     }
     // Zeroing the width and height cause Firefox to release graphics resources
     // immediately, which can greatly reduce memory consumption.

+ 35 - 11
app/src/asset/pdf/ui_utils.js

@@ -13,8 +13,6 @@
  * limitations under the License.
  */
 
-import {hasClosestByAttribute} from "../../protyle/util/hasClosest";
-
 const DEFAULT_SCALE_VALUE = "auto";
 const DEFAULT_SCALE = 1.0;
 const DEFAULT_SCALE_DELTA = 1.1;
@@ -76,6 +74,12 @@ const SpreadMode = {
   EVEN: 2,
 };
 
+const CursorTool = {
+  SELECT: 0, // The default value.
+  HAND: 1,
+  ZOOM: 2,
+};
+
 // Used by `PDFViewerApplication`, and by the API unit-tests.
 const AutoPrintRegExp = /\bprint\s*\(/;
 
@@ -624,17 +628,16 @@ function normalizeWheelEventDirection(evt) {
 }
 
 function normalizeWheelEventDelta(evt) {
+  const deltaMode = evt.deltaMode; // Avoid being affected by bug 1392460.
   let delta = normalizeWheelEventDirection(evt);
 
-  const MOUSE_DOM_DELTA_PIXEL_MODE = 0;
-  const MOUSE_DOM_DELTA_LINE_MODE = 1;
   const MOUSE_PIXELS_PER_LINE = 30;
   const MOUSE_LINES_PER_PAGE = 30;
 
   // Converts delta to per-page units
-  if (evt.deltaMode === MOUSE_DOM_DELTA_PIXEL_MODE) {
+  if (deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
     delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
-  } else if (evt.deltaMode === MOUSE_DOM_DELTA_LINE_MODE) {
+  } else if (deltaMode === WheelEvent.DOM_DELTA_LINE) {
     delta /= MOUSE_LINES_PER_PAGE;
   }
   return delta;
@@ -695,13 +698,18 @@ function clamp(v, min, max) {
 class ProgressBar {
   #classList = null;
 
+  #disableAutoFetchTimeout = null;
+
   #percent = 0;
 
+  #style = null;
+
   #visible = true;
 
   // NOTE
   constructor(bar) {
     this.#classList = bar.classList;
+    this.#style = bar.style;
   }
 
   get percent() {
@@ -717,7 +725,7 @@ class ProgressBar {
     }
     this.#classList.remove("indeterminate");
 
-    docStyle.setProperty("--progressBar-percent", `${this.#percent}%`);
+    this.#style.setProperty("--progressBar-percent", `${this.#percent}%`);
   }
 
   setWidth(viewer) {
@@ -727,10 +735,28 @@ class ProgressBar {
     const container = viewer.parentNode;
     const scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
     if (scrollbarWidth > 0) {
-      docStyle.setProperty("--progressBar-end-offset", `${scrollbarWidth}px`);
+      this.#style.setProperty(
+        "--progressBar-end-offset",
+        `${scrollbarWidth}px`
+      );
     }
   }
 
+  setDisableAutoFetch(delay = /* ms = */ 5000) {
+    if (isNaN(this.#percent)) {
+      return;
+    }
+    if (this.#disableAutoFetchTimeout) {
+      clearTimeout(this.#disableAutoFetchTimeout);
+    }
+    this.show();
+
+    this.#disableAutoFetchTimeout = setTimeout(() => {
+      this.#disableAutoFetchTimeout = null;
+      this.hide();
+    }, delay);
+  }
+
   hide() {
     if (!this.#visible) {
       return;
@@ -775,9 +801,6 @@ function getActiveOrFocusedElement() {
 
 /**
  * Converts API PageLayout values to the format used by `BaseViewer`.
- * NOTE: This is supported to the extent that the viewer implements the
- *       necessary Scroll/Spread modes (since SinglePage, TwoPageLeft,
- *       and TwoPageRight all suggests using non-continuous scrolling).
  * @param {string} mode - The API PageLayout value.
  * @returns {Object}
  */
@@ -839,6 +862,7 @@ export {
   AutoPrintRegExp,
   backtrackBeforeAllVisibleElements, // only exported for testing
   binarySearchFirstItem,
+  CursorTool,
   DEFAULT_SCALE,
   DEFAULT_SCALE_DELTA,
   DEFAULT_SCALE_VALUE,

+ 0 - 2
app/src/asset/pdf/view_history.js

@@ -13,8 +13,6 @@
  * limitations under the License.
  */
 
-import {setStorageVal} from "../../protyle/util/compatibility";
-
 const DEFAULT_VIEW_HISTORY_CACHE_SIZE = 20;
 
 /**

+ 145 - 190
app/src/asset/pdf/viewer.js

@@ -13,237 +13,192 @@
  * limitations under the License.
  */
 
-import { RenderingStates, ScrollMode, SpreadMode } from './ui_utils.js'
-import { AppOptions } from './app_options.js'
-import { LinkTarget } from './pdf_link_service.js'
-import { PDFViewerApplication } from './app.js'
-import { initAnno } from '../anno'
+import { RenderingStates, ScrollMode, SpreadMode } from "./ui_utils.js";
+import { AppOptions } from "./app_options.js";
+import { LinkTarget } from "./pdf_link_service.js";
+import { PDFViewerApplication } from "./app.js";
+import { initAnno } from "../anno";
 
 /* eslint-disable-next-line no-unused-vars */
 const pdfjsVersion =
-  typeof PDFJSDev !== 'undefined' ? PDFJSDev.eval('BUNDLE_VERSION') : void 0
+  typeof PDFJSDev !== "undefined" ? PDFJSDev.eval("BUNDLE_VERSION") : void 0;
 /* eslint-disable-next-line no-unused-vars */
 const pdfjsBuild =
-  typeof PDFJSDev !== 'undefined' ? PDFJSDev.eval('BUNDLE_BUILD') : void 0
+  typeof PDFJSDev !== "undefined" ? PDFJSDev.eval("BUNDLE_BUILD") : void 0;
 
 const AppConstants =
-  typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
-    ? {LinkTarget, RenderingStates, ScrollMode, SpreadMode}
-    : null
-
-window.PDFViewerApplication = PDFViewerApplication
-window.PDFViewerApplicationConstants = AppConstants
-window.PDFViewerApplicationOptions = AppOptions
-
-if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
-  (function rewriteUrlClosure () {
-    // Run this code outside DOMContentLoaded to make sure that the URL
-    // is rewritten as soon as possible.
-    const queryString = document.location.search.slice(1)
-    const m = /(^|&)file=([^&]*)/.exec(queryString)
-    const defaultUrl = m ? decodeURIComponent(m[2]) : ''
-
-    // Example: chrome-extension://.../http://example.com/file.pdf
-    const humanReadableUrl = '/' + defaultUrl + location.hash
-    history.replaceState(history.state, '', humanReadableUrl)
-    if (top === window) {
-      // eslint-disable-next-line no-undef
-      chrome.runtime.sendMessage('showPageAction')
-    }
-
-    AppOptions.set('defaultUrl', defaultUrl)
-  })()
-}
+  typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
+    ? { LinkTarget, RenderingStates, ScrollMode, SpreadMode }
+    : null;
 
 // NOTE
-// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) {
-//   require('./firefoxcom.js')
-//   require('./firefox_print_service.js')
-// }
-// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC')) {
-//   require('./genericcom.js')
-// }
-// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
-//   require('./chromecom.js')
-// }
-// if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME || GENERIC')) {
-//   require('./pdf_print_service.js')
-// }
+// window.PDFViewerApplication = PDFViewerApplication;
+// window.PDFViewerApplicationConstants = AppConstants;
+// window.PDFViewerApplicationOptions = AppOptions;
 
-// NOTE
-function getViewerConfiguration (element) {
+function getViewerConfiguration(element) {
   return {
     appContainer: element,
-    mainContainer: element.querySelector('#viewerContainer'),
-    viewerContainer: element.querySelector('#viewer'),
+    mainContainer: element.querySelector("#viewerContainer"),
+    viewerContainer: element.querySelector("#viewer"),
     toolbar: {
       // NOTE
-      rectAnno: element.querySelector('#rectAnno'),
-      container: element.querySelector('#toolbarViewer'),
-      numPages: element.querySelector('#numPages'),
-      pageNumber: element.querySelector('#pageNumber'),
-      scaleSelect: element.querySelector('#scaleSelect'),
-      customScaleOption: element.querySelector('#customScaleOption'),
-      previous: element.querySelector('#previous'),
-      next: element.querySelector('#next'),
-      zoomIn: element.querySelector('#zoomIn'),
-      zoomOut: element.querySelector('#zoomOut'),
-      viewFind: element.querySelector('#viewFind'),
+      rectAnno: element.querySelector("#rectAnno"),
+      container: element.querySelector("#toolbarViewer"),
+      numPages: element.querySelector("#numPages"),
+      pageNumber: element.querySelector("#pageNumber"),
+      scaleSelect: element.querySelector("#scaleSelect"),
+      customScaleOption: element.querySelector("#customScaleOption"),
+      previous: element.querySelector("#previous"),
+      next: element.querySelector("#next"),
+      zoomIn: element.querySelector("#zoomIn"),
+      zoomOut: element.querySelector("#zoomOut"),
+      viewFind: element.querySelector("#viewFind"),
       openFile:
-        typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
-          ? element.querySelector('#openFile')
+        typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
+          ? element.querySelector("#openFile")
           : null,
-      print: element.querySelector('#print'),
-      editorFreeTextButton: element.querySelector('#editorFreeText'),
-      editorFreeTextParamsToolbar: element.querySelector('#editorFreeTextParamsToolbar'),
-      editorInkButton: element.querySelector('#editorInk'),
-      editorInkParamsToolbar: element.querySelector('#editorInkParamsToolbar'),
-      download: element.querySelector('#download'),
+      print: element.querySelector("#print"),
+      editorFreeTextButton: element.querySelector("#editorFreeText"),
+      editorFreeTextParamsToolbar: element.querySelector(
+        "#editorFreeTextParamsToolbar"
+      ),
+      editorInkButton: element.querySelector("#editorInk"),
+      editorInkParamsToolbar: element.querySelector("#editorInkParamsToolbar"),
+      download: element.querySelector("#download"),
     },
     secondaryToolbar: {
-      toolbar: element.querySelector('#secondaryToolbar'),
-      toggleButton: element.querySelector('#secondaryToolbarToggle'),
-      presentationModeButton: element.querySelector('#presentationMode'),
+      toolbar: element.querySelector("#secondaryToolbar"),
+      toggleButton: element.querySelector("#secondaryToolbarToggle"),
+      presentationModeButton: element.querySelector("#presentationMode"),
       openFileButton:
-        typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
-          ? element.querySelector('#secondaryOpenFile')
+        typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
+          ? element.querySelector("#secondaryOpenFile")
           : null,
-      printButton: element.querySelector('#secondaryPrint'),
-      downloadButton: element.querySelector('#secondaryDownload'),
-      viewBookmarkButton: element.querySelector('#viewBookmark'),
-      firstPageButton: element.querySelector('#firstPage'),
-      lastPageButton: element.querySelector('#lastPage'),
-      pageRotateCwButton: element.querySelector('#pageRotateCw'),
-      pageRotateCcwButton: element.querySelector('#pageRotateCcw'),
-      cursorSelectToolButton: element.querySelector('#cursorSelectTool'),
-      cursorHandToolButton: element.querySelector('#cursorHandTool'),
-      scrollPageButton: element.querySelector('#scrollPage'),
-      scrollVerticalButton: element.querySelector('#scrollVertical'),
-      scrollHorizontalButton: element.querySelector('#scrollHorizontal'),
-      scrollWrappedButton: element.querySelector('#scrollWrapped'),
-      spreadNoneButton: element.querySelector('#spreadNone'),
-      spreadOddButton: element.querySelector('#spreadOdd'),
-      spreadEvenButton: element.querySelector('#spreadEven'),
-      documentPropertiesButton: element.querySelector('#documentProperties'),
+      printButton: element.querySelector("#secondaryPrint"),
+      downloadButton: element.querySelector("#secondaryDownload"),
+      viewBookmarkButton: element.querySelector("#viewBookmark"),
+      firstPageButton: element.querySelector("#firstPage"),
+      lastPageButton: element.querySelector("#lastPage"),
+      pageRotateCwButton: element.querySelector("#pageRotateCw"),
+      pageRotateCcwButton: element.querySelector("#pageRotateCcw"),
+      cursorSelectToolButton: element.querySelector("#cursorSelectTool"),
+      cursorHandToolButton: element.querySelector("#cursorHandTool"),
+      scrollPageButton: element.querySelector("#scrollPage"),
+      scrollVerticalButton: element.querySelector("#scrollVertical"),
+      scrollHorizontalButton: element.querySelector("#scrollHorizontal"),
+      scrollWrappedButton: element.querySelector("#scrollWrapped"),
+      spreadNoneButton: element.querySelector("#spreadNone"),
+      spreadOddButton: element.querySelector("#spreadOdd"),
+      spreadEvenButton: element.querySelector("#spreadEven"),
+      documentPropertiesButton: element.querySelector("#documentProperties"),
     },
     sidebar: {
       // Divs (and sidebar button)
-      outerContainer: element.querySelector('#outerContainer'),
-      sidebarContainer: element.querySelector('#sidebarContainer'),
-      toggleButton: element.querySelector('#sidebarToggle'),
+      outerContainer: element.querySelector("#outerContainer"),
+      sidebarContainer: element.querySelector("#sidebarContainer"),
+      toggleButton: element.querySelector("#sidebarToggle"),
       // Buttons
-      thumbnailButton: element.querySelector('#viewThumbnail'),
-      outlineButton: element.querySelector('#viewOutline'),
-      attachmentsButton: element.querySelector('#viewAttachments'),
-      layersButton: element.querySelector('#viewLayers'),
+      thumbnailButton: element.querySelector("#viewThumbnail"),
+      outlineButton: element.querySelector("#viewOutline"),
+      attachmentsButton: element.querySelector("#viewAttachments"),
+      layersButton: element.querySelector("#viewLayers"),
       // Views
-      thumbnailView: element.querySelector('#thumbnailView'),
-      outlineView: element.querySelector('#outlineView'),
-      attachmentsView: element.querySelector('#attachmentsView'),
-      layersView: element.querySelector('#layersView'),
+      thumbnailView: element.querySelector("#thumbnailView"),
+      outlineView: element.querySelector("#outlineView"),
+      attachmentsView: element.querySelector("#attachmentsView"),
+      layersView: element.querySelector("#layersView"),
       // View-specific options
-      outlineOptionsContainer: element.querySelector('#outlineOptionsContainer'),
-      currentOutlineItemButton: element.querySelector('#currentOutlineItem'),
+      outlineOptionsContainer: element.querySelector("#outlineOptionsContainer"),
+      currentOutlineItemButton: element.querySelector("#currentOutlineItem"),
     },
     sidebarResizer: {
-      outerContainer: element.querySelector('#outerContainer'),
-      resizer: element.querySelector('#sidebarResizer'),
+      outerContainer: element.querySelector("#outerContainer"),
+      resizer: element.querySelector("#sidebarResizer"),
     },
     findBar: {
-      bar: element.querySelector('#findbar'),
-      toggleButton: element.querySelector('#viewFind'),
-      findField: element.querySelector('#findInput'),
-      highlightAllCheckbox: element.querySelector('#findHighlightAll'),
-      caseSensitiveCheckbox: element.querySelector('#findMatchCase'),
-      matchDiacriticsCheckbox: element.querySelector('#findMatchDiacritics'),
-      entireWordCheckbox: element.querySelector('#findEntireWord'),
-      findMsg: element.querySelector('#findMsg'),
-      findResultsCount: element.querySelector('#findResultsCount'),
-      findPreviousButton: element.querySelector('#findPrevious'),
-      findNextButton: element.querySelector('#findNext'),
+      bar: element.querySelector("#findbar"),
+      toggleButton: element.querySelector("#viewFind"),
+      findField: element.querySelector("#findInput"),
+      highlightAllCheckbox: element.querySelector("#findHighlightAll"),
+      caseSensitiveCheckbox: element.querySelector("#findMatchCase"),
+      matchDiacriticsCheckbox: element.querySelector("#findMatchDiacritics"),
+      entireWordCheckbox: element.querySelector("#findEntireWord"),
+      findMsg: element.querySelector("#findMsg"),
+      findResultsCount: element.querySelector("#findResultsCount"),
+      findPreviousButton: element.querySelector("#findPrevious"),
+      findNextButton: element.querySelector("#findNext"),
     },
     passwordOverlay: {
-      dialog: element.querySelector('#passwordDialog'),
-      label: element.querySelector('#passwordText'),
-      input: element.querySelector('#password'),
-      submitButton: element.querySelector('#passwordSubmit'),
-      cancelButton: element.querySelector('#passwordCancel'),
+      dialog: element.querySelector("#passwordDialog"),
+      label: element.querySelector("#passwordText"),
+      input: element.querySelector("#password"),
+      submitButton: element.querySelector("#passwordSubmit"),
+      cancelButton: element.querySelector("#passwordCancel"),
     },
     documentProperties: {
-      dialog: element.querySelector('#documentPropertiesDialog'),
-      closeButton: element.querySelector('#documentPropertiesClose'),
+      dialog: element.querySelector("#documentPropertiesDialog"),
+      closeButton: element.querySelector("#documentPropertiesClose"),
       fields: {
-        fileName: element.querySelector('#fileNameField'),
-        fileSize: element.querySelector('#fileSizeField'),
-        title: element.querySelector('#titleField'),
-        author: element.querySelector('#authorField'),
-        subject: element.querySelector('#subjectField'),
-        keywords: element.querySelector('#keywordsField'),
-        creationDate: element.querySelector('#creationDateField'),
-        modificationDate: element.querySelector('#modificationDateField'),
-        creator: element.querySelector('#creatorField'),
-        producer: element.querySelector('#producerField'),
-        version: element.querySelector('#versionField'),
-        pageCount: element.querySelector('#pageCountField'),
-        pageSize: element.querySelector('#pageSizeField'),
-        linearized: element.querySelector('#linearizedField'),
+        fileName: element.querySelector("#fileNameField"),
+        fileSize: element.querySelector("#fileSizeField"),
+        title: element.querySelector("#titleField"),
+        author: element.querySelector("#authorField"),
+        subject: element.querySelector("#subjectField"),
+        keywords: element.querySelector("#keywordsField"),
+        creationDate: element.querySelector("#creationDateField"),
+        modificationDate: element.querySelector("#modificationDateField"),
+        creator: element.querySelector("#creatorField"),
+        producer: element.querySelector("#producerField"),
+        version: element.querySelector("#versionField"),
+        pageCount: element.querySelector("#pageCountField"),
+        pageSize: element.querySelector("#pageSizeField"),
+        linearized: element.querySelector("#linearizedField"),
       },
     },
     annotationEditorParams: {
-      editorFreeTextFontSize: element.querySelector('#editorFreeTextFontSize'),
-      editorFreeTextColor: element.querySelector('#editorFreeTextColor'),
-      editorInkColor: element.querySelector('#editorInkColor'),
-      editorInkThickness: element.querySelector('#editorInkThickness'),
-      editorInkOpacity: element.querySelector('#editorInkOpacity'),
+      editorFreeTextFontSize: element.querySelector("#editorFreeTextFontSize"),
+      editorFreeTextColor: element.querySelector("#editorFreeTextColor"),
+      editorInkColor: element.querySelector("#editorInkColor"),
+      editorInkThickness: element.querySelector("#editorInkThickness"),
+      editorInkOpacity: element.querySelector("#editorInkOpacity"),
     },
-    errorWrapper:
-      typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')
-        ? {
-          container: element.querySelector('#errorWrapper'),
-          errorMessage: element.querySelector('#errorMessage'),
-          closeButton: element.querySelector('#errorClose'),
-          errorMoreInfo: element.querySelector('#errorMoreInfo'),
-          moreInfoButton: element.querySelector('#errorShowMore'),
-          lessInfoButton: element.querySelector('#errorShowLess'),
-        }
-        : null,
-    printContainer: element.querySelector('#printContainer'),
+    printContainer: element.querySelector("#printContainer"),
     openFileInput:
-      typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')
-        ? element.querySelector('#fileInput')
+      typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
+        ? element.querySelector("#fileInput")
         : null,
-    debuggerScriptPath: './debugger.js',
+    debuggerScriptPath: "./debugger.js",
   }
 }
 
 // NOTE
-function webViewerLoad (file, element, pdfPage, annoId) {
+function webViewerLoad(file, element, pdfPage, annoId) {
   const pdf = new PDFViewerApplication(pdfPage)
   pdf.annoId = annoId
-  const config = getViewerConfiguration(element)
-  if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('PRODUCTION')) {
-    config.file = file
-  } else {
-    if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('GENERIC')) {
-      // Give custom implementations of the default viewer a simpler way to
-      // set various `AppOptions`, by dispatching an event once all viewer
-      // files are loaded but *before* the viewer initialization has run.
-      const event = document.createEvent('CustomEvent')
-      event.initCustomEvent('webviewerloaded', true, true, {
-        source: window,
-      })
-      try {
-        // Attempt to dispatch the event at the embedding `document`,
-        // in order to support cases where the viewer is embedded in
-        // a *dynamically* created <iframe> element.
-        parent.document.dispatchEvent(event)
-      } catch (ex) {
-        // The viewer could be in e.g. a cross-origin <iframe> element,
-        // fallback to dispatching the event at the current `document`.
-        console.error(`webviewerloaded: ${ex}`)
-        document.dispatchEvent(event)
-      }
+  const config = getViewerConfiguration(element);
+
+  if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
+    // Give custom implementations of the default viewer a simpler way to
+    // set various `AppOptions`, by dispatching an event once all viewer
+    // files are loaded but *before* the viewer initialization has run.
+    const event = document.createEvent("CustomEvent");
+    event.initCustomEvent("webviewerloaded", true, true, {
+      source: window,
+    });
+    try {
+      // Attempt to dispatch the event at the embedding `document`,
+      // in order to support cases where the viewer is embedded in
+      // a *dynamically* created <iframe> element.
+      parent.document.dispatchEvent(event);
+    } catch (ex) {
+      // The viewer could be in e.g. a cross-origin <iframe> element,
+      // fallback to dispatching the event at the current `document`.
+      console.error(`webviewerloaded: ${ex}`);
+      document.dispatchEvent(event);
     }
+  } else {
+    config.file = file
   }
   pdf.run(config)
   initAnno(file, element, annoId, pdf, config);
@@ -252,19 +207,19 @@ function webViewerLoad (file, element, pdfPage, annoId) {
 
 // Block the "load" event until all pages are loaded, to ensure that printing
 // works in Firefox; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
+document.blockUnblockOnload?.(true);
 
-document.blockUnblockOnload?.(true)
 // NOTE
 // if (
-//   document.readyState === 'interactive' ||
-//   document.readyState === 'complete'
+//   document.readyState === "interactive" ||
+//   document.readyState === "complete"
 // ) {
-//   webViewerLoad()
+//   webViewerLoad();
 // } else {
-//   document.addEventListener('DOMContentLoaded', webViewerLoad, true)
+//   document.addEventListener("DOMContentLoaded", webViewerLoad, true);
 // }
 
 // NOTE
 export {
-  webViewerLoad
-}
+  webViewerLoad,
+};

+ 24 - 30
app/src/asset/pdf/xfa_layer_builder.js

@@ -55,9 +55,9 @@ class XfaLayerBuilder {
    * @param {string} intent (default value is 'display')
    * @returns {Promise<Object | void>} A promise that is resolved when rendering
    *   of the XFA layer is complete. The first rendering will return an object
-   *   with a `textDivs` property that  can be used with the TextHighlighter.
+   *   with a `textDivs` property that can be used with the TextHighlighter.
    */
-  render(viewport, intent = "display") {
+  async render(viewport, intent = "display") {
     if (intent === "print") {
       const parameters = {
         viewport: viewport.clone({ dontFlip: true }),
@@ -73,39 +73,33 @@ class XfaLayerBuilder {
       this.pageDiv.append(div);
       parameters.div = div;
 
-      const result = XfaLayer.render(parameters);
-      return Promise.resolve(result);
+      return XfaLayer.render(parameters);
     }
 
     // intent === "display"
-    return this.pdfPage
-      .getXfa()
-      .then(xfaHtml => {
-        if (this._cancelled || !xfaHtml) {
-          return { textDivs: [] };
-        }
+    const xfaHtml = await this.pdfPage.getXfa();
+    if (this._cancelled || !xfaHtml) {
+      return { textDivs: [] };
+    }
+
+    const parameters = {
+      viewport: viewport.clone({ dontFlip: true }),
+      div: this.div,
+      xfaHtml,
+      annotationStorage: this.annotationStorage,
+      linkService: this.linkService,
+      intent,
+    };
 
-        const parameters = {
-          viewport: viewport.clone({ dontFlip: true }),
-          div: this.div,
-          xfaHtml,
-          annotationStorage: this.annotationStorage,
-          linkService: this.linkService,
-          intent,
-        };
+    if (this.div) {
+      return XfaLayer.update(parameters);
+    }
+    // Create an xfa layer div and render the form
+    this.div = document.createElement("div");
+    this.pageDiv.append(this.div);
+    parameters.div = this.div;
 
-        if (this.div) {
-          return XfaLayer.update(parameters);
-        }
-        // Create an xfa layer div and render the form
-        this.div = document.createElement("div");
-        this.pageDiv.append(this.div);
-        parameters.div = this.div;
-        return XfaLayer.render(parameters);
-      })
-      .catch(error => {
-        console.error(error);
-      });
+    return XfaLayer.render(parameters);
   }
 
   cancel() {

+ 12 - 6
app/src/assets/scss/pdf/_pdf.scss

@@ -398,12 +398,18 @@
   }
 }
 
-.dropdownToolbarButton select {
-  min-width: 124px;
-  height: 24px;
-  margin: 4px 8px 0 0;
-  line-height: 24px;
-  padding: 0 8px;
+.dropdownToolbarButton {
+  --scale-select-width: 140px;
+  width: var(--scale-select-width);
+
+  select {
+    width: inherit;
+    min-width: auto;
+    height: 24px;
+    margin: 4px 8px 0 0;
+    line-height: 24px;
+    padding: 0 8px;
+  }
 }
 
 #customScaleOption {

+ 142 - 34
app/src/assets/scss/pdf/annotation_layer_builder.scss

@@ -13,9 +13,65 @@
  * limitations under the License.
  */
 
+:root {
+  --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>");
+  --input-focus-border-color: Highlight;
+  --input-focus-outline: 1px solid Canvas;
+  --input-unfocused-border-color: transparent;
+  --input-disabled-border-color: transparent;
+  --input-hover-border-color: black;
+  --link-outline: none;
+}
+
+@media screen and (forced-colors: active) {
+  :root {
+    --input-focus-border-color: CanvasText;
+    --input-unfocused-border-color: ActiveText;
+    --input-disabled-border-color: GrayText;
+    --input-hover-border-color: Highlight;
+    --link-outline: 1.5px solid LinkText;
+  }
+
+  .annotationLayer .linkAnnotation:hover {
+    backdrop-filter: invert(100%);
+  }
+}
+
+.annotationLayer {
+  position: absolute;
+  top: 0;
+  left: 0;
+  pointer-events: none;
+  transform-origin: 0 0;
+  z-index: 3;
+}
+
+.annotationLayer[data-main-rotation="90"] .norotate {
+  transform: rotate(270deg) translateX(-100%);
+}
+.annotationLayer[data-main-rotation="180"] .norotate {
+  transform: rotate(180deg) translate(-100%, -100%);
+}
+.annotationLayer[data-main-rotation="270"] .norotate {
+  transform: rotate(90deg) translateY(-100%);
+}
+
+.annotationLayer canvas {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+}
+
 .annotationLayer section {
   position: absolute;
   text-align: initial;
+  pointer-events: auto;
+  box-sizing: border-box;
+  transform-origin: 0 0;
+}
+
+.annotationLayer .linkAnnotation {
+  outline: var(--link-outline);
 }
 
 .annotationLayer .linkAnnotation > a,
@@ -28,13 +84,6 @@
   height: 100%;
 }
 
-.annotationLayer .buttonWidgetAnnotation.pushButton > canvas {
-  position: relative;
-  top: 0;
-  left: 0;
-  z-index: -1;
-}
-
 .annotationLayer .linkAnnotation > a:hover,
 .annotationLayer .buttonWidgetAnnotation.pushButton > a:hover {
   opacity: 0.2;
@@ -45,6 +94,10 @@
 .annotationLayer .textAnnotation img {
   position: absolute;
   cursor: pointer;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
 }
 
 .annotationLayer .textWidgetAnnotation input,
@@ -52,17 +105,24 @@
 .annotationLayer .choiceWidgetAnnotation select,
 .annotationLayer .buttonWidgetAnnotation.checkBox input,
 .annotationLayer .buttonWidgetAnnotation.radioButton input {
-  background-color: rgba(0, 54, 255, 0.13);
-  border: 1px solid transparent;
+  background-color: rgba(0, 54, 255, 0.13); // NOTE
+  border: 2px solid var(--input-unfocused-border-color);
   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;
 }
@@ -72,8 +132,6 @@
 }
 
 .annotationLayer .textWidgetAnnotation textarea {
-  font: message-box;
-  font-size: 9px;
   resize: none;
 }
 
@@ -83,7 +141,7 @@
 .annotationLayer .buttonWidgetAnnotation.checkBox input[disabled],
 .annotationLayer .buttonWidgetAnnotation.radioButton input[disabled] {
   background: none;
-  border: 1px solid transparent;
+  border: 2px solid var(--input-disabled-border-color);
   cursor: not-allowed;
 }
 
@@ -92,30 +150,45 @@
 .annotationLayer .choiceWidgetAnnotation select:hover,
 .annotationLayer .buttonWidgetAnnotation.checkBox input:hover,
 .annotationLayer .buttonWidgetAnnotation.radioButton input:hover {
-  border: 1px solid rgba(0, 0, 0, 1);
+  border: 2px solid var(--input-hover-border-color);
+}
+.annotationLayer .textWidgetAnnotation input:hover,
+.annotationLayer .textWidgetAnnotation textarea:hover,
+.annotationLayer .choiceWidgetAnnotation select:hover,
+.annotationLayer .buttonWidgetAnnotation.checkBox input:hover {
+  border-radius: 2px;
 }
 
 .annotationLayer .textWidgetAnnotation input:focus,
 .annotationLayer .textWidgetAnnotation textarea:focus,
 .annotationLayer .choiceWidgetAnnotation select:focus {
   background: none;
-  border: 1px solid transparent;
+  border: 2px solid var(--input-focus-border-color);
+  border-radius: 2px;
+  outline: var(--input-focus-outline);
 }
 
-.annotationLayer .textWidgetAnnotation input :focus,
-.annotationLayer .textWidgetAnnotation textarea :focus,
-.annotationLayer .choiceWidgetAnnotation select :focus,
 .annotationLayer .buttonWidgetAnnotation.checkBox :focus,
 .annotationLayer .buttonWidgetAnnotation.radioButton :focus {
   background-image: none;
   background-color: transparent;
-  outline: auto;
+}
+
+.annotationLayer .buttonWidgetAnnotation.checkBox :focus {
+  border: 2px solid var(--input-focus-border-color);
+  border-radius: 2px;
+  outline: var(--input-focus-outline);
+}
+
+.annotationLayer .buttonWidgetAnnotation.radioButton :focus {
+  border: 2px solid var(--input-focus-border-color);
+  outline: var(--input-focus-outline);
 }
 
 .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: rgba(0, 0, 0, 1); // NOTE
   content: "";
   display: block;
   position: absolute;
@@ -163,32 +236,43 @@
 .annotationLayer .buttonWidgetAnnotation.checkBox input,
 .annotationLayer .buttonWidgetAnnotation.radioButton input {
   appearance: none;
-  padding: 0;
+}
+
+.annotationLayer .popupTriggerArea {
+  height: 100%;
+  width: 100%;
+}
+
+.annotationLayer .fileAttachmentAnnotation .popupTriggerArea {
+  position: absolute;
 }
 
 .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 {
@@ -197,17 +281,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,
@@ -226,3 +311,26 @@
 .annotationLayer .fileAttachmentAnnotation {
   cursor: pointer;
 }
+
+.annotationLayer section svg {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+}
+
+.annotationLayer .annotationTextContent {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  opacity: 0;
+  color: transparent;
+  user-select: none;
+  pointer-events: none;
+}
+
+.annotationLayer .annotationTextContent span {
+  width: 100%;
+  display: inline-block;
+}

+ 65 - 23
app/src/assets/scss/pdf/pdf_viewer.scss

@@ -17,29 +17,46 @@
 @import "xfa_layer_builder";
 
 :root {
+  --viewer-container-height: 0;
   --pdfViewer-padding-bottom: 0;
   --page-margin: 1px auto -8px;
   --page-border: 9px solid transparent;
   --spreadHorizontalWrapped-margin-LR: -3.5px;
-  --zoom-factor: 1;
-  --viewport-scale-factor: 1;
+  --loading-icon-delay: 400ms;
 }
 
 @media screen and (forced-colors: active) {
   :root {
     --pdfViewer-padding-bottom: 9px;
-    --page-margin: 9px auto 0;
+    --page-margin: 8px auto -1px;
     --page-border: none;
-    --spreadHorizontalWrapped-margin-LR: 4.5px;
+    --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 {
+  /* Define this variable here and not in :root to avoid to reflow all the UI
+     when scaling (see #15929). */
+  --scale-factor: 1;
+
   padding-bottom: var(--pdfViewer-padding-bottom);
 }
 
 .pdfViewer .canvasWrapper {
   overflow: hidden;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
 }
 
 .pdfViewer .page {
@@ -51,21 +68,23 @@
   overflow: visible;
   border: var(--page-border);
   background-clip: content-box;
-  // border-image: url(images/shadow.png) 9 9 repeat;
-  // background-color: rgba(255, 255, 255, 1);
+  background-color: rgba(255, 255, 255, 1);
 }
 
 .pdfViewer .dummyPage {
   position: relative;
   width: 0;
-  /* The height is set via JS, see `BaseViewer.#ensurePageViewVisible`. */
+  height: var(--viewer-container-height);
 }
 
+/*#if GENERIC*/
 .pdfViewer.removePageBorders .page {
   margin: 0 auto 10px;
   border: none;
 }
+/*#endif*/
 
+/*#if COMPONENTS*/
 .pdfViewer.singlePageView {
   display: inline-block;
 }
@@ -74,12 +93,12 @@
   margin: 0;
   border: none;
 }
+/*#endif*/
 
 .pdfViewer.scrollHorizontal,
 .pdfViewer.scrollWrapped,
 .spread {
-  margin-left: 3.5px;
-  margin-right: 3.5px;
+  margin-inline: 3.5px;
   text-align: center;
 }
 
@@ -88,11 +107,12 @@
   white-space: nowrap;
 }
 
+/*#if GENERIC*/
 .pdfViewer.removePageBorders,
+  /*#endif*/
 .pdfViewer.scrollHorizontal .spread,
 .pdfViewer.scrollWrapped .spread {
-  margin-left: 0;
-  margin-right: 0;
+  margin-inline: 0;
 }
 
 .spread .page,
@@ -108,37 +128,59 @@
 .spread .page,
 .pdfViewer.scrollHorizontal .page,
 .pdfViewer.scrollWrapped .page {
-  margin-left: var(--spreadHorizontalWrapped-margin-LR);
-  margin-right: var(--spreadHorizontalWrapped-margin-LR);
+  margin-inline: var(--spreadHorizontalWrapped-margin-LR);
 }
 
+/*#if GENERIC*/
 .pdfViewer.removePageBorders .spread .page,
 .pdfViewer.removePageBorders.scrollHorizontal .page,
 .pdfViewer.removePageBorders.scrollWrapped .page {
-  margin-left: 5px;
-  margin-right: 5px;
+  margin-inline: 5px;
 }
+/*#endif*/
 
 .pdfViewer .page canvas {
   margin: 0;
   display: block;
 }
 
+.pdfViewer .page canvas .structTree {
+  contain: strict;
+}
+
 .pdfViewer .page canvas[hidden] {
   display: none;
 }
 
-.pdfViewer .page .loadingIcon {
+.pdfViewer .page canvas[zooming] {
+  width: 100%;
+  height: 100%;
+}
+
+.pdfViewer .page.loadingIcon:after {
   position: absolute;
-  display: block;
-  left: 0;
   top: 0;
-  right: 0;
-  bottom: 0;
-  // background: url("images/loading-icon.gif") center no-repeat;
+  left: 0;
+  content: "";
+  width: 100%;
+  height: 100%;
+  // NOTE background: url("images/loading-icon.gif") center no-repeat;
+  display: none;
+  /* Using a delay with background-image doesn't work,
+     consequently we use the display. */
+  transition-property: display;
+  transition-delay: var(--loading-icon-delay);
+  z-index: 5;
+  contain: strict;
 }
-.pdfViewer .page .loadingIcon.notVisible {
-  background: none;
+
+.pdfViewer .page.loading:after {
+  display: block;
+}
+
+.pdfViewer .page:not(.loading):after {
+  transition-property: none;
+  display: none;
 }
 
 .pdfViewer.enablePermissions .textLayer span {

File diff suppressed because it is too large
+ 1 - 1
app/stage/protyle/js/pdf/pdf.js


File diff suppressed because it is too large
+ 1 - 1
app/stage/protyle/js/pdf/pdf.worker.js


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitDingbats.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitFixed.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitFixedBold.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitFixedBoldItalic.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitFixedItalic.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSans.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSansBold.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSansBoldItalic.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSansItalic.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSerif.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSerifBold.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSerifBoldItalic.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSerifItalic.pfb


BIN
app/stage/protyle/js/pdf/standard_fonts/FoxitSymbol.pfb


+ 27 - 0
app/stage/protyle/js/pdf/standard_fonts/LICENSE_FOXIT

@@ -0,0 +1,27 @@
+// Copyright 2014 PDFium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 102 - 0
app/stage/protyle/js/pdf/standard_fonts/LICENSE_LIBERATION

@@ -0,0 +1,102 @@
+Digitized data copyright (c) 2010 Google Corporation
+	with Reserved Font Arimo, Tinos and Cousine.
+Copyright (c) 2012 Red Hat, Inc.
+	with Reserved Font Name Liberation.
+
+This Font Software is licensed under the SIL Open Font License,
+Version 1.1.
+
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+
+PREAMBLE The goals of the Open Font License (OFL) are to stimulate
+worldwide development of collaborative font projects, to support the font
+creation efforts of academic and linguistic communities, and to provide
+a free and open framework in which fonts may be shared and improved in
+partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves.
+The fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works.  The fonts and derivatives,
+however, cannot be released under any other type of license.  The
+requirement for fonts to remain under this license does not apply to
+any document created using the fonts or their derivatives.
+
+ 
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such.
+This may include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components
+as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting ? in part or in whole ?
+any of the components of the Original Version, by changing formats or
+by porting the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical writer
+or other person who contributed to the Font Software.
+
+
+PERMISSION & CONDITIONS
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,in
+   Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+   redistributed and/or sold with any software, provided that each copy
+   contains the above copyright notice and this license. These can be
+   included either as stand-alone text files, human-readable headers or
+   in the appropriate machine-readable metadata fields within text or
+   binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+   Name(s) unless explicit written permission is granted by the
+   corresponding Copyright Holder. This restriction only applies to the
+   primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+   Software shall not be used to promote, endorse or advertise any
+   Modified Version, except to acknowledge the contribution(s) of the
+   Copyright Holder(s) and the Author(s) or with their explicit written
+   permission.
+
+5) The Font Software, modified or unmodified, in part or in whole, must
+   be distributed entirely under this license, and must not be distributed
+   under any other license. The requirement for fonts to remain under
+   this license does not apply to any document created using the Font
+   Software.
+
+
+ 
+TERMINATION
+This license becomes null and void if any of the above conditions are not met.
+
+ 
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT.  IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.
+

BIN
app/stage/protyle/js/pdf/standard_fonts/LiberationSans-Bold.ttf


BIN
app/stage/protyle/js/pdf/standard_fonts/LiberationSans-BoldItalic.ttf


BIN
app/stage/protyle/js/pdf/standard_fonts/LiberationSans-Italic.ttf


BIN
app/stage/protyle/js/pdf/standard_fonts/LiberationSans-Regular.ttf


Some files were not shown because too many files changed in this diff