[Editor] Add a new editor to highlight some text in a pdf (bug 1866119)

This patch is first big step for the new highlight feature.
Few patches will follow in order to conform to the specs UX/UI gave us.
This commit is contained in:
Calixte Denizet 2023-11-22 19:02:42 +01:00
parent 4bf7ff2027
commit 1ea6293923
19 changed files with 897 additions and 56 deletions

View File

@ -81,6 +81,10 @@
"description": "Whether to allow execution of active content (JavaScript) by PDF files.",
"default": false
},
"enableHighlightEditor": {
"type": "boolean",
"default": false
},
"disableRange": {
"title": "Disable range requests",
"description": "Whether to disable range requests (not recommended).",

View File

@ -21,10 +21,12 @@
/** @typedef {import("../../../web/interfaces").IL10n} IL10n */
// eslint-disable-next-line max-len
/** @typedef {import("../annotation_layer.js").AnnotationLayer} AnnotationLayer */
/** @typedef {import("../draw_layer.js").DrawLayer} DrawLayer */
import { AnnotationEditorType, FeatureTest } from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js";
import { FreeTextEditor } from "./freetext.js";
import { HighlightEditor } from "./highlight.js";
import { InkEditor } from "./ink.js";
import { setLayerDimensions } from "../display_utils.js";
import { StampEditor } from "./stamp.js";
@ -39,6 +41,8 @@ import { StampEditor } from "./stamp.js";
* @property {number} pageIndex
* @property {IL10n} l10n
* @property {AnnotationLayer} [annotationLayer]
* @property {HTMLDivElement} [textLayer]
* @property {DrawLayer} drawLayer
* @property {PageViewport} viewport
*/
@ -59,10 +63,14 @@ class AnnotationEditorLayer {
#boundPointerup = this.pointerup.bind(this);
#boundPointerUpAfterSelection = this.pointerUpAfterSelection.bind(this);
#boundPointerdown = this.pointerdown.bind(this);
#editorFocusTimeoutId = null;
#boundSelectionStart = this.selectionStart.bind(this);
#editors = new Map();
#hadPointerDown = false;
@ -71,12 +79,14 @@ class AnnotationEditorLayer {
#isDisabling = false;
#textLayer = null;
#uiManager;
static _initialized = false;
static #editorTypes = new Map(
[FreeTextEditor, InkEditor, StampEditor].map(type => [
[FreeTextEditor, InkEditor, StampEditor, HighlightEditor].map(type => [
type._editorType,
type,
])
@ -91,6 +101,8 @@ class AnnotationEditorLayer {
div,
accessibilityManager,
annotationLayer,
drawLayer,
textLayer,
viewport,
l10n,
}) {
@ -109,6 +121,8 @@ class AnnotationEditorLayer {
this.#accessibilityManager = accessibilityManager;
this.#annotationLayer = annotationLayer;
this.viewport = viewport;
this.#textLayer = textLayer;
this.drawLayer = drawLayer;
this.#uiManager.addLayer(this);
}
@ -131,11 +145,23 @@ class AnnotationEditorLayer {
*/
updateMode(mode = this.#uiManager.getMode()) {
this.#cleanup();
if (mode === AnnotationEditorType.INK) {
// We always want to an ink editor ready to draw in.
switch (mode) {
case AnnotationEditorType.INK:
// We always want to have an ink editor ready to draw in.
this.addInkEditorIfNeeded(false);
this.disableTextSelection();
this.togglePointerEvents(true);
this.disableClick();
} else {
break;
case AnnotationEditorType.HIGHLIGHT:
this.enableTextSelection();
this.togglePointerEvents(false);
this.disableClick();
break;
default:
this.disableTextSelection();
this.togglePointerEvents(true);
this.enableClick();
}
@ -272,6 +298,7 @@ class AnnotationEditorLayer {
for (const editorType of AnnotationEditorLayer.#editorTypes.values()) {
classList.remove(`${editorType._type}Editing`);
}
this.disableTextSelection();
this.#isDisabling = false;
}
@ -293,6 +320,18 @@ class AnnotationEditorLayer {
this.#uiManager.setActiveEditor(editor);
}
enableTextSelection() {
if (this.#textLayer?.div) {
document.addEventListener("selectstart", this.#boundSelectionStart);
}
}
disableTextSelection() {
if (this.#textLayer?.div) {
document.removeEventListener("selectstart", this.#boundSelectionStart);
}
}
enableClick() {
this.div.addEventListener("pointerdown", this.#boundPointerdown);
this.div.addEventListener("pointerup", this.#boundPointerup);
@ -458,18 +497,24 @@ class AnnotationEditorLayer {
return this.#uiManager.getId();
}
get #currentEditorType() {
return AnnotationEditorLayer.#editorTypes.get(this.#uiManager.getMode());
}
/**
* Create a new editor
* @param {Object} params
* @returns {AnnotationEditor}
*/
#createNewEditor(params) {
const editorType = AnnotationEditorLayer.#editorTypes.get(
this.#uiManager.getMode()
);
const editorType = this.#currentEditorType;
return editorType ? new editorType.prototype.constructor(params) : null;
}
canCreateNewEmptyEditor() {
return this.#currentEditorType?.canCreateNewEmptyEditor();
}
/**
* Paste some content into a new editor.
* @param {number} mode
@ -512,9 +557,10 @@ class AnnotationEditorLayer {
* Create and add a new editor.
* @param {PointerEvent} event
* @param {boolean} isCentered
* @param [Object] data
* @returns {AnnotationEditor}
*/
#createAndAddNewEditor(event, isCentered) {
#createAndAddNewEditor(event, isCentered, data = {}) {
const id = this.getNextId();
const editor = this.#createNewEditor({
parent: this,
@ -523,6 +569,7 @@ class AnnotationEditorLayer {
y: event.offsetY,
uiManager: this.#uiManager,
isCentered,
...data,
});
if (editor) {
this.add(editor);
@ -589,6 +636,98 @@ class AnnotationEditorLayer {
this.#uiManager.unselect(editor);
}
/**
* SelectionChange callback.
* @param {Event} _event
*/
selectionStart(_event) {
this.#textLayer?.div.addEventListener(
"pointerup",
this.#boundPointerUpAfterSelection,
{ once: true }
);
}
/**
* Called when the user releases the mouse button after having selected
* some text.
* @param {PointerEvent} event
*/
pointerUpAfterSelection(event) {
const selection = document.getSelection();
if (selection.rangeCount === 0) {
return;
}
const range = selection.getRangeAt(0);
if (range.collapsed) {
return;
}
if (!this.#textLayer?.div.contains(range.commonAncestorContainer)) {
return;
}
const {
x: layerX,
y: layerY,
width: parentWidth,
height: parentHeight,
} = this.#textLayer.div.getBoundingClientRect();
const bboxes = range.getClientRects();
// We must rotate the boxes because we want to have them in the non-rotated
// page coordinates.
let rotator;
switch (this.viewport.rotation) {
case 90:
rotator = (x, y, w, h) => ({
x: (y - layerY) / parentHeight,
y: 1 - (x + w - layerX) / parentWidth,
width: h / parentHeight,
height: w / parentWidth,
});
break;
case 180:
rotator = (x, y, w, h) => ({
x: 1 - (x + w - layerX) / parentWidth,
y: 1 - (y + h - layerY) / parentHeight,
width: w / parentWidth,
height: h / parentHeight,
});
break;
case 270:
rotator = (x, y, w, h) => ({
x: 1 - (y + h - layerY) / parentHeight,
y: (x - layerX) / parentWidth,
width: h / parentHeight,
height: w / parentWidth,
});
break;
default:
rotator = (x, y, w, h) => ({
x: (x - layerX) / parentWidth,
y: (y - layerY) / parentHeight,
width: w / parentWidth,
height: h / parentHeight,
});
break;
}
const boxes = [];
for (const { x, y, width, height } of bboxes) {
if (width === 0 || height === 0) {
continue;
}
boxes.push(rotator(x, y, width, height));
}
if (boxes.length !== 0) {
this.#createAndAddNewEditor(event, false, {
boxes,
});
}
selection.empty();
}
/**
* Pointerup callback.
* @param {PointerEvent} event
@ -631,6 +770,9 @@ class AnnotationEditorLayer {
* @param {PointerEvent} event
*/
pointerdown(event) {
if (this.#uiManager.getMode() === AnnotationEditorType.HIGHLIGHT) {
this.enableTextSelection();
}
if (this.#hadPointerDown) {
// It's possible to have a second pointerdown event before a pointerup one
// when the user puts a finger on a touchscreen and then add a second one
@ -734,8 +876,15 @@ class AnnotationEditorLayer {
// the viewport.
this.#uiManager.commitOrRemove();
const oldRotation = this.viewport.rotation;
const rotation = viewport.rotation;
this.viewport = viewport;
setLayerDimensions(this.div, { rotation: viewport.rotation });
setLayerDimensions(this.div, { rotation });
if (oldRotation !== rotation) {
for (const editor of this.#editors.values()) {
editor.rotate(rotation);
}
}
this.updateMode();
}

View File

@ -507,7 +507,11 @@ class AnnotationEditor {
}
}
fixAndSetPosition() {
/**
* Fix the position of the editor in order to keep it inside its parent page.
* @param {number} [rotation] - the rotation of the page.
*/
fixAndSetPosition(rotation = this.rotation) {
const [pageWidth, pageHeight] = this.pageDimensions;
let { x, y, width, height } = this;
width *= pageWidth;
@ -515,7 +519,7 @@ class AnnotationEditor {
x *= pageWidth;
y *= pageHeight;
switch (this.rotation) {
switch (rotation) {
case 0:
x = Math.max(0, Math.min(pageWidth - width, x));
y = Math.max(0, Math.min(pageHeight - height, y));
@ -1125,14 +1129,28 @@ class AnnotationEditor {
this.#hasBeenClicked = true;
if (this._isDraggable) {
this.#setUpDragSession(event);
}
#setUpDragSession(event) {
if (!this._isDraggable) {
return;
}
this.#selectOnPointerEvent(event);
}
#selectOnPointerEvent(event) {
const { isMac } = FeatureTest.platform;
if (
(event.ctrlKey && !isMac) ||
event.shiftKey ||
(event.metaKey && isMac)
) {
this.parent.toggleSelected(this);
} else {
this.parent.setSelected(this);
}
}
#setUpDragSession(event) {
const isSelected = this._uiManager.isSelected(this);
this._uiManager.setUpDragSession();
@ -1163,16 +1181,7 @@ class AnnotationEditor {
this.#hasBeenClicked = false;
if (!this._uiManager.endDragSession()) {
const { isMac } = FeatureTest.platform;
if (
(event.ctrlKey && !isMac) ||
event.shiftKey ||
(event.metaKey && isMac)
) {
this.parent.toggleSelected(this);
} else {
this.parent.setSelected(this);
}
this.#selectOnPointerEvent(event);
}
};
window.addEventListener("pointerup", pointerUpCallback);
@ -1204,8 +1213,11 @@ class AnnotationEditor {
/**
* Convert the current rect into a page one.
* @param {number} tx - x-translation in screen coordinates.
* @param {number} ty - y-translation in screen coordinates.
* @param {number} [rotation] - the rotation of the page.
*/
getRect(tx, ty) {
getRect(tx, ty, rotation = this.rotation) {
const scale = this.parentScale;
const [pageWidth, pageHeight] = this.pageDimensions;
const [pageX, pageY] = this.pageTranslation;
@ -1216,7 +1228,7 @@ class AnnotationEditor {
const width = this.width * pageWidth;
const height = this.height * pageHeight;
switch (this.rotation) {
switch (rotation) {
case 0:
return [
x + shiftX + pageX,
@ -1332,6 +1344,12 @@ class AnnotationEditor {
this.div?.addEventListener("focusout", this.#boundFocusout);
}
/**
* Rotate the editor.
* @param {number} angle
*/
rotate(_angle) {}
/**
* Serialize the editor.
* The result of the serialization will be used to construct a
@ -1426,6 +1444,10 @@ class AnnotationEditor {
}
}
get toolbarPosition() {
return null;
}
/**
* onkeydown callback.
* @param {KeyboardEvent} event
@ -1669,6 +1691,10 @@ class AnnotationEditor {
static get MIN_SIZE() {
return 16;
}
static canCreateNewEmptyEditor() {
return true;
}
}
// This class is used to fake an editor which has been deleted.

View File

@ -0,0 +1,454 @@
/* 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.
*/
import {
AnnotationEditorParamsType,
AnnotationEditorType,
Util,
} from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js";
import { bindEvents } from "./tools.js";
import { Outliner } from "./outliner.js";
/**
* Basic draw editor in order to generate an Highlight annotation.
*/
class HighlightEditor extends AnnotationEditor {
#boxes;
#clipPathId = null;
#color;
#focusOutlines = null;
#highlightDiv = null;
#highlightOutlines = null;
#id = null;
#lastPoint = null;
#opacity;
#outlineId = null;
static _defaultColor = "#FFF066";
static _defaultOpacity = 0.4;
static _l10nPromise;
static _type = "highlight";
static _editorType = AnnotationEditorType.HIGHLIGHT;
constructor(params) {
super({ ...params, name: "highlightEditor" });
this.#color = params.color || HighlightEditor._defaultColor;
this.#opacity = params.opacity || HighlightEditor._defaultOpacity;
this.#boxes = params.boxes || null;
this._isDraggable = false;
this.#createOutlines();
this.#addToDrawLayer();
this.rotate(this.rotation);
}
#createOutlines() {
const outliner = new Outliner(this.#boxes, /* borderWidth = */ 0.001);
this.#highlightOutlines = outliner.getOutlines();
({
x: this.x,
y: this.y,
width: this.width,
height: this.height,
} = this.#highlightOutlines.box);
const outlinerForOutline = new Outliner(
this.#boxes,
/* borderWidth = */ 0.0025,
/* innerMargin = */ 0.001,
this._uiManager.direction === "ltr"
);
this.#focusOutlines = outlinerForOutline.getOutlines();
// The last point is in the pages coordinate system.
const { lastPoint } = this.#focusOutlines.box;
this.#lastPoint = [
(lastPoint[0] - this.x) / this.width,
(lastPoint[1] - this.y) / this.height,
];
}
static initialize(l10n) {
AnnotationEditor.initialize(l10n);
}
static updateDefaultParams(type, value) {
switch (type) {
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
HighlightEditor._defaultColor = value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
HighlightEditor._defaultOpacity = value / 100;
break;
}
}
/** @inheritdoc */
get toolbarPosition() {
return this.#lastPoint;
}
/** @inheritdoc */
updateParams(type, value) {
switch (type) {
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
this.#updateColor(value);
break;
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
this.#updateOpacity(value);
break;
}
}
static get defaultPropertiesToUpdate() {
return [
[
AnnotationEditorParamsType.HIGHLIGHT_COLOR,
HighlightEditor._defaultColor,
],
[
AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
Math.round(HighlightEditor._defaultOpacity * 100),
],
];
}
/** @inheritdoc */
get propertiesToUpdate() {
return [
[
AnnotationEditorParamsType.HIGHLIGHT_COLOR,
this.#color || HighlightEditor._defaultColor,
],
[
AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
Math.round(100 * (this.#opacity ?? HighlightEditor._defaultOpacity)),
],
];
}
/**
* Update the color and make this action undoable.
* @param {string} color
*/
#updateColor(color) {
const savedColor = this.color;
this.addCommands({
cmd: () => {
this.#color = color;
this.parent.drawLayer.changeColor(this.#id, color);
},
undo: () => {
this.#color = savedColor;
this.parent.drawLayer.changeColor(this.#id, savedColor);
},
mustExec: true,
type: AnnotationEditorParamsType.HIGHLIGHT_COLOR,
overwriteIfSameType: true,
keepUndo: true,
});
}
/**
* Update the opacity and make this action undoable.
* @param {number} opacity
*/
#updateOpacity(opacity) {
opacity /= 100;
const savedOpacity = this.#opacity;
this.addCommands({
cmd: () => {
this.#opacity = opacity;
this.parent.drawLayer.changeOpacity(this.#id, opacity);
},
undo: () => {
this.#opacity = savedOpacity;
this.parent.drawLayer.changeOpacity(this.#id, savedOpacity);
},
mustExec: true,
type: AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
overwriteIfSameType: true,
keepUndo: true,
});
}
/** @inheritdoc */
fixAndSetPosition() {
return super.fixAndSetPosition(0);
}
/** @inheritdoc */
getRect(tx, ty) {
return super.getRect(tx, ty, 0);
}
/** @inheritdoc */
onceAdded() {
this.parent.addUndoableEditor(this);
this.div.focus();
}
/** @inheritdoc */
remove() {
super.remove();
this.#cleanDrawLayer();
}
/** @inheritdoc */
rebuild() {
if (!this.parent) {
return;
}
super.rebuild();
if (this.div === null) {
return;
}
this.#addToDrawLayer();
if (!this.isAttachedToDOM) {
// At some point this editor was removed and we're rebuilting it,
// hence we must add it to its parent.
this.parent.add(this);
}
}
setParent(parent) {
if (this.parent && !parent) {
this.#cleanDrawLayer();
} else if (parent) {
this.#addToDrawLayer(parent);
}
super.setParent(parent);
}
#cleanDrawLayer() {
if (this.#id === null || !this.parent) {
return;
}
this.parent.drawLayer.remove(this.#id);
this.#id = null;
this.parent.drawLayer.remove(this.#outlineId);
this.#outlineId = null;
}
#addToDrawLayer(parent = this.parent) {
if (this.#id !== null) {
return;
}
({ id: this.#id, clipPathId: this.#clipPathId } =
parent.drawLayer.highlight(
this.#highlightOutlines,
this.#color,
this.#opacity
));
if (this.#highlightDiv) {
this.#highlightDiv.style.clipPath = this.#clipPathId;
}
this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
}
static #rotateBbox({ x, y, width, height }, angle) {
switch (angle) {
case 90:
return {
x: 1 - y - height,
y: x,
width: height,
height: width,
};
case 180:
return {
x: 1 - x - width,
y: 1 - y - height,
width,
height,
};
case 270:
return {
x: y,
y: 1 - x - width,
width: height,
height: width,
};
}
return {
x,
y,
width,
height,
};
}
/** @inheritdoc */
rotate(angle) {
const { drawLayer } = this.parent;
drawLayer.rotate(this.#id, angle);
drawLayer.rotate(this.#outlineId, angle);
drawLayer.updateBox(this.#id, HighlightEditor.#rotateBbox(this, angle));
drawLayer.updateBox(
this.#outlineId,
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
);
}
/** @inheritdoc */
render() {
if (this.div) {
return this.div;
}
const div = super.render();
const highlightDiv = (this.#highlightDiv = document.createElement("div"));
div.append(highlightDiv);
highlightDiv.className = "internal";
highlightDiv.style.clipPath = this.#clipPathId;
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(this.width * parentWidth, this.height * parentHeight);
bindEvents(this, this.#highlightDiv, ["pointerover", "pointerleave"]);
this.enableEditing();
return div;
}
pointerover() {
this.parent.drawLayer.addClass(this.#outlineId, "hovered");
}
pointerleave() {
this.parent.drawLayer.removeClass(this.#outlineId, "hovered");
}
/** @inheritdoc */
select() {
super.select();
this.parent?.drawLayer.removeClass(this.#outlineId, "hovered");
this.parent?.drawLayer.addClass(this.#outlineId, "selected");
}
/** @inheritdoc */
unselect() {
super.unselect();
this.parent?.drawLayer.removeClass(this.#outlineId, "selected");
}
#serializeBoxes() {
const [pageWidth, pageHeight] = this.pageDimensions;
const boxes = this.#boxes;
const quadPoints = new Array(boxes.length * 8);
let i = 0;
for (const { x, y, width, height } of boxes) {
const sx = x * pageWidth;
const sy = (1 - y - height) * pageHeight;
// The specifications say that the rectangle should start from the bottom
// left corner and go counter-clockwise.
// But when opening the file in Adobe Acrobat it appears that this isn't
// correct hence the 4th and 6th numbers are just swapped.
quadPoints[i] = quadPoints[i + 4] = sx;
quadPoints[i + 1] = quadPoints[i + 3] = sy;
quadPoints[i + 2] = quadPoints[i + 6] = sx + width * pageWidth;
quadPoints[i + 5] = quadPoints[i + 7] = sy + height * pageHeight;
i += 8;
}
return quadPoints;
}
#serializeOutlines() {
const [pageWidth, pageHeight] = this.pageDimensions;
const width = this.width * pageWidth;
const height = this.height * pageHeight;
const tx = this.x * pageWidth;
const ty = (1 - this.y - this.height) * pageHeight;
const outlines = [];
for (const outline of this.#highlightOutlines.outlines) {
const points = new Array(outline.length);
for (let i = 0; i < outline.length; i += 2) {
points[i] = tx + outline[i] * width;
points[i + 1] = ty + (1 - outline[i + 1]) * height;
}
outlines.push(points);
}
return outlines;
}
/** @inheritdoc */
static deserialize(data, parent, uiManager) {
const editor = super.deserialize(data, parent, uiManager);
const { rect, color, quadPoints } = data;
editor.#color = Util.makeHexColor(...color);
editor.#opacity = data.opacity;
const [pageWidth, pageHeight] = editor.pageDimensions;
editor.width = (rect[2] - rect[0]) / pageWidth;
editor.height = (rect[3] - rect[1]) / pageHeight;
const boxes = (editor.#boxes = []);
for (let i = 0; i < quadPoints.length; i += 8) {
boxes.push({
x: quadPoints[4] / pageWidth,
y: 1 - quadPoints[i + 5] / pageHeight,
width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth,
height: (quadPoints[i + 5] - quadPoints[i + 1]) / pageHeight,
});
}
editor.#createOutlines();
return editor;
}
/** @inheritdoc */
serialize(isForCopying = false) {
// It doesn't make sense to copy/paste a highlight annotation.
if (this.isEmpty() || isForCopying) {
return null;
}
const rect = this.getRect(0, 0);
const color = AnnotationEditor._colorManager.convert(this.#color);
return {
annotationType: AnnotationEditorType.HIGHLIGHT,
color,
opacity: this.#opacity,
quadPoints: this.#serializeBoxes(),
outlines: this.#serializeOutlines(),
pageIndex: this.pageIndex,
rect,
rotation: 0,
structTreeParentId: this._structTreeParentId,
};
}
static canCreateNewEmptyEditor() {
return false;
}
}
export { HighlightEditor };

View File

@ -36,6 +36,19 @@ class EditorToolbar {
buttons.className = "buttons";
editToolbar.append(buttons);
const position = this.#editor.toolbarPosition;
if (position) {
const { style } = editToolbar;
const x =
this.#editor._uiManager.direction === "ltr"
? 1 - position[0]
: position[0];
style.insetInlineEnd = `${100 * x}%`;
style.top = `calc(${
100 * position[1]
}% + var(--editor-toolbar-vert-offset))`;
}
this.#addDeleteButton();
return editToolbar;

View File

@ -1217,8 +1217,10 @@ class AnnotationEditorUIManager {
}
addNewEditorFromKeyboard() {
if (this.currentLayer.canCreateNewEmptyEditor()) {
this.currentLayer.addNewEditor();
}
}
/**
* Update the toolbar if it's required to reflect the tool currently used.

View File

@ -86,6 +86,8 @@ const AnnotationEditorParamsType = {
INK_COLOR: 21,
INK_THICKNESS: 22,
INK_OPACITY: 23,
HIGHLIGHT_COLOR: 31,
HIGHLIGHT_OPACITY: 32,
};
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.

View File

@ -13,6 +13,8 @@
* limitations under the License.
*/
@import url(draw_layer_builder.css);
:root {
--outline-width: 2px;
--outline-color: #0060df;
@ -188,7 +190,10 @@
border: var(--focus-outline-around);
}
}
}
.annotationEditorLayer
:is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor) {
.editToolbar {
--editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg);
--editor-toolbar-bg-color: #f0f0f4;
@ -198,6 +203,7 @@
--editor-toolbar-active-bg-color: #cfcfd8;
--editor-toolbar-focus-outline-color: #0060df;
--editor-toolbar-shadow: 0 2px 6px 0 rgb(58 57 68 / 0.2);
--editor-toolbar-vert-offset: 6px;
@media (prefers-color-scheme: dark) {
--editor-toolbar-bg-color: #2b2a33;
@ -225,10 +231,11 @@
justify-content: center;
align-items: center;
cursor: default;
pointer-events: auto;
position: absolute;
inset-inline-end: 0;
inset-block-start: calc(100% + 6px);
inset-block-start: calc(100% + var(--editor-toolbar-vert-offset));
border-radius: 4px;
background-color: var(--editor-toolbar-bg-color);
@ -316,7 +323,7 @@
height: 100%;
}
.annotationEditorLayer .freeTextEditor .overlay.enabled {
.annotationEditorLayer freeTextEditor .overlay.enabled {
display: block;
}
@ -513,12 +520,12 @@
rotate: 270deg;
&:dir(ltr) {
inset-inline-start: calc(100% + 6px);
inset-inline-start: calc(100% + var(--editor-toolbar-vert-offset));
inset-block-start: 0;
}
&:dir(rtl) {
inset-inline-end: calc(100% + 6px);
inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset));
inset-block-end: 0;
inset-block-start: unset;
}
@ -547,7 +554,7 @@
.editToolbar {
rotate: 180deg;
inset-inline-start: 0;
inset-block-end: calc(100% + 6px);
inset-block-end: calc(100% + var(--editor-toolbar-vert-offset));
inset-block-start: unset;
}
}
@ -585,13 +592,13 @@
rotate: 90deg;
&:dir(ltr) {
inset-inline-end: calc(100% + 6px);
inset-inline-end: calc(100% + var(--editor-toolbar-vert-offset));
inset-block-end: 0;
inset-block-start: unset;
}
&:dir(rtl) {
inset-inline-start: calc(100% + 6px);
inset-inline-start: calc(100% + var(--editor-toolbar-vert-offset));
inset-block-start: 0;
}
}
@ -1000,3 +1007,62 @@
}
}
}
.annotationEditorLayer {
&[data-main-rotation="0"] {
.highlightEditor > .editToolbar {
rotate: 0deg;
}
}
&[data-main-rotation="90"] {
.highlightEditor > .editToolbar {
rotate: 270deg;
}
}
&[data-main-rotation="180"] {
.highlightEditor > .editToolbar {
rotate: 180deg;
}
}
&[data-main-rotation="270"] {
.highlightEditor > .editToolbar {
rotate: 90deg;
}
}
.highlightEditor {
position: absolute;
background: transparent;
z-index: 1;
transform-origin: 0 0;
cursor: auto;
max-width: 100%;
max-height: 100%;
border: none;
outline: none;
pointer-events: none;
transform: none;
.internal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto;
}
&.selectedEditor {
.internal {
cursor: pointer;
}
}
.editToolbar {
transform-origin: center;
}
}
}

View File

@ -35,11 +35,17 @@ import { NullL10n } from "web-l10n_utils";
* @property {IL10n} [l10n]
* @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationLayer} [annotationLayer]
* @property {TextLayer} [textLayer]
* @property {DrawLayer} [drawLayer]
*/
class AnnotationEditorLayerBuilder {
#annotationLayer = null;
#drawLayer = null;
#textLayer = null;
#uiManager;
/**
@ -55,6 +61,8 @@ class AnnotationEditorLayerBuilder {
this._cancelled = false;
this.#uiManager = options.uiManager;
this.#annotationLayer = options.annotationLayer || null;
this.#textLayer = options.textLayer || null;
this.#drawLayer = options.drawLayer || null;
}
/**
@ -93,6 +101,8 @@ class AnnotationEditorLayerBuilder {
l10n: this.l10n,
viewport: clonedViewport,
annotationLayer: this.#annotationLayer,
textLayer: this.#textLayer,
drawLayer: this.#drawLayer,
});
const parameters = {

View File

@ -28,6 +28,8 @@ class AnnotationEditorParams {
#bindListeners({
editorFreeTextFontSize,
editorFreeTextColor,
editorHighlightColor,
editorHighlightOpacity,
editorInkColor,
editorInkThickness,
editorInkOpacity,
@ -46,6 +48,12 @@ class AnnotationEditorParams {
editorFreeTextColor.addEventListener("input", function () {
dispatchEvent("FREETEXT_COLOR", this.value);
});
editorHighlightColor.addEventListener("input", function () {
dispatchEvent("HIGHLIGHT_COLOR", this.value);
});
editorHighlightOpacity.addEventListener("input", function () {
dispatchEvent("HIGHLIGHT_OPACITY", this.valueAsNumber);
});
editorInkColor.addEventListener("input", function () {
dispatchEvent("INK_COLOR", this.value);
});
@ -68,6 +76,12 @@ class AnnotationEditorParams {
case AnnotationEditorParamsType.FREETEXT_COLOR:
editorFreeTextColor.value = value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
editorHighlightColor.value = value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
editorHighlightOpacity.value = value;
break;
case AnnotationEditorParamsType.INK_COLOR:
editorInkColor.value = value;
break;

View File

@ -486,6 +486,11 @@ const PDFViewerApplication = {
appConfig.toolbar?.editorStampButton?.classList.add("hidden");
}
const editorHighlightButton = appConfig.toolbar?.editorHighlightButton;
if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) {
editorHighlightButton.hidden = false;
}
this.annotationEditorParams = new AnnotationEditorParams(
appConfig.annotationEditorParams,
eventBus

View File

@ -125,6 +125,14 @@ const defaultOptions = {
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableHighlightEditor: {
// We'll probably want to make some experiments before enabling this
// in Firefox release, but it has to be temporary.
// TODO: remove it when unnecessary.
/** @type {boolean} */
value: typeof PDFJSDev === "undefined",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enablePermissions: {
/** @type {boolean} */
value: false,

View File

@ -13,13 +13,11 @@
* limitations under the License.
*/
/** @typedef {import("../src/display/draw_layer.js").DrawLayer} DrawLayer */
import { DrawLayer } from "pdfjs-lib";
/**
* @typedef {Object} DrawLayerBuilderOptions
* @property {DrawLayer} [drawLayer]
* @property {number} pageIndex
*/
class DrawLayerBuilder {
@ -53,6 +51,14 @@ class DrawLayerBuilder {
this.#drawLayer.destroy();
this.#drawLayer = null;
}
setParent(parent) {
this.#drawLayer?.setParent(parent);
}
getDrawLayer() {
return this.#drawLayer;
}
}
export { DrawLayerBuilder };

View File

@ -0,0 +1,6 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.10918 11.66C7.24918 11.8 7.43918 11.88 7.63918 11.88C7.83918 11.88 8.02918 11.8 8.16918 11.66L14.9192 4.91C15.2692 4.57 15.4592 4.11 15.4592 3.62C15.4592 3.13 15.2692 2.67 14.9192 2.33L13.1292 0.54C12.7892 0.19 12.3292 0 11.8392 0C11.3492 0 10.8892 0.2 10.5492 0.54L3.79918 7.29C3.50918 7.58 3.50918 8.06 3.79918 8.35L4.38988 8.9407L1.40918 11.93H5.64918L6.51419 11.065L7.10918 11.66ZM7.63918 10.07L5.38918 7.82V7.81L7.8648 5.33438L10.1198 7.58938L7.63918 10.07ZM11.1805 6.52872L13.8592 3.85C13.9892 3.72 13.9892 3.52 13.8592 3.39L12.0692 1.6C11.9892 1.52 11.8892 1.5 11.8392 1.5C11.8392 1.5 11.6892 1.51 11.6092 1.59L8.92546 4.27372L11.1805 6.52872Z" fill="#000"/>
<path d="M0.40918 14H15.4092V16H0.40918V14Z" fill="#000"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 910 B

View File

@ -42,6 +42,7 @@ import {
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
import { compatibilityParams } from "./app_options.js";
import { DrawLayerBuilder } from "./draw_layer_builder.js";
import { NullL10n } from "web-l10n_utils";
import { SimpleLinkService } from "./pdf_link_service.js";
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
@ -177,6 +178,7 @@ class PDFPageView {
this.zoomLayer = null;
this.xfaLayer = null;
this.structTreeLayer = null;
this.drawLayer = null;
const div = document.createElement("div");
div.className = "page";
@ -354,6 +356,14 @@ class PDFPageView {
}
}
async #renderDrawLayer() {
try {
await this.drawLayer.render("display");
} catch (ex) {
console.error(`#renderDrawLayer: "${ex}".`);
}
}
async #renderXfaLayer() {
let error = null;
try {
@ -718,6 +728,10 @@ class PDFPageView {
this.annotationEditorLayer &&
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
) {
if (this.drawLayer) {
this.drawLayer.cancel();
this.drawLayer = null;
}
this.annotationEditorLayer.cancel();
this.annotationEditorLayer = null;
}
@ -770,6 +784,9 @@ class PDFPageView {
this.#renderAnnotationLayer();
}
if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
if (this.drawLayer) {
this.#renderDrawLayer();
}
this.#renderAnnotationEditorLayer();
}
if (redrawXfaLayer && this.xfaLayer) {
@ -1001,12 +1018,19 @@ class PDFPageView {
await this.#renderAnnotationLayer();
}
if (!this.annotationEditorLayer) {
const { annotationEditorUIManager } = this.#layerProperties;
if (!annotationEditorUIManager) {
return;
}
this.drawLayer ||= new DrawLayerBuilder({
pageIndex: this.id,
});
await this.#renderDrawLayer();
this.drawLayer.setParent(canvasWrapper);
if (!this.annotationEditorLayer) {
this.annotationEditorLayer = new AnnotationEditorLayerBuilder({
uiManager: annotationEditorUIManager,
pageDiv: div,
@ -1014,6 +1038,8 @@ class PDFPageView {
l10n,
accessibilityManager: this._accessibilityManager,
annotationLayer: this.annotationLayer?.annotationLayer,
textLayer: this.textLayer,
drawLayer: this.drawLayer.getDrawLayer(),
});
}
this.#renderAnnotationEditorLayer();

View File

@ -69,6 +69,18 @@ class Toolbar {
},
},
},
{
element: options.editorHighlightButton,
eventName: "switchannotationeditormode",
eventDetails: {
get mode() {
const { classList } = options.editorHighlightButton;
return classList.contains("toggled")
? AnnotationEditorType.NONE
: AnnotationEditorType.HIGHLIGHT;
},
},
},
{
element: options.editorInkButton,
eventName: "switchannotationeditormode",
@ -202,6 +214,8 @@ class Toolbar {
#bindEditorToolsListener({
editorFreeTextButton,
editorFreeTextParamsToolbar,
editorHighlightButton,
editorHighlightParamsToolbar,
editorInkButton,
editorInkParamsToolbar,
editorStampButton,
@ -213,6 +227,11 @@ class Toolbar {
mode === AnnotationEditorType.FREETEXT,
editorFreeTextParamsToolbar
);
toggleCheckedBtn(
editorHighlightButton,
mode === AnnotationEditorType.HIGHLIGHT,
editorHighlightParamsToolbar
);
toggleCheckedBtn(
editorInkButton,
mode === AnnotationEditorType.INK,
@ -226,6 +245,7 @@ class Toolbar {
const isDisable = mode === AnnotationEditorType.DISABLE;
editorFreeTextButton.disabled = isDisable;
editorHighlightButton.disabled = isDisable;
editorInkButton.disabled = isDisable;
editorStampButton.disabled = isDisable;
};

View File

@ -81,6 +81,7 @@
--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-editorHighlight-icon: url(images/toolbarButton-editorHighlight.svg);
--toolbarButton-editorInk-icon: url(images/toolbarButton-editorInk.svg);
--toolbarButton-editorStamp-icon: url(images/toolbarButton-editorStamp.svg);
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
@ -584,6 +585,10 @@ body {
inset-inline-end: calc(var(--editor-toolbar-base-offset) + 56px);
}
#editorHighlightParamsToolbar {
inset-inline-end: calc(var(--editor-toolbar-base-offset) + 84px);
}
#editorStampAddImage::before {
mask-image: var(--editorParams-stampAddImage-icon);
}
@ -903,6 +908,10 @@ body {
mask-image: var(--toolbarButton-editorFreeText-icon);
}
#editorHighlight::before {
mask-image: var(--toolbarButton-editorHighlight-icon);
}
#editorInk::before {
mask-image: var(--toolbarButton-editorInk-icon);
}

View File

@ -171,15 +171,28 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
</div> <!-- findbar -->
<div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorHighlightColor" class="editorParamsLabel" data-l10n-id="editor_highlight_color">Color</label>
<input type="color" value="#FFFF00" id="editorHighlightColor" class="editorParamsColor" tabindex="100">
</div>
<div class="editorParamsSetter">
<label for="editorHighlightOpacity" class="editorParamsLabel" data-l10n-id="editor_highlight_opacity">Opacity</label>
<input type="range" id="editorHighlightOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="101">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorFreeTextColor" class="editorParamsLabel" data-l10n-id="pdfjs-editor-free-text-color-input">Color</label>
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="100">
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="102">
</div>
<div class="editorParamsSetter">
<label for="editorFreeTextFontSize" class="editorParamsLabel" data-l10n-id="pdfjs-editor-free-text-size-input">Size</label>
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100" step="1" tabindex="101">
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100" step="1" tabindex="103">
</div>
</div>
</div>
@ -188,22 +201,22 @@ See https://github.com/adobe-type-tools/cmap-resources
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorInkColor" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-color-input">Color</label>
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="102">
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="104">
</div>
<div class="editorParamsSetter">
<label for="editorInkThickness" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-thickness-input">Thickness</label>
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1" tabindex="103">
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1" tabindex="105">
</div>
<div class="editorParamsSetter">
<label for="editorInkOpacity" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="104">
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="106">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar">
<div class="editorParamsToolbarContainer">
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="105" data-l10n-id="pdfjs-editor-stamp-add-image-button">
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="107" data-l10n-id="pdfjs-editor-stamp-add-image-button">
<span data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add image</span>
</button>
</div>
@ -334,13 +347,15 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
<div id="toolbarViewerRight">
<div id="editorModeButtons" class="splitToolbarButton toggled" role="radiogroup">
<button id="editorFreeText" class="toolbarButton" disabled="disabled" title="Text" role="radio" aria-checked="false" aria-controls="editorFreeTextParamsToolbar" tabindex="31" data-l10n-id="pdfjs-editor-free-text-button">
<button id="editorHighlight" class="toolbarButton" hidden="true" disabled="disabled" title="Highlight" role="radio" aria-checked="false" aria-controls="editorHighlightParamsToolbar" tabindex="31" data-l10n-id="pdfjs-editor-highlight">
<span data-l10n-id="pdfjs-editor-highlight-label">Highlight</span>
<button id="editorFreeText" class="toolbarButton" disabled="disabled" title="Text" role="radio" aria-checked="false" aria-controls="editorFreeTextParamsToolbar" tabindex="32" data-l10n-id="pdfjs-editor-free-text-button">
<span data-l10n-id="pdfjs-editor-free-text-button-label">Text</span>
</button>
<button id="editorInk" class="toolbarButton" disabled="disabled" title="Draw" role="radio" aria-checked="false" aria-controls="editorInkParamsToolbar" tabindex="32" data-l10n-id="pdfjs-editor-ink-button">
<button id="editorInk" class="toolbarButton" disabled="disabled" title="Draw" role="radio" aria-checked="false" aria-controls="editorInkParamsToolbar" tabindex="33" data-l10n-id="pdfjs-editor-ink-button">
<span data-l10n-id="pdfjs-editor-ink-button-label">Draw</span>
</button>
<button id="editorStamp" class="toolbarButton" disabled="disabled" title="Add or edit images" role="radio" aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="33" data-l10n-id="pdfjs-editor-stamp-button">
<button id="editorStamp" class="toolbarButton" disabled="disabled" title="Add or edit images" role="radio" aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="34" data-l10n-id="pdfjs-editor-stamp-button">
<span data-l10n-id="pdfjs-editor-stamp-button-label">Add or edit images</span>
</button>
</div>

View File

@ -57,6 +57,10 @@ function getViewerConfiguration() {
editorFreeTextParamsToolbar: document.getElementById(
"editorFreeTextParamsToolbar"
),
editorHighlightButton: document.getElementById("editorHighlight"),
editorHighlightParamsToolbar: document.getElementById(
"editorHighlightParamsToolbar"
),
editorInkButton: document.getElementById("editorInk"),
editorInkParamsToolbar: document.getElementById("editorInkParamsToolbar"),
editorStampButton: document.getElementById("editorStamp"),
@ -164,6 +168,8 @@ function getViewerConfiguration() {
annotationEditorParams: {
editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"),
editorFreeTextColor: document.getElementById("editorFreeTextColor"),
editorHighlightColor: document.getElementById("editorHighlightColor"),
editorHighlightOpacity: document.getElementById("editorHighlightOpacity"),
editorInkColor: document.getElementById("editorInkColor"),
editorInkThickness: document.getElementById("editorInkThickness"),
editorInkOpacity: document.getElementById("editorInkOpacity"),