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:
parent
1ac33c960d
commit
be1aa11986
28 changed files with 2321 additions and 18 deletions
85
web/annotation_editor_layer_builder.css
Normal file
85
web/annotation_editor_layer_builder.css
Normal 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;
|
||||
}
|
128
web/annotation_editor_layer_builder.js
Normal file
128
web/annotation_editor_layer_builder.js
Normal 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 };
|
16
web/app.js
16
web/app.js
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
24
web/images/toolbarButton-editorFreeText.svg
Normal file
24
web/images/toolbarButton-editorFreeText.svg
Normal 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 |
4
web/images/toolbarButton-editorNone.svg
Normal file
4
web/images/toolbarButton-editorNone.svg
Normal 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 |
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue