first commit
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
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
This commit is contained in:
351
web/annotation_layer_builder.js
Normal file
351
web/annotation_layer_builder.js
Normal file
@@ -0,0 +1,351 @@
|
||||
/* 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 };
|
||||
Reference in New Issue
Block a user