diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js
index 796dd9618..5beff243f 100644
--- a/src/display/editor/annotation_editor_layer.js
+++ b/src/display/editor/annotation_editor_layer.js
@@ -408,6 +408,21 @@ class AnnotationEditorLayer {
return null;
}
+ /**
+ * Create a new editor
+ * @param {Object} data
+ * @returns {AnnotationEditor}
+ */
+ deserialize(data) {
+ switch (data.annotationType) {
+ case AnnotationEditorType.FREETEXT:
+ return FreeTextEditor.deserialize(data, this);
+ case AnnotationEditorType.INK:
+ return InkEditor.deserialize(data, this);
+ }
+ return null;
+ }
+
/**
* Create and add a new editor.
* @param {MouseEvent} event
diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js
index 110e8464d..796f3c85b 100644
--- a/src/display/editor/editor.js
+++ b/src/display/editor/editor.js
@@ -290,6 +290,26 @@ class AnnotationEditor {
}
}
+ getRectInCurrentCoords(rect, pageHeight) {
+ const [x1, y1, x2, y2] = rect;
+
+ const width = x2 - x1;
+ const height = y2 - y1;
+
+ switch (this.rotation) {
+ case 0:
+ return [x1, pageHeight - y2, width, height];
+ case 90:
+ return [x1, pageHeight - y1, height, width];
+ case 180:
+ return [x2, pageHeight - y1, width, height];
+ case 270:
+ return [x2, pageHeight - y2, height, width];
+ default:
+ throw new Error("Invalid rotation");
+ }
+ }
+
/**
* Executed once this editor has been rendered.
*/
@@ -336,18 +356,6 @@ class AnnotationEditor {
return false;
}
- /**
- * Copy the elements of an editor in order to be able to build
- * a new one from these data.
- * It's used on ctrl+c action.
- *
- * To implement in subclasses.
- * @returns {AnnotationEditor}
- */
- copy() {
- unreachable("An editor must be copyable");
- }
-
/**
* Check if this editor needs to be rebuilt or not.
* @returns {boolean}
@@ -378,6 +386,34 @@ class AnnotationEditor {
unreachable("An editor must be serializable");
}
+ /**
+ * Deserialize the editor.
+ * The result of the deserialization is a new editor.
+ *
+ * @param {Object} data
+ * @param {AnnotationEditorLayer} parent
+ * @returns {AnnotationEditor}
+ */
+ static deserialize(data, parent) {
+ const editor = new this.prototype.constructor({
+ parent,
+ id: parent.getNextId(),
+ });
+ editor.rotation = data.rotation;
+
+ const [pageWidth, pageHeight] = parent.pageDimensions;
+ const [x, y, width, height] = editor.getRectInCurrentCoords(
+ data.rect,
+ pageHeight
+ );
+ editor.x = x / pageWidth;
+ editor.y = y / pageHeight;
+ editor.width = width / pageWidth;
+ editor.height = height / pageHeight;
+
+ return editor;
+ }
+
/**
* Remove this editor.
* It's used on ctrl+backspace action.
diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js
index 0bada5621..836924084 100644
--- a/src/display/editor/freetext.js
+++ b/src/display/editor/freetext.js
@@ -13,11 +13,15 @@
* limitations under the License.
*/
+// eslint-disable-next-line max-len
+/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
+
import {
AnnotationEditorParamsType,
AnnotationEditorType,
assert,
LINE_FACTOR,
+ Util,
} from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js";
import { bindEvents } from "./tools.js";
@@ -77,26 +81,6 @@ class FreeTextEditor extends AnnotationEditor {
);
}
- /** @inheritdoc */
- copy() {
- const [width, height] = this.parent.viewportBaseDimensions;
- const editor = new FreeTextEditor({
- parent: this.parent,
- id: this.parent.getNextId(),
- x: this.x * width,
- y: this.y * height,
- });
-
- editor.width = this.width;
- editor.height = this.height;
- editor.#color = this.#color;
- editor.#fontSize = this.#fontSize;
- editor.#content = this.#content;
- editor.#contentHTML = this.#contentHTML;
-
- return editor;
- }
-
static updateDefaultParams(type, value) {
switch (type) {
case AnnotationEditorParamsType.FREETEXT_SIZE:
@@ -370,6 +354,21 @@ class FreeTextEditor extends AnnotationEditor {
return this.div;
}
+ /** @inheritdoc */
+ static deserialize(data, parent) {
+ const editor = super.deserialize(data, parent);
+
+ editor.#fontSize = data.fontSize;
+ editor.#color = Util.makeHexColor(...data.color);
+ editor.#content = data.value;
+ editor.#contentHTML = data.value
+ .split("\n")
+ .map(line => `
${line}
`)
+ .join("");
+
+ return editor;
+ }
+
/** @inheritdoc */
serialize() {
if (this.isEmpty()) {
diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js
index 1767d8941..afcb62e58 100644
--- a/src/display/editor/ink.js
+++ b/src/display/editor/ink.js
@@ -76,34 +76,6 @@ class InkEditor extends AnnotationEditor {
this.#boundCanvasMousedown = this.canvasMousedown.bind(this);
}
- /** @inheritdoc */
- copy() {
- const editor = new InkEditor({
- parent: this.parent,
- id: this.parent.getNextId(),
- });
-
- editor.x = this.x;
- editor.y = this.y;
- editor.width = this.width;
- editor.height = this.height;
- editor.color = this.color;
- editor.thickness = this.thickness;
- editor.paths = this.paths.slice();
- editor.bezierPath2D = this.bezierPath2D.slice();
- editor.scaleFactor = this.scaleFactor;
- editor.translationX = this.translationX;
- editor.translationY = this.translationY;
- editor.#aspectRatio = this.#aspectRatio;
- editor.#baseWidth = this.#baseWidth;
- editor.#baseHeight = this.#baseHeight;
- editor.#disableEditing = this.#disableEditing;
- editor.#realWidth = this.#realWidth;
- editor.#realHeight = this.#realHeight;
-
- return editor;
- }
-
static updateDefaultParams(type, value) {
switch (type) {
case AnnotationEditorParamsType.INK_THICKNESS:
@@ -351,7 +323,7 @@ class InkEditor extends AnnotationEditor {
const xy = [x, y];
bezier = [[xy, xy.slice(), xy.slice(), xy]];
}
- const path2D = this.#buildPath2D(bezier);
+ const path2D = InkEditor.#buildPath2D(bezier);
this.currentPath.length = 0;
const cmd = () => {
@@ -543,7 +515,6 @@ class InkEditor extends AnnotationEditor {
if (this.width) {
// This editor was created in using copy (ctrl+c).
- this.#isCanvasInitialized = true;
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
this.setAt(
baseX * parentWidth,
@@ -551,9 +522,11 @@ class InkEditor extends AnnotationEditor {
this.width * parentWidth,
this.height * parentHeight
);
- this.setDims(this.width * parentWidth, this.height * parentHeight);
+ this.#isCanvasInitialized = true;
this.#setCanvasDims();
+ this.setDims(this.width * parentWidth, this.height * parentHeight);
this.#redraw();
+ this.#setMinDims();
this.div.classList.add("disabled");
} else {
this.div.classList.add("editing");
@@ -570,8 +543,8 @@ class InkEditor extends AnnotationEditor {
return;
}
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
- this.canvas.width = this.width * parentWidth;
- this.canvas.height = this.height * parentHeight;
+ this.canvas.width = Math.ceil(this.width * parentWidth);
+ this.canvas.height = Math.ceil(this.height * parentHeight);
this.#updateTransform();
}
@@ -610,10 +583,7 @@ class InkEditor extends AnnotationEditor {
this.height = height / parentHeight;
if (this.#disableEditing) {
- const padding = this.#getPadding();
- const scaleFactorW = (width - padding) / this.#baseWidth;
- const scaleFactorH = (height - padding) / this.#baseHeight;
- this.scaleFactor = Math.min(scaleFactorW, scaleFactorH);
+ this.#setScaleFactor(width, height);
}
this.#setCanvasDims();
@@ -622,6 +592,13 @@ class InkEditor extends AnnotationEditor {
this.canvas.style.visibility = "visible";
}
+ #setScaleFactor(width, height) {
+ const padding = this.#getPadding();
+ const scaleFactorW = (width - padding) / this.#baseWidth;
+ const scaleFactorH = (height - padding) / this.#baseHeight;
+ this.scaleFactor = Math.min(scaleFactorW, scaleFactorH);
+ }
+
/**
* Update the canvas transform.
*/
@@ -642,7 +619,7 @@ class InkEditor extends AnnotationEditor {
* @param {Arra} bezier
* @returns {Path2D}
*/
- #buildPath2D(bezier) {
+ static #buildPath2D(bezier) {
const path2D = new Path2D();
for (let i = 0, ii = bezier.length; i < ii; i++) {
const [first, control1, control2, second] = bezier[i];
@@ -859,14 +836,7 @@ class InkEditor extends AnnotationEditor {
this.height = height / parentHeight;
this.#aspectRatio = width / height;
- const { style } = this.div;
- if (this.#aspectRatio >= 1) {
- style.minHeight = `${RESIZER_SIZE}px`;
- style.minWidth = `${Math.round(this.#aspectRatio * RESIZER_SIZE)}px`;
- } else {
- style.minWidth = `${RESIZER_SIZE}px`;
- style.minHeight = `${Math.round(RESIZER_SIZE / this.#aspectRatio)}px`;
- }
+ this.#setMinDims();
const prevTranslationX = this.translationX;
const prevTranslationY = this.translationY;
@@ -886,6 +856,68 @@ class InkEditor extends AnnotationEditor {
);
}
+ #setMinDims() {
+ const { style } = this.div;
+ if (this.#aspectRatio >= 1) {
+ style.minHeight = `${RESIZER_SIZE}px`;
+ style.minWidth = `${Math.round(this.#aspectRatio * RESIZER_SIZE)}px`;
+ } else {
+ style.minWidth = `${RESIZER_SIZE}px`;
+ style.minHeight = `${Math.round(RESIZER_SIZE / this.#aspectRatio)}px`;
+ }
+ }
+
+ /** @inheritdoc */
+ static deserialize(data, parent) {
+ const editor = super.deserialize(data, parent);
+
+ editor.thickness = data.thickness;
+ editor.color = Util.makeHexColor(...data.color);
+
+ const [pageWidth, pageHeight] = parent.pageDimensions;
+ const width = editor.width * pageWidth;
+ const height = editor.height * pageHeight;
+ const scaleFactor = parent.scaleFactor;
+ const padding = data.thickness / 2;
+
+ editor.#aspectRatio = width / height;
+ editor.#disableEditing = true;
+ editor.#realWidth = Math.round(width);
+ editor.#realHeight = Math.round(height);
+
+ for (const { bezier } of data.paths) {
+ const path = [];
+ editor.paths.push(path);
+ let p0 = scaleFactor * (bezier[0] - padding);
+ let p1 = scaleFactor * (height - bezier[1] - padding);
+ for (let i = 2, ii = bezier.length; i < ii; i += 6) {
+ const p10 = scaleFactor * (bezier[i] - padding);
+ const p11 = scaleFactor * (height - bezier[i + 1] - padding);
+ const p20 = scaleFactor * (bezier[i + 2] - padding);
+ const p21 = scaleFactor * (height - bezier[i + 3] - padding);
+ const p30 = scaleFactor * (bezier[i + 4] - padding);
+ const p31 = scaleFactor * (height - bezier[i + 5] - padding);
+ path.push([
+ [p0, p1],
+ [p10, p11],
+ [p20, p21],
+ [p30, p31],
+ ]);
+ p0 = p30;
+ p1 = p31;
+ }
+ const path2D = this.#buildPath2D(path);
+ editor.bezierPath2D.push(path2D);
+ }
+
+ const bbox = editor.#getBbox();
+ editor.#baseWidth = bbox[2] - bbox[0];
+ editor.#baseHeight = bbox[3] - bbox[1];
+ editor.#setScaleFactor(width, height);
+
+ return editor;
+ }
+
/** @inheritdoc */
serialize() {
if (this.isEmpty()) {
diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js
index 39f2f26bc..d6f9c1646 100644
--- a/src/display/editor/tools.js
+++ b/src/display/editor/tools.js
@@ -284,16 +284,25 @@ class KeyboardManager {
* It has to be used as a singleton.
*/
class ClipboardManager {
- constructor() {
- this.element = null;
- }
+ #elements = null;
/**
* Copy an element.
- * @param {AnnotationEditor} element
+ * @param {AnnotationEditor|Array} element
*/
copy(element) {
- this.element = element.copy();
+ if (!element) {
+ return;
+ }
+ if (Array.isArray(element)) {
+ this.#elements = element.map(el => el.serialize());
+ } else {
+ this.#elements = [element.serialize()];
+ }
+ this.#elements = this.#elements.filter(el => !!el);
+ if (this.#elements.length === 0) {
+ this.#elements = null;
+ }
}
/**
@@ -301,7 +310,7 @@ class ClipboardManager {
* @returns {AnnotationEditor|null}
*/
paste() {
- return this.element?.copy() || null;
+ return this.#elements;
}
/**
@@ -309,11 +318,11 @@ class ClipboardManager {
* @returns {boolean}
*/
isEmpty() {
- return this.element === null;
+ return this.#elements === null;
}
destroy() {
- this.element = null;
+ this.#elements = null;
}
}
@@ -397,6 +406,8 @@ class AnnotationEditorUIManager {
#commandManager = new CommandManager();
+ #currentPageIndex = 0;
+
#editorTypes = null;
#eventBus = null;
@@ -413,6 +424,8 @@ class AnnotationEditorUIManager {
#boundOnEditingAction = this.onEditingAction.bind(this);
+ #boundOnPageChanging = this.onPageChanging.bind(this);
+
#previousStates = {
isEditing: false,
isEmpty: true,
@@ -425,10 +438,12 @@ class AnnotationEditorUIManager {
constructor(eventBus) {
this.#eventBus = eventBus;
this.#eventBus._on("editingaction", this.#boundOnEditingAction);
+ this.#eventBus._on("pagechanging", this.#boundOnPageChanging);
}
destroy() {
this.#eventBus._off("editingaction", this.#boundOnEditingAction);
+ this.#eventBus._off("pagechanging", this.#boundOnPageChanging);
for (const layer of this.#allLayers.values()) {
layer.destroy();
}
@@ -439,6 +454,10 @@ class AnnotationEditorUIManager {
this.#commandManager.destroy();
}
+ onPageChanging({ pageNumber }) {
+ this.#currentPageIndex = pageNumber - 1;
+ }
+
/**
* Execute an action for a given name.
* For example, the user can click on the "Undo" entry in the context menu
@@ -822,18 +841,21 @@ class AnnotationEditorUIManager {
* @returns {undefined}
*/
paste() {
- const editor = this.#clipboardManager.paste();
- if (!editor) {
+ if (this.#clipboardManager.isEmpty()) {
return;
}
- // TODO: paste in the current visible layer.
+
+ const layer = this.#allLayers.get(this.#currentPageIndex);
+ const newEditors = this.#clipboardManager
+ .paste()
+ .map(data => layer.deserialize(data));
+
const cmd = () => {
- this.#addEditorToLayer(editor);
+ newEditors.map(editor => this.#addEditorToLayer(editor));
};
const undo = () => {
- editor.remove();
+ newEditors.map(editor => editor.remove());
};
-
this.addCommands({ cmd, undo, mustExec: true });
}
diff --git a/test/integration/freetext_editor_spec.js b/test/integration/freetext_editor_spec.js
index ed275087c..35c911e1f 100644
--- a/test/integration/freetext_editor_spec.js
+++ b/test/integration/freetext_editor_spec.js
@@ -95,7 +95,7 @@ describe("Editor", () => {
el.innerText.trimEnd()
);
- let pastedContent = await page.$eval(`${editorPrefix}2`, el =>
+ let pastedContent = await page.$eval(`${editorPrefix}1`, el =>
el.innerText.trimEnd()
);
@@ -111,7 +111,7 @@ describe("Editor", () => {
await page.keyboard.press("v");
await page.keyboard.up("Control");
- pastedContent = await page.$eval(`${editorPrefix}4`, el =>
+ pastedContent = await page.$eval(`${editorPrefix}2`, el =>
el.innerText.trimEnd()
);
expect(pastedContent)
@@ -132,7 +132,7 @@ describe("Editor", () => {
await page.keyboard.press("Backspace");
await page.keyboard.up("Control");
- for (const n of [0, 2, 4]) {
+ for (const n of [0, 1, 2]) {
const hasEditor = await page.evaluate(sel => {
return !!document.querySelector(sel);
}, `${editorPrefix}${n}`);
@@ -153,9 +153,9 @@ describe("Editor", () => {
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
- await page.type(`${editorPrefix}5 .internal`, data);
+ await page.type(`${editorPrefix}3 .internal`, data);
- const editorRect = await page.$eval(`${editorPrefix}5`, el => {
+ const editorRect = await page.$eval(`${editorPrefix}3`, el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
@@ -181,7 +181,7 @@ describe("Editor", () => {
let hasEditor = await page.evaluate(sel => {
return !!document.querySelector(sel);
- }, `${editorPrefix}7`);
+ }, `${editorPrefix}4`);
expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);
@@ -191,7 +191,7 @@ describe("Editor", () => {
hasEditor = await page.evaluate(sel => {
return !!document.querySelector(sel);
- }, `${editorPrefix}7`);
+ }, `${editorPrefix}4`);
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
})