1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

[edition] Add a FreeText editor (#14970)

- add a basic UI to edit some text in a pdf;
- an editor can be moved, suppressed, cut, copied, pasted, selected;
- add an undo/redo manager.
This commit is contained in:
Calixte Denizet 2022-06-01 10:38:08 +02:00
parent 1ac33c960d
commit be1aa11986
28 changed files with 2321 additions and 18 deletions

View file

@ -0,0 +1,85 @@
/* Copyright 2022 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:root {
--focus-outline: solid 2px red;
--hover-outline: dashed 2px blue;
}
.annotationEditorLayer {
background: transparent;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.annotationEditorLayer .freeTextEditor {
position: absolute;
background: transparent;
border-radius: 3px;
padding: 5px;
resize: none;
width: auto;
height: auto;
}
.annotationEditorLayer .freeTextEditor .internal {
background: transparent;
border: none;
top: 0;
left: 0;
min-height: 15px;
overflow: visible;
white-space: nowrap;
resize: none;
}
.annotationEditorLayer .freeTextEditor .overlay {
position: absolute;
display: none;
background: transparent;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.annotationEditorLayer .freeTextEditor .overlay.enabled {
display: block;
}
.annotationEditorLayer .freeTextEditor .internal:empty::before {
content: attr(default-content);
color: gray;
}
.annotationEditorLayer .freeTextEditor .internal:focus {
outline: none;
}
.annotationEditorLayer .freeTextEditor:focus-within {
outline: var(--focus-outline);
}
.annotationEditorLayer .freeTextEditor:hover:not(:focus-within) {
outline: var(--hover-outline);
}
.annotationEditorLayer .selectedEditor {
outline: var(--focus-outline);
resize: none;
}

View file

@ -0,0 +1,128 @@
/* Copyright 2022 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @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 */
/** @typedef {import("./interfaces").IL10n} IL10n */
import { AnnotationEditorLayer } from "pdfjs-lib";
import { NullL10n } from "./l10n_utils.js";
/**
* @typedef {Object} AnnotationEditorLayerBuilderOptions
* @property {number} mode - Editor mode
* @property {HTMLDivElement} pageDiv
* @property {PDFPageProxy} pdfPage
* @property {AnnotationStorage} annotationStorage
* @property {IL10n} l10n - Localization service.
* @property {AnnotationEditorUIManager} uiManager
*/
class AnnotationEditorLayerBuilder {
#uiManager;
/**
* @param {AnnotationEditorLayerBuilderOptions} options
*/
constructor(options) {
this.pageDiv = options.pageDiv;
this.pdfPage = options.pdfPage;
this.annotationStorage = options.annotationStorage || null;
this.l10n = options.l10n || NullL10n;
this.annotationEditorLayer = null;
this.div = null;
this._cancelled = false;
this.#uiManager = options.uiManager;
}
/**
* @param {PageViewport} viewport
* @param {string} intent (default value is 'display')
*/
async render(viewport, intent = "display") {
if (intent !== "display") {
return;
}
if (this._cancelled) {
return;
}
if (this.div) {
this.annotationEditorLayer.update({ viewport: viewport.clone() });
this.show();
return;
}
// Create an AnnotationEditor layer div
this.div = document.createElement("div");
this.div.className = "annotationEditorLayer";
this.div.tabIndex = 0;
this.annotationEditorLayer = new AnnotationEditorLayer({
uiManager: this.#uiManager,
div: this.div,
annotationStorage: this.annotationStorage,
pageIndex: this.pdfPage._pageIndex,
l10n: this.l10n,
});
const parameters = {
viewport: viewport.clone(),
div: this.div,
annotations: null,
intent,
};
this.annotationEditorLayer.render(parameters);
this.pageDiv.appendChild(this.div);
}
cancel() {
this._cancelled = true;
}
hide() {
if (!this.div) {
return;
}
this.div.hidden = true;
}
show() {
if (!this.div) {
return;
}
this.div.hidden = false;
}
destroy() {
if (!this.div) {
return;
}
this.pageDiv = null;
this.div.remove();
this.annotationEditorLayer.destroy();
}
}
export { AnnotationEditorLayerBuilder };

View file

@ -525,6 +525,7 @@ const PDFViewerApplication = {
l10n: this.l10n,
textLayerMode: AppOptions.get("textLayerMode"),
annotationMode: AppOptions.get("annotationMode"),
annotationEditorEnabled: AppOptions.get("annotationEditorEnabled"),
imageResourcesPath: AppOptions.get("imageResourcesPath"),
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
@ -560,6 +561,10 @@ const PDFViewerApplication = {
this.findBar = new PDFFindBar(appConfig.findBar, eventBus, this.l10n);
}
if (AppOptions.get("annotationEditorEnabled")) {
document.getElementById("editorModeButtons").classList.remove("hidden");
}
this.pdfDocumentProperties = new PDFDocumentProperties(
appConfig.documentProperties,
this.overlayManager,
@ -1878,6 +1883,10 @@ const PDFViewerApplication = {
eventBus._on("namedaction", webViewerNamedAction);
eventBus._on("presentationmodechanged", webViewerPresentationModeChanged);
eventBus._on("presentationmode", webViewerPresentationMode);
eventBus._on(
"switchannotationeditormode",
webViewerSwitchAnnotationEditorMode
);
eventBus._on("print", webViewerPrint);
eventBus._on("download", webViewerDownload);
eventBus._on("firstpage", webViewerFirstPage);
@ -2459,6 +2468,13 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
function webViewerPresentationMode() {
PDFViewerApplication.requestPresentationMode();
}
function webViewerSwitchAnnotationEditorMode(evt) {
if (evt.toggle) {
PDFViewerApplication.pdfViewer.annotionEditorEnabled = true;
} else {
PDFViewerApplication.pdfViewer.annotationEditorMode = evt.mode;
}
}
function webViewerPrint() {
PDFViewerApplication.triggerPrinting();
}

View file

@ -58,6 +58,11 @@ const defaultOptions = {
value: 2,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
annotationEditorEnabled: {
/** @type {boolean} */
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
cursorToolOnLoad: {
/** @type {number} */
value: 0,

View file

@ -22,6 +22,8 @@
/** @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 */
@ -30,6 +32,8 @@
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
import {
AnnotationEditorType,
AnnotationEditorUIManager,
AnnotationMode,
createPromiseCapability,
PermissionFlag,
@ -61,6 +65,7 @@ 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";
@ -104,6 +109,8 @@ const PagesCountLimit = {
* being rendered. The constants from {@link AnnotationMode} should be used;
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
* The default value is `AnnotationMode.ENABLE_FORMS`.
* @property {boolean} [annotationEditorEnabled] - Enables the creation and
* editing of new Annotations.
* @property {string} [imageResourcesPath] - Path for image resources, mainly
* mainly for annotation icons. Include trailing slash.
* @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
@ -194,6 +201,7 @@ class PDFPageViewBuffer {
* Simple viewer control to display PDF content/pages.
*
* @implements {IPDFAnnotationLayerFactory}
* @implements {IPDFAnnotationEditorLayerFactory}
* @implements {IPDFStructTreeLayerFactory}
* @implements {IPDFTextLayerFactory}
* @implements {IPDFXfaLayerFactory}
@ -201,6 +209,10 @@ class PDFPageViewBuffer {
class BaseViewer {
#buffer = null;
#annotationEditorMode = AnnotationEditorType.NONE;
#annotationEditorUIManager = null;
#annotationMode = AnnotationMode.ENABLE_FORMS;
#previousAnnotationMode = null;
@ -268,6 +280,10 @@ class BaseViewer {
this.#enablePermissions = options.enablePermissions || false;
this.pageColors = options.pageColors || null;
if (options.annotationEditorEnabled === true) {
this.#annotationEditorUIManager = new AnnotationEditorUIManager();
}
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
if (
this.pageColors &&
@ -699,6 +715,9 @@ class BaseViewer {
const annotationLayerFactory =
this.#annotationMode !== AnnotationMode.DISABLE ? this : null;
const xfaLayerFactory = isPureXfa ? this : null;
const annotationEditorLayerFactory = this.#annotationEditorUIManager
? this
: null;
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
const pageView = new PDFPageView({
@ -714,6 +733,7 @@ class BaseViewer {
annotationLayerFactory,
annotationMode: this.#annotationMode,
xfaLayerFactory,
annotationEditorLayerFactory,
textHighlighterFactory: this,
structTreeLayerFactory: this,
imageResourcesPath: this.imageResourcesPath,
@ -1656,6 +1676,30 @@ class BaseViewer {
});
}
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPageProxy} pdfPage
* @param {IL10n} l10n
* @param {AnnotationStorage} [annotationStorage] - Storage for annotation
* data in forms.
* @returns {AnnotationEditorLayerBuilder}
*/
createAnnotationEditorLayerBuilder(
pageDiv,
pdfPage,
l10n,
annotationStorage = null
) {
return new AnnotationEditorLayerBuilder({
uiManager: this.#annotationEditorUIManager,
pageDiv,
pdfPage,
annotationStorage:
annotationStorage || this.pdfDocument?.annotationStorage,
l10n,
});
}
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPageProxy} pdfPage
@ -2072,6 +2116,36 @@ class BaseViewer {
docStyle.setProperty("--viewer-container-height", `${height}px`);
}
}
get annotationEditorMode() {
return this.#annotationEditorMode;
}
/**
* @param {number} mode - Annotation Editor mode (None, FreeText, Ink, ...)
*/
set annotationEditorMode(mode) {
if (!this.#annotationEditorUIManager) {
throw new Error(`The AnnotationEditor is not enabled.`);
}
if (this.#annotationEditorMode === mode) {
return;
}
if (!Object.values(AnnotationEditorType).includes(mode)) {
throw new Error(`Invalid AnnotationEditor mode: ${mode}`);
}
// If the mode is the same as before, it means that this mode is disabled
// and consequently the mode is NONE.
this.#annotationEditorMode = mode;
this.eventBus.dispatch("annotationeditormodechanged", {
source: this,
mode,
});
this.#annotationEditorUIManager.updateMode(mode);
}
}
export { BaseViewer, PagesCountLimit, PDFPageViewBuffer };

View file

@ -21,6 +21,8 @@
/** @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 */
@ -29,6 +31,7 @@
/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
import { NullL10n } from "./l10n_utils.js";
import { SimpleLinkService } from "./pdf_link_service.js";
@ -87,6 +90,32 @@ class DefaultAnnotationLayerFactory {
}
}
/**
* @implements IPDFAnnotationEditorLayerFactory
*/
class DefaultAnnotationEditorLayerFactory {
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPageProxy} pdfPage
* @param {IL10n} l10n
* @param {AnnotationStorage} [annotationStorage]
* @returns {AnnotationEditorLayerBuilder}
*/
createAnnotationEditorLayerBuilder(
pageDiv,
pdfPage,
l10n,
annotationStorage = null
) {
return new AnnotationEditorLayerBuilder({
pageDiv,
pdfPage,
l10n,
annotationStorage,
});
}
}
/**
* @implements IPDFStructTreeLayerFactory
*/
@ -161,6 +190,7 @@ class DefaultXfaLayerFactory {
}
export {
DefaultAnnotationEditorLayerFactory,
DefaultAnnotationLayerFactory,
DefaultStructTreeLayerFactory,
DefaultTextLayerFactory,

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- copied from https://www.svgrepo.com/svg/255881/text -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<g>
<g transform="scale(0.03125)">
<path d="M405.787,43.574H8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83c0,4.512,3.657,8.17,8.17,8.17h32.681
c4.513,0,8.17-3.658,8.17-8.17v-24.511h95.319v119.83c0,4.512,3.657,8.17,8.17,8.17c4.513,0,8.17-3.658,8.17-8.17v-128
c0-4.512-3.657-8.17-8.17-8.17H40.851c-4.513,0-8.17,3.658-8.17,8.17v24.511H16.34V59.915h381.277v103.489h-16.34v-24.511
c0-4.512-3.657-8.17-8.17-8.17h-111.66c-4.513,0-8.17,3.658-8.17,8.17v288.681c0,4.512,3.657,8.17,8.17,8.17h57.191v16.34H95.319
v-16.34h57.191c4.513,0,8.17-3.658,8.17-8.17v-128c0-4.512-3.657-8.17-8.17-8.17c-4.513,0-8.17,3.658-8.17,8.17v119.83H87.149
c-4.513,0-8.17,3.658-8.17,8.17v32.681c0,4.512,3.657,8.17,8.17,8.17h239.66c4.513,0,8.17-3.658,8.17-8.17v-32.681
c0-4.512-3.657-8.17-8.17-8.17h-57.192v-272.34h95.319v24.511c0,4.512,3.657,8.17,8.17,8.17h32.681c4.513,0,8.17-3.658,8.17-8.17
V51.745C413.957,47.233,410.3,43.574,405.787,43.574z"/>
</g>
</g>
<g>
<g transform="scale(0.03125)">
<path d="M503.83,452.085h-24.511V59.915h24.511c4.513,0,8.17-3.658,8.17-8.17s-3.657-8.17-8.17-8.17h-65.362
c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17h24.511v392.17h-24.511c-4.513,0-8.17,3.658-8.17,8.17s3.657,8.17,8.17,8.17
h65.362c4.513,0,8.17-3.658,8.17-8.17S508.343,452.085,503.83,452.085z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,4 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>

After

Width:  |  Height:  |  Size: 478 B

View file

@ -19,6 +19,8 @@
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
// eslint-disable-next-line max-len
/** @typedef {import("./annotation_layer_builder").AnnotationLayerBuilder} AnnotationLayerBuilder */
// eslint-disable-next-line max-len
/** @typedef {import("./annotation_editor_layer_builder").AnnotationEditorLayerBuilder} AnnotationEditorLayerBuilder */
/** @typedef {import("./event_utils").EventBus} EventBus */
// eslint-disable-next-line max-len
/** @typedef {import("./struct_tree_builder").StructTreeLayerBuilder} StructTreeLayerBuilder */
@ -208,6 +210,26 @@ class IPDFAnnotationLayerFactory {
) {}
}
/**
* @interface
*/
class IPDFAnnotationEditorLayerFactory {
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPageProxy} pdfPage
* @param {IL10n} l10n
* @param {AnnotationStorage} [annotationStorage] - Storage for annotation
* data in forms.
* @returns {AnnotationEditorLayerBuilder}
*/
createAnnotationEditorLayerBuilder(
pageDiv,
pdfPage,
l10n = undefined,
annotationStorage = null
) {}
}
/**
* @interface
*/
@ -307,6 +329,7 @@ class IL10n {
export {
IDownloadManager,
IL10n,
IPDFAnnotationEditorLayerFactory,
IPDFAnnotationLayerFactory,
IPDFLinkService,
IPDFStructTreeLayerFactory,

View file

@ -81,6 +81,7 @@ const DEFAULT_L10N_STRINGS = {
printing_not_ready: "Warning: The PDF is not fully loaded for printing.",
web_fonts_disabled:
"Web fonts are disabled: unable to use embedded PDF fonts.",
freetext_default_content: "Enter some text…",
};
function getL10nFallback(key, args) {

View file

@ -22,6 +22,8 @@
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */
// eslint-disable-next-line max-len
/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */
@ -72,6 +74,7 @@ import { NullL10n } from "./l10n_utils.js";
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
* The default value is `AnnotationMode.ENABLE_FORMS`.
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory
* @property {IPDFAnnotationEditorLayerFactory} annotationEditorLayerFactory
* @property {IPDFXfaLayerFactory} xfaLayerFactory
* @property {IPDFStructTreeLayerFactory} structTreeLayerFactory
* @property {Object} [textHighlighterFactory]
@ -128,6 +131,7 @@ class PDFPageView {
this.renderingQueue = options.renderingQueue;
this.textLayerFactory = options.textLayerFactory;
this.annotationLayerFactory = options.annotationLayerFactory;
this.annotationEditorLayerFactory = options.annotationEditorLayerFactory;
this.xfaLayerFactory = options.xfaLayerFactory;
this.textHighlighter =
options.textHighlighterFactory?.createTextHighlighter(
@ -148,6 +152,7 @@ class PDFPageView {
this._annotationCanvasMap = null;
this.annotationLayer = null;
this.annotationEditorLayer = null;
this.textLayer = null;
this.zoomLayer = null;
this.xfaLayer = null;
@ -204,6 +209,24 @@ class PDFPageView {
}
}
/**
* @private
*/
async _renderAnnotationEditorLayer() {
let error = null;
try {
await this.annotationEditorLayer.render(this.viewport, "display");
} catch (ex) {
error = ex;
} finally {
this.eventBus.dispatch("annotationeditorlayerrendered", {
source: this,
pageNumber: this.id,
error,
});
}
}
/**
* @private
*/
@ -259,9 +282,14 @@ class PDFPageView {
reset({
keepZoomLayer = false,
keepAnnotationLayer = false,
keepAnnotationEditorLayer = false,
keepXfaLayer = false,
} = {}) {
this.cancelRendering({ keepAnnotationLayer, keepXfaLayer });
this.cancelRendering({
keepAnnotationLayer,
keepAnnotationEditorLayer,
keepXfaLayer,
});
this.renderingState = RenderingStates.INITIAL;
const div = this.div;
@ -272,12 +300,15 @@ class PDFPageView {
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
annotationLayerNode =
(keepAnnotationLayer && this.annotationLayer?.div) || null,
annotationEditorLayerNode =
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null;
for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i];
switch (node) {
case zoomLayerNode:
case annotationLayerNode:
case annotationEditorLayerNode:
case xfaLayerNode:
continue;
}
@ -290,6 +321,12 @@ class PDFPageView {
// so they are not displayed on the already resized page.
this.annotationLayer.hide();
}
if (annotationEditorLayerNode) {
this.annotationEditorLayer.hide();
} else {
this.annotationEditorLayer?.destroy();
}
if (xfaLayerNode) {
// Hide the XFA layer until all elements are resized
// so they are not displayed on the already resized page.
@ -347,6 +384,7 @@ class PDFPageView {
this.cssTransform({
target: this.svg,
redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true,
redrawXfaLayer: true,
});
@ -380,6 +418,7 @@ class PDFPageView {
this.cssTransform({
target: this.canvas,
redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true,
redrawXfaLayer: true,
});
@ -403,6 +442,7 @@ class PDFPageView {
this.reset({
keepZoomLayer: true,
keepAnnotationLayer: true,
keepAnnotationEditorLayer: true,
keepXfaLayer: true,
});
}
@ -411,7 +451,11 @@ class PDFPageView {
* PLEASE NOTE: Most likely you want to use the `this.reset()` method,
* rather than calling this one directly.
*/
cancelRendering({ keepAnnotationLayer = false, keepXfaLayer = false } = {}) {
cancelRendering({
keepAnnotationLayer = false,
keepAnnotationEditorLayer = false,
keepXfaLayer = false,
} = {}) {
if (this.paintTask) {
this.paintTask.cancel();
this.paintTask = null;
@ -430,6 +474,13 @@ class PDFPageView {
this.annotationLayer = null;
this._annotationCanvasMap = null;
}
if (
this.annotationEditorLayer &&
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
) {
this.annotationEditorLayer.cancel();
this.annotationEditorLayer = null;
}
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
this.xfaLayer.cancel();
this.xfaLayer = null;
@ -444,6 +495,7 @@ class PDFPageView {
cssTransform({
target,
redrawAnnotationLayer = false,
redrawAnnotationEditorLayer = false,
redrawXfaLayer = false,
}) {
// Scale target (canvas or svg), its wrapper and page container.
@ -517,6 +569,9 @@ class PDFPageView {
if (redrawAnnotationLayer && this.annotationLayer) {
this._renderAnnotationLayer();
}
if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
this._renderAnnotationEditorLayer();
}
if (redrawXfaLayer && this.xfaLayer) {
this._renderXfaLayer();
}
@ -567,9 +622,12 @@ class PDFPageView {
canvasWrapper.style.height = div.style.height;
canvasWrapper.classList.add("canvasWrapper");
if (this.annotationLayer?.div) {
const lastDivBeforeTextDiv =
this.annotationLayer?.div || this.annotationEditorLayer?.div;
if (lastDivBeforeTextDiv) {
// The annotation layer needs to stay on top.
div.insertBefore(canvasWrapper, this.annotationLayer.div);
div.insertBefore(canvasWrapper, lastDivBeforeTextDiv);
} else {
div.appendChild(canvasWrapper);
}
@ -580,9 +638,9 @@ class PDFPageView {
textLayerDiv.className = "textLayer";
textLayerDiv.style.width = canvasWrapper.style.width;
textLayerDiv.style.height = canvasWrapper.style.height;
if (this.annotationLayer?.div) {
if (lastDivBeforeTextDiv) {
// The annotation layer needs to stay on top.
div.insertBefore(textLayerDiv, this.annotationLayer.div);
div.insertBefore(textLayerDiv, lastDivBeforeTextDiv);
} else {
div.appendChild(textLayerDiv);
}
@ -693,7 +751,18 @@ class PDFPageView {
}
if (this.annotationLayer) {
this._renderAnnotationLayer();
this._renderAnnotationLayer().then(() => {
if (this.annotationEditorLayerFactory) {
this.annotationEditorLayer ||=
this.annotationEditorLayerFactory.createAnnotationEditorLayerBuilder(
div,
pdfPage,
this.l10n,
/* annotationStorage = */ null
);
this._renderAnnotationEditorLayer();
}
});
}
});
},

View file

@ -15,6 +15,7 @@
@import url(text_layer_builder.css);
@import url(annotation_layer_builder.css);
@import url(xfa_layer_builder.css);
@import url(annotation_editor_layer_builder.css);
:root {
--viewer-container-height: 0;

View file

@ -22,6 +22,7 @@ import {
MIN_SCALE,
noContextMenuHandler,
} from "./ui_utils.js";
import { AnnotationEditorType } from "pdfjs-lib";
const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
@ -43,6 +44,9 @@ const PAGE_NUMBER_LOADING_INDICATOR = "visiblePageIsLoading";
* @property {HTMLButtonElement} openFile - Button to open a new document.
* @property {HTMLButtonElement} presentationModeButton - Button to switch to
* presentation mode.
* @property {HTMLButtonElement} editorNoneButton - Button to disable editing.
* @property {HTMLButtonElement} editorFreeTextButton - Button to switch to Free
* Text edition.
* @property {HTMLButtonElement} download - Button to download the document.
* @property {HTMLAnchorElement} viewBookmark - Button to obtain a bookmark link
* to the current location in the document.
@ -70,6 +74,16 @@ class Toolbar {
},
{ element: options.download, eventName: "download" },
{ element: options.viewBookmark, eventName: null },
{
element: options.editorNoneButton,
eventName: "switchannotationeditormode",
eventDetails: { mode: AnnotationEditorType.NONE },
},
{
element: options.editorFreeTextButton,
eventName: "switchannotationeditormode",
eventDetails: { mode: AnnotationEditorType.FREETEXT },
},
];
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
this.buttons.push({ element: options.openFile, eventName: "openfile" });
@ -89,7 +103,7 @@ class Toolbar {
this.reset();
// Bind the event listeners for click and various other actions.
this._bindListeners();
this._bindListeners(options);
}
setPageNumber(pageNumber, pageLabel) {
@ -121,15 +135,21 @@ class Toolbar {
this.updateLoadingIndicatorState();
}
_bindListeners() {
_bindListeners(options) {
const { pageNumber, scaleSelect } = this.items;
const self = this;
// The buttons within the toolbar.
for (const { element, eventName } of this.buttons) {
for (const { element, eventName, eventDetails } of this.buttons) {
element.addEventListener("click", evt => {
if (eventName !== null) {
this.eventBus.dispatch(eventName, { source: this });
const details = { source: this };
if (eventDetails) {
for (const property in eventDetails) {
details[property] = eventDetails[property];
}
}
this.eventBus.dispatch(eventName, details);
}
});
}
@ -174,6 +194,23 @@ class Toolbar {
this.#adjustScaleWidth();
this._updateUIState(true);
});
this.#bindEditorToolsListener(options);
}
#bindEditorToolsListener({ editorNoneButton, editorFreeTextButton }) {
this.eventBus._on("annotationeditormodechanged", evt => {
const editorButtons = [
[AnnotationEditorType.NONE, editorNoneButton],
[AnnotationEditorType.FREETEXT, editorFreeTextButton],
];
for (const [mode, button] of editorButtons) {
const checked = mode === evt.mode;
button.classList.toggle("toggled", checked);
button.setAttribute("aria-checked", checked);
}
});
}
_updateUIState(resetNumPages = false) {

View file

@ -71,6 +71,8 @@
--loading-icon: url(images/loading.svg);
--treeitem-expanded-icon: url(images/treeitem-expanded.svg);
--treeitem-collapsed-icon: url(images/treeitem-collapsed.svg);
--toolbarButton-editorFreeText-icon: url(images/toolbarButton-editorFreeText.svg);
--toolbarButton-editorNone-icon: url(images/toolbarButton-editorNone.svg);
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
--toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
@ -824,6 +826,14 @@ select {
mask-image: var(--toolbarButton-presentationMode-icon);
}
#editorNone::before {
mask-image: var(--toolbarButton-editorNone-icon);
}
#editorFreeText::before {
mask-image: var(--toolbarButton-editorFreeText-icon);
}
#print::before,
#secondaryPrint::before {
mask-image: var(--toolbarButton-print-icon);

View file

@ -263,30 +263,39 @@ See https://github.com/adobe-type-tools/cmap-resources
<span id="numPages" class="toolbarLabel"></span>
</div>
<div id="toolbarViewerRight">
<button id="presentationMode" class="toolbarButton hiddenLargeView" title="Switch to Presentation Mode" tabindex="31" data-l10n-id="presentation_mode">
<div id="editorModeButtons" class="splitToolbarButton hidden" role="radiogroup">
<button id="editorNone" class="toolbarButton" title="Disable Annotation Editing" role="radio" aria-checked="false" tabindex="31" data-l10n-id="editor_none">
<span data-l10n-id="editor_none_label">Disable Editing</span>
</button>
<button id="editorFreeText" class="toolbarButton" title="Add FreeText Annotation" role="radio" aria-checked="false" tabindex="32" data-l10n-id="editor_free_text">
<span data-l10n-id="editor_free_text_label">FreeText Annotation</span>
</button>
</div>
<button id="presentationMode" class="toolbarButton hiddenLargeView" title="Switch to Presentation Mode" tabindex="43" data-l10n-id="presentation_mode">
<span data-l10n-id="presentation_mode_label">Presentation Mode</span>
</button>
<!--#if GENERIC-->
<button id="openFile" class="toolbarButton hiddenLargeView" title="Open File" tabindex="32" data-l10n-id="open_file">
<button id="openFile" class="toolbarButton hiddenLargeView" title="Open File" tabindex="44" data-l10n-id="open_file">
<span data-l10n-id="open_file_label">Open</span>
</button>
<!--#endif-->
<button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="33" data-l10n-id="print">
<button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="45" data-l10n-id="print">
<span data-l10n-id="print_label">Print</span>
</button>
<button id="download" class="toolbarButton hiddenMediumView" title="Download" tabindex="34" data-l10n-id="download">
<button id="download" class="toolbarButton hiddenMediumView" title="Download" tabindex="46" data-l10n-id="download">
<span data-l10n-id="download_label">Download</span>
</button>
<a href="#" id="viewBookmark" class="toolbarButton hiddenSmallView" title="Current view (copy or open in new window)" tabindex="35" data-l10n-id="bookmark">
<a href="#" id="viewBookmark" class="toolbarButton hiddenSmallView" title="Current view (copy or open in new window)" tabindex="47" data-l10n-id="bookmark">
<span data-l10n-id="bookmark_label">Current View</span>
</a>
<div class="verticalToolbarSeparator hiddenSmallView"></div>
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="36" data-l10n-id="tools" aria-expanded="false" aria-controls="secondaryToolbar">
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="48" data-l10n-id="tools" aria-expanded="false" aria-controls="secondaryToolbar">
<span data-l10n-id="tools_label">Tools</span>
</button>
</div>

View file

@ -93,6 +93,8 @@ function getViewerConfiguration() {
? document.getElementById("openFile")
: null,
print: document.getElementById("print"),
editorFreeTextButton: document.getElementById("editorFreeText"),
editorNoneButton: document.getElementById("editorNone"),
presentationModeButton: document.getElementById("presentationMode"),
download: document.getElementById("download"),
viewBookmark: document.getElementById("viewBookmark"),