Some checks failed
		
		
	
	Types tests / Test (lts/*) (push) Has been cancelled
				
			Lint / Lint (lts/*) (push) Has been cancelled
				
			CodeQL / Analyze (javascript) (push) Has been cancelled
				
			CI / Test (20) (push) Has been cancelled
				
			CI / Test (22) (push) Has been cancelled
				
			CI / Test (24) (push) Has been cancelled
				
			
		
			
				
	
	
		
			352 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* Copyright 2014 Mozilla Foundation
 | 
						|
 *
 | 
						|
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
 * you may not use this file except in compliance with the License.
 | 
						|
 * You may obtain a copy of the License at
 | 
						|
 *
 | 
						|
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 *
 | 
						|
 * Unless required by applicable law or agreed to in writing, software
 | 
						|
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
 * See the License for the specific language governing permissions and
 | 
						|
 * limitations under the License.
 | 
						|
 */
 | 
						|
 | 
						|
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
 | 
						|
// eslint-disable-next-line max-len
 | 
						|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
 | 
						|
// eslint-disable-next-line max-len
 | 
						|
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
 | 
						|
/** @typedef {import("./interfaces").IDownloadManager} IDownloadManager */
 | 
						|
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
 | 
						|
// eslint-disable-next-line max-len
 | 
						|
/** @typedef {import("./struct_tree_layer_builder.js").StructTreeLayerBuilder} StructTreeLayerBuilder */
 | 
						|
// eslint-disable-next-line max-len
 | 
						|
/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
 | 
						|
// eslint-disable-next-line max-len
 | 
						|
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
 | 
						|
/** @typedef {import("./comment_manager.js").CommentManager} CommentManager */
 | 
						|
 | 
						|
import {
 | 
						|
  AnnotationLayer,
 | 
						|
  AnnotationType,
 | 
						|
  setLayerDimensions,
 | 
						|
  Util,
 | 
						|
} from "pdfjs-lib";
 | 
						|
import { PresentationModeState } from "./ui_utils.js";
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {Object} AnnotationLayerBuilderOptions
 | 
						|
 * @property {PDFPageProxy} pdfPage
 | 
						|
 * @property {AnnotationStorage} [annotationStorage]
 | 
						|
 * @property {string} [imageResourcesPath] - Path for image resources, mainly
 | 
						|
 *   for annotation icons. Include trailing slash.
 | 
						|
 * @property {boolean} renderForms
 | 
						|
 * @property {IPDFLinkService} linkService
 | 
						|
 * @property {IDownloadManager} [downloadManager]
 | 
						|
 * @property {boolean} [enableComment]
 | 
						|
 * @property {boolean} [enableScripting]
 | 
						|
 * @property {Promise<boolean>} [hasJSActionsPromise]
 | 
						|
 * @property {Promise<Object<string, Array<Object>> | null>}
 | 
						|
 *   [fieldObjectsPromise]
 | 
						|
 * @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap]
 | 
						|
 * @property {TextAccessibilityManager} [accessibilityManager]
 | 
						|
 * @property {AnnotationEditorUIManager} [annotationEditorUIManager]
 | 
						|
 * @property {function} [onAppend]
 | 
						|
 * @property {CommentManager} [commentManager]
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {Object} AnnotationLayerBuilderRenderOptions
 | 
						|
 * @property {PageViewport} viewport
 | 
						|
 * @property {string} [intent] - The default value is "display".
 | 
						|
 * @property {StructTreeLayerBuilder} [structTreeLayer]
 | 
						|
 */
 | 
						|
 | 
						|
class AnnotationLayerBuilder {
 | 
						|
  #annotations = null;
 | 
						|
 | 
						|
  #commentManager = null;
 | 
						|
 | 
						|
  #externalHide = false;
 | 
						|
 | 
						|
  #onAppend = null;
 | 
						|
 | 
						|
  #eventAbortController = null;
 | 
						|
 | 
						|
  #linksInjected = false;
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {AnnotationLayerBuilderOptions} options
 | 
						|
   */
 | 
						|
  constructor({
 | 
						|
    pdfPage,
 | 
						|
    linkService,
 | 
						|
    downloadManager,
 | 
						|
    annotationStorage = null,
 | 
						|
    imageResourcesPath = "",
 | 
						|
    renderForms = true,
 | 
						|
    enableComment = false,
 | 
						|
    commentManager = null,
 | 
						|
    enableScripting = false,
 | 
						|
    hasJSActionsPromise = null,
 | 
						|
    fieldObjectsPromise = null,
 | 
						|
    annotationCanvasMap = null,
 | 
						|
    accessibilityManager = null,
 | 
						|
    annotationEditorUIManager = null,
 | 
						|
    onAppend = null,
 | 
						|
  }) {
 | 
						|
    this.pdfPage = pdfPage;
 | 
						|
    this.linkService = linkService;
 | 
						|
    this.downloadManager = downloadManager;
 | 
						|
    this.imageResourcesPath = imageResourcesPath;
 | 
						|
    this.renderForms = renderForms;
 | 
						|
    this.annotationStorage = annotationStorage;
 | 
						|
    this.enableComment = enableComment;
 | 
						|
    this.#commentManager = commentManager;
 | 
						|
    this.enableScripting = enableScripting;
 | 
						|
    this._hasJSActionsPromise = hasJSActionsPromise || Promise.resolve(false);
 | 
						|
    this._fieldObjectsPromise = fieldObjectsPromise || Promise.resolve(null);
 | 
						|
    this._annotationCanvasMap = annotationCanvasMap;
 | 
						|
    this._accessibilityManager = accessibilityManager;
 | 
						|
    this._annotationEditorUIManager = annotationEditorUIManager;
 | 
						|
    this.#onAppend = onAppend;
 | 
						|
 | 
						|
    this.annotationLayer = null;
 | 
						|
    this.div = null;
 | 
						|
    this._cancelled = false;
 | 
						|
    this._eventBus = linkService.eventBus;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {AnnotationLayerBuilderRenderOptions} options
 | 
						|
   * @returns {Promise<void>} A promise that is resolved when rendering of the
 | 
						|
   *   annotations is complete.
 | 
						|
   */
 | 
						|
  async render({ viewport, intent = "display", structTreeLayer = null }) {
 | 
						|
    if (this.div) {
 | 
						|
      if (this._cancelled || !this.annotationLayer) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // If an annotationLayer already exists, refresh its children's
 | 
						|
      // transformation matrices.
 | 
						|
      this.annotationLayer.update({
 | 
						|
        viewport: viewport.clone({ dontFlip: true }),
 | 
						|
      });
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const [annotations, hasJSActions, fieldObjects] = await Promise.all([
 | 
						|
      this.pdfPage.getAnnotations({ intent }),
 | 
						|
      this._hasJSActionsPromise,
 | 
						|
      this._fieldObjectsPromise,
 | 
						|
    ]);
 | 
						|
    if (this._cancelled) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Create an annotation layer div and render the annotations
 | 
						|
    // if there is at least one annotation.
 | 
						|
    const div = (this.div = document.createElement("div"));
 | 
						|
    div.className = "annotationLayer";
 | 
						|
    this.#onAppend?.(div);
 | 
						|
    this.#initAnnotationLayer(viewport, structTreeLayer);
 | 
						|
 | 
						|
    if (annotations.length === 0) {
 | 
						|
      this.#annotations = annotations;
 | 
						|
      setLayerDimensions(this.div, viewport);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    await this.annotationLayer.render({
 | 
						|
      annotations,
 | 
						|
      imageResourcesPath: this.imageResourcesPath,
 | 
						|
      renderForms: this.renderForms,
 | 
						|
      downloadManager: this.downloadManager,
 | 
						|
      enableComment: this.enableComment,
 | 
						|
      enableScripting: this.enableScripting,
 | 
						|
      hasJSActions,
 | 
						|
      fieldObjects,
 | 
						|
    });
 | 
						|
 | 
						|
    this.#annotations = annotations;
 | 
						|
 | 
						|
    // 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.#eventAbortController) {
 | 
						|
      this.#eventAbortController = new AbortController();
 | 
						|
 | 
						|
      this._eventBus?._on(
 | 
						|
        "presentationmodechanged",
 | 
						|
        evt => {
 | 
						|
          this.#updatePresentationModeState(evt.state);
 | 
						|
        },
 | 
						|
        { signal: this.#eventAbortController.signal }
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #initAnnotationLayer(viewport, structTreeLayer) {
 | 
						|
    this.annotationLayer = new AnnotationLayer({
 | 
						|
      div: this.div,
 | 
						|
      accessibilityManager: this._accessibilityManager,
 | 
						|
      annotationCanvasMap: this._annotationCanvasMap,
 | 
						|
      annotationEditorUIManager: this._annotationEditorUIManager,
 | 
						|
      annotationStorage: this.annotationStorage,
 | 
						|
      page: this.pdfPage,
 | 
						|
      viewport: viewport.clone({ dontFlip: true }),
 | 
						|
      structTreeLayer,
 | 
						|
      commentManager: this.#commentManager,
 | 
						|
      linkService: this.linkService,
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  cancel() {
 | 
						|
    this._cancelled = true;
 | 
						|
 | 
						|
    this.#eventAbortController?.abort();
 | 
						|
    this.#eventAbortController = null;
 | 
						|
  }
 | 
						|
 | 
						|
  hide(internal = false) {
 | 
						|
    this.#externalHide = !internal;
 | 
						|
    if (!this.div) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.div.hidden = true;
 | 
						|
  }
 | 
						|
 | 
						|
  hasEditableAnnotations() {
 | 
						|
    return !!this.annotationLayer?.hasEditableAnnotations();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {Array<Object>} inferredLinks
 | 
						|
   * @returns {Promise<void>} A promise that is resolved when the inferred links
 | 
						|
   *   are added to the annotation layer.
 | 
						|
   */
 | 
						|
  async injectLinkAnnotations(inferredLinks) {
 | 
						|
    if (this.#annotations === null) {
 | 
						|
      throw new Error(
 | 
						|
        "`render` method must be called before `injectLinkAnnotations`."
 | 
						|
      );
 | 
						|
    }
 | 
						|
    if (this._cancelled || this.#linksInjected) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.#linksInjected = true;
 | 
						|
 | 
						|
    const newLinks = this.#annotations.length
 | 
						|
      ? this.#checkInferredLinks(inferredLinks)
 | 
						|
      : inferredLinks;
 | 
						|
 | 
						|
    if (!newLinks.length) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    await this.annotationLayer.addLinkAnnotations(newLinks);
 | 
						|
    // Don't show the annotation layer if it was explicitly hidden previously.
 | 
						|
    if (!this.#externalHide) {
 | 
						|
      this.div.hidden = false;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #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;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #checkInferredLinks(inferredLinks) {
 | 
						|
    function annotationRects(annot) {
 | 
						|
      if (!annot.quadPoints) {
 | 
						|
        return [annot.rect];
 | 
						|
      }
 | 
						|
      const rects = [];
 | 
						|
      for (let i = 2, ii = annot.quadPoints.length; i < ii; i += 8) {
 | 
						|
        const trX = annot.quadPoints[i];
 | 
						|
        const trY = annot.quadPoints[i + 1];
 | 
						|
        const blX = annot.quadPoints[i + 2];
 | 
						|
        const blY = annot.quadPoints[i + 3];
 | 
						|
        rects.push([blX, blY, trX, trY]);
 | 
						|
      }
 | 
						|
      return rects;
 | 
						|
    }
 | 
						|
 | 
						|
    function intersectAnnotations(annot1, annot2) {
 | 
						|
      const intersections = [];
 | 
						|
      const annot1Rects = annotationRects(annot1);
 | 
						|
      const annot2Rects = annotationRects(annot2);
 | 
						|
      for (const rect1 of annot1Rects) {
 | 
						|
        for (const rect2 of annot2Rects) {
 | 
						|
          const intersection = Util.intersect(rect1, rect2);
 | 
						|
          if (intersection) {
 | 
						|
            intersections.push(intersection);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
      return intersections;
 | 
						|
    }
 | 
						|
 | 
						|
    function areaRects(rects) {
 | 
						|
      let totalArea = 0;
 | 
						|
      for (const rect of rects) {
 | 
						|
        totalArea += Math.abs((rect[2] - rect[0]) * (rect[3] - rect[1]));
 | 
						|
      }
 | 
						|
      return totalArea;
 | 
						|
    }
 | 
						|
 | 
						|
    return inferredLinks.filter(link => {
 | 
						|
      let linkAreaRects;
 | 
						|
 | 
						|
      for (const annotation of this.#annotations) {
 | 
						|
        if (
 | 
						|
          annotation.annotationType !== AnnotationType.LINK ||
 | 
						|
          !annotation.url
 | 
						|
        ) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
        // TODO: Add a test case to verify that we can find the intersection
 | 
						|
        //       between two annotations with quadPoints properly.
 | 
						|
        const intersections = intersectAnnotations(annotation, link);
 | 
						|
 | 
						|
        if (intersections.length === 0) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
        linkAreaRects ??= areaRects(annotationRects(link));
 | 
						|
 | 
						|
        if (
 | 
						|
          areaRects(intersections) / linkAreaRects >
 | 
						|
          0.5 /* If the overlap is more than 50%. */
 | 
						|
        ) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      return true;
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export { AnnotationLayerBuilder };
 |