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

Support rotating editor layer

- As in the annotation layer, use percent instead of pixels as unit;
- handle the rotation of the editor layer in allowing editing when rotation
  angle is not zero;
- the different editors are rotated counterclockwise in order to be usable
  when the main page is itself rotated;
- add support for saving/printing rotated editors.
This commit is contained in:
Calixte Denizet 2022-06-23 15:47:45 +02:00
parent b5fea8ff14
commit 0c420f5135
13 changed files with 473 additions and 159 deletions

View file

@ -1509,6 +1509,19 @@ class WidgetAnnotation extends Annotation {
return !!(this.data.fieldFlags & flag);
}
static _getRotationMatrix(rotation, width, height) {
switch (rotation) {
case 90:
return [0, 1, -1, 0, width, 0];
case 180:
return [-1, 0, 0, -1, width, height];
case 270:
return [0, -1, 1, 0, 0, height];
default:
throw new Error("Invalid rotation");
}
}
getRotationMatrix(annotationStorage) {
const storageEntry = annotationStorage
? annotationStorage.get(this.data.id)
@ -1525,16 +1538,7 @@ class WidgetAnnotation extends Annotation {
const width = this.data.rect[2] - this.data.rect[0];
const height = this.data.rect[3] - this.data.rect[1];
switch (rotation) {
case 90:
return [0, 1, -1, 0, width, 0];
case 180:
return [-1, 0, 0, -1, width, height];
case 270:
return [0, -1, 1, 0, 0, height];
default:
throw new Error("Invalid rotation");
}
return WidgetAnnotation._getRotationMatrix(rotation, width, height);
}
getBorderAndBackgroundAppearances(annotationStorage) {
@ -3185,7 +3189,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
}
static createNewDict(annotation, xref, { apRef, ap }) {
const { color, fontSize, rect, user, value } = annotation;
const { color, fontSize, rect, rotation, user, value } = annotation;
const freetext = new Dict(xref);
freetext.set("Type", Name.get("Annot"));
freetext.set("Subtype", Name.get("FreeText"));
@ -3196,7 +3200,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
freetext.set("Contents", value);
freetext.set("F", 4);
freetext.set("Border", [0, 0, 0]);
freetext.set("Rotate", 0);
freetext.set("Rotate", rotation);
if (user) {
freetext.set("T", stringToUTF8String(user));
@ -3216,7 +3220,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
static async createNewAppearanceStream(annotation, xref, params) {
const { baseFontRef, evaluator, task } = params;
const { color, fontSize, rect, value } = annotation;
const { color, fontSize, rect, rotation, value } = annotation;
const resources = new Dict(xref);
const font = new Dict(xref);
@ -3244,8 +3248,12 @@ class FreeTextAnnotation extends MarkupAnnotation {
);
const [x1, y1, x2, y2] = rect;
const w = x2 - x1;
const h = y2 - y1;
let w = x2 - x1;
let h = y2 - y1;
if (rotation % 180 !== 0) {
[w, h] = [h, w];
}
const lines = value.split("\n");
const scale = fontSize / 1000;
@ -3301,6 +3309,11 @@ class FreeTextAnnotation extends MarkupAnnotation {
appearanceStreamDict.set("Length", appearance.length);
appearanceStreamDict.set("Resources", resources);
if (rotation) {
const matrix = WidgetAnnotation._getRotationMatrix(rotation, w, h);
appearanceStreamDict.set("Matrix", matrix);
}
const ap = new StringStream(appearance);
ap.dict = appearanceStreamDict;
@ -3669,18 +3682,19 @@ class InkAnnotation extends MarkupAnnotation {
}
static createNewDict(annotation, xref, { apRef, ap }) {
const { paths, rect, rotation } = annotation;
const ink = new Dict(xref);
ink.set("Type", Name.get("Annot"));
ink.set("Subtype", Name.get("Ink"));
ink.set("CreationDate", `D:${getModificationDate()}`);
ink.set("Rect", annotation.rect);
ink.set("Rect", rect);
ink.set(
"InkList",
annotation.paths.map(p => p.points)
paths.map(p => p.points)
);
ink.set("F", 4);
ink.set("Border", [0, 0, 0]);
ink.set("Rotate", 0);
ink.set("Rotate", rotation);
const n = new Dict(xref);
ink.set("AP", n);
@ -3695,16 +3709,21 @@ class InkAnnotation extends MarkupAnnotation {
}
static async createNewAppearanceStream(annotation, xref, params) {
const [x1, y1, x2, y2] = annotation.rect;
const w = x2 - x1;
const h = y2 - y1;
const { color, rect, rotation, paths, thickness } = annotation;
const [x1, y1, x2, y2] = rect;
let w = x2 - x1;
let h = y2 - y1;
if (rotation % 180 !== 0) {
[w, h] = [h, w];
}
const appearanceBuffer = [
`${annotation.thickness} w`,
`${getPdfColor(annotation.color, /* isFill */ false)}`,
`${thickness} w`,
`${getPdfColor(color, /* isFill */ false)}`,
];
const buffer = [];
for (const { bezier } of annotation.paths) {
for (const { bezier } of paths) {
buffer.length = 0;
buffer.push(
`${numberToString(bezier[0])} ${numberToString(bezier[1])} m`
@ -3728,6 +3747,11 @@ class InkAnnotation extends MarkupAnnotation {
appearanceStreamDict.set("BBox", [0, 0, w, h]);
appearanceStreamDict.set("Length", appearance.length);
if (rotation) {
const matrix = WidgetAnnotation._getRotationMatrix(rotation, w, h);
appearanceStreamDict.set("Matrix", matrix);
}
const ap = new StringStream(appearance);
ap.dict = appearanceStreamDict;

View file

@ -294,7 +294,7 @@ class AnnotationElement {
container.style.width = `${elementWidth}%`;
container.style.height = `${elementHeight}%`;
container.setAttribute("data-annotation-rotation", (360 - angle) % 360);
container.setAttribute("data-main-rotation", (360 - angle) % 360);
}
get _commonActions() {
@ -2552,7 +2552,7 @@ class AnnotationLayer {
style.width = flipOrientation ? heightStr : widthStr;
style.height = flipOrientation ? widthStr : heightStr;
div.setAttribute("data-annotation-rotation", rotation);
div.setAttribute("data-main-rotation", rotation);
}
static #setAnnotationCanvasMap(div, annotationCanvasMap) {

View file

@ -20,8 +20,8 @@
/** @typedef {import("../annotation_storage.js").AnnotationStorage} AnnotationStorage */
/** @typedef {import("../../web/interfaces").IL10n} IL10n */
import { AnnotationEditorType, Util } from "../../shared/util.js";
import { bindEvents, KeyboardManager } from "./tools.js";
import { AnnotationEditorType } from "../../shared/util.js";
import { FreeTextEditor } from "./freetext.js";
import { InkEditor } from "./ink.js";
@ -106,6 +106,7 @@ class AnnotationEditorLayer {
} else {
this.div.removeEventListener("mouseover", this.#boundMouseover);
}
this.setActiveEditor(null);
}
/**
@ -273,6 +274,11 @@ class AnnotationEditorLayer {
if (editor.parent === this) {
return;
}
if (this.#uiManager.isActive(editor)) {
editor.parent.setActiveEditor(null);
}
this.attach(editor);
editor.pageIndex = this.pageIndex;
editor.parent.detach(editor);
@ -419,10 +425,10 @@ class AnnotationEditorLayer {
this.#changeParent(editor);
const rect = this.div.getBoundingClientRect();
editor.setAt(
event.clientX - rect.x - editor.mouseX,
event.clientY - rect.y - editor.mouseY
);
const endX = event.clientX - rect.x;
const endY = event.clientY - rect.y;
editor.translate(endX - editor.startX, endY - editor.startY);
}
/**
@ -463,11 +469,9 @@ class AnnotationEditorLayer {
*/
render(parameters) {
this.viewport = parameters.viewport;
this.inverseViewportTransform = Util.inverseTransform(
this.viewport.transform
);
bindEvents(this, this.div, ["dragover", "drop", "keydown"]);
this.div.addEventListener("click", this.#boundClick);
this.setDimensions();
}
/**
@ -475,17 +479,9 @@ class AnnotationEditorLayer {
* @param {Object} parameters
*/
update(parameters) {
const transform = Util.transform(
parameters.viewport.transform,
this.inverseViewportTransform
);
this.setActiveEditor(null);
this.viewport = parameters.viewport;
this.inverseViewportTransform = Util.inverseTransform(
this.viewport.transform
);
for (const editor of this.#editors.values()) {
editor.transform(transform);
}
this.setDimensions();
}
/**
@ -495,6 +491,38 @@ class AnnotationEditorLayer {
get scaleFactor() {
return this.viewport.scale;
}
/**
* Get page dimensions.
* @returns {Object} dimensions.
*/
get pageDimensions() {
const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox;
const width = pageURx - pageLLx;
const height = pageURy - pageLLy;
return [width, height];
}
get viewportBaseDimensions() {
const { width, height, rotation } = this.viewport;
return rotation % 180 === 0 ? [width, height] : [height, width];
}
/**
* Set the dimensions of the main div.
*/
setDimensions() {
const { width, height, rotation } = this.viewport;
const flipOrientation = rotation % 180 !== 0,
widthStr = Math.floor(width) + "px",
heightStr = Math.floor(height) + "px";
this.div.style.width = flipOrientation ? heightStr : widthStr;
this.div.style.height = flipOrientation ? widthStr : heightStr;
this.div.setAttribute("data-main-rotation", rotation);
}
}
export { AnnotationEditorLayer };

View file

@ -16,8 +16,8 @@
// eslint-disable-next-line max-len
/** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */
import { unreachable, Util } from "../../shared/util.js";
import { bindEvents } from "./tools.js";
import { unreachable } from "../../shared/util.js";
/**
* @typedef {Object} AnnotationEditorParameters
@ -47,8 +47,11 @@ class AnnotationEditor {
this.pageIndex = parameters.parent.pageIndex;
this.name = parameters.name;
this.div = null;
this.x = Math.round(parameters.x);
this.y = Math.round(parameters.y);
const [width, height] = this.parent.viewportBaseDimensions;
this.x = parameters.x / width;
this.y = parameters.y / height;
this.rotation = this.parent.viewport.rotation;
this.isAttachedToDOM = false;
}
@ -107,21 +110,14 @@ class AnnotationEditor {
}
}
/**
* Get the pointer coordinates in order to correctly translate the
* div in case of drag-and-drop.
* @param {MouseEvent} event
*/
mousedown(event) {
this.mouseX = event.offsetX;
this.mouseY = event.offsetY;
}
/**
* We use drag-and-drop in order to move an editor on a page.
* @param {DragEvent} event
*/
dragstart(event) {
const rect = this.parent.div.getBoundingClientRect();
this.startX = event.clientX - rect.x;
this.startY = event.clientY - rect.y;
event.dataTransfer.setData("text/plain", this.id);
event.dataTransfer.effectAllowed = "move";
}
@ -130,22 +126,53 @@ class AnnotationEditor {
* Set the editor position within its parent.
* @param {number} x
* @param {number} y
* @param {number} tx - x-translation in screen coordinates.
* @param {number} ty - y-translation in screen coordinates.
*/
setAt(x, y) {
this.x = Math.round(x);
this.y = Math.round(y);
setAt(x, y, tx, ty) {
const [width, height] = this.parent.viewportBaseDimensions;
[tx, ty] = this.screenToPageTranslation(tx, ty);
this.div.style.left = `${this.x}px`;
this.div.style.top = `${this.y}px`;
this.x = (x + tx) / width;
this.y = (y + ty) / height;
this.div.style.left = `${100 * this.x}%`;
this.div.style.top = `${100 * this.y}%`;
}
/**
* Translate the editor position within its parent.
* @param {number} x - x-translation in screen coordinates.
* @param {number} y - y-translation in screen coordinates.
*/
translate(x, y) {
const [width, height] = this.parent.viewportBaseDimensions;
[x, y] = this.screenToPageTranslation(x, y);
this.x += x / width;
this.y += y / height;
this.div.style.left = `${100 * this.x}%`;
this.div.style.top = `${100 * this.y}%`;
}
/**
* Convert a screen translation into a page one.
* @param {number} x
* @param {number} y
*/
translate(x, y) {
this.setAt(this.x + x, this.y + y);
screenToPageTranslation(x, y) {
const { rotation } = this.parent.viewport;
switch (rotation) {
case 90:
return [y, -x];
case 180:
return [-x, -y];
case 270:
return [-y, x];
default:
return [x, y];
}
}
/**
@ -154,8 +181,9 @@ class AnnotationEditor {
* @param {number} height
*/
setDims(width, height) {
this.div.style.width = `${width}px`;
this.div.style.height = `${height}px`;
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
this.div.style.width = `${(100 * width) / parentWidth}%`;
this.div.style.height = `${(100 * height) / parentHeight}%`;
}
/**
@ -172,54 +200,68 @@ class AnnotationEditor {
*/
render() {
this.div = document.createElement("div");
this.div.setAttribute("data-editor-rotation", (360 - this.rotation) % 360);
this.div.className = this.name;
this.div.setAttribute("id", this.id);
this.div.tabIndex = 100;
const [tx, ty] = this.getInitialTranslation();
this.x = Math.round(this.x + tx);
this.y = Math.round(this.y + ty);
this.translate(tx, ty);
this.div.style.left = `${this.x}px`;
this.div.style.top = `${this.y}px`;
bindEvents(this, this.div, [
"dragstart",
"focusin",
"focusout",
"mousedown",
]);
bindEvents(this, this.div, ["dragstart", "focusin", "focusout"]);
return this.div;
}
getRect(tx, ty) {
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
const [pageWidth, pageHeight] = this.parent.pageDimensions;
const shiftX = (pageWidth * tx) / parentWidth;
const shiftY = (pageHeight * ty) / parentHeight;
const x = this.x * pageWidth;
const y = this.y * pageHeight;
const width = this.width * pageWidth;
const height = this.height * pageHeight;
switch (this.rotation) {
case 0:
return [
x + shiftX,
pageHeight - y - shiftY - height,
x + shiftX + width,
pageHeight - y - shiftY,
];
case 90:
return [
x + shiftY,
pageHeight - y + shiftX,
x + shiftY + height,
pageHeight - y + shiftX + width,
];
case 180:
return [
x - shiftX - width,
pageHeight - y + shiftY,
x - shiftX,
pageHeight - y + shiftY + height,
];
case 270:
return [
x - shiftY - height,
pageHeight - y - shiftX - width,
x - shiftY,
pageHeight - y - shiftX,
];
default:
throw new Error("Invalid rotation");
}
}
/**
* Executed once this editor has been rendered.
*/
onceAdded() {}
/**
* Apply the current transform (zoom) to this editor.
* @param {Array<number>} transform
*/
transform(transform) {
const { style } = this.div;
const width = parseFloat(style.width);
const height = parseFloat(style.height);
const [x1, y1] = Util.applyTransform([this.x, this.y], transform);
if (!Number.isNaN(width)) {
const [x2] = Util.applyTransform([this.x + width, 0], transform);
this.div.style.width = `${x2 - x1}px`;
}
if (!Number.isNaN(height)) {
const [, y2] = Util.applyTransform([0, this.y + height], transform);
this.div.style.height = `${y2 - y1}px`;
}
this.setAt(x1, y1);
}
/**
* Check if the editor contains something.
* @returns {boolean}

View file

@ -17,7 +17,6 @@ import {
AnnotationEditorType,
assert,
LINE_FACTOR,
Util,
} from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js";
import { bindEvents } from "./tools.js";
@ -72,11 +71,12 @@ 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,
y: this.y,
x: this.x * width,
y: this.y * height,
});
editor.width = this.width;
@ -180,9 +180,10 @@ class FreeTextEditor extends AnnotationEditor {
this.#contentHTML = this.editorDiv.innerHTML;
this.#content = this.#extractText().trimEnd();
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
const style = getComputedStyle(this.div);
this.width = parseFloat(style.width);
this.height = parseFloat(style.height);
this.width = parseFloat(style.width) / parentWidth;
this.height = parseFloat(style.height) / parentHeight;
}
/** @inheritdoc */
@ -205,6 +206,12 @@ class FreeTextEditor extends AnnotationEditor {
return this.div;
}
let baseX, baseY;
if (this.width) {
baseX = this.x;
baseY = this.y;
}
super.render();
this.editorDiv = document.createElement("div");
this.editorDiv.tabIndex = 0;
@ -232,7 +239,13 @@ class FreeTextEditor extends AnnotationEditor {
if (this.width) {
// This editor was created in using copy (ctrl+c).
this.setAt(this.x + this.width, this.y + this.height);
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
this.setAt(
baseX * parentWidth,
baseY * parentHeight,
this.width * parentWidth,
this.height * parentHeight
);
// eslint-disable-next-line no-unsanitized/property
this.editorDiv.innerHTML = this.#contentHTML;
}
@ -242,24 +255,17 @@ class FreeTextEditor extends AnnotationEditor {
/** @inheritdoc */
serialize() {
const rect = this.editorDiv.getBoundingClientRect();
const padding = FreeTextEditor._internalPadding * this.parent.scaleFactor;
const [x1, y1] = Util.applyTransform(
[this.x + padding, this.y + padding + rect.height],
this.parent.inverseViewportTransform
);
const rect = this.getRect(padding, padding);
const [x2, y2] = Util.applyTransform(
[this.x + padding + rect.width, this.y + padding],
this.parent.inverseViewportTransform
);
return {
annotationType: AnnotationEditorType.FREETEXT,
color: [0, 0, 0],
fontSize: this.#fontSize,
value: this.#content,
pageIndex: this.parent.pageIndex,
rect: [x1, y1, x2, y2],
rect,
rotation: this.rotation,
};
}
}

View file

@ -39,6 +39,10 @@ class InkEditor extends AnnotationEditor {
#observer = null;
#realWidth = 0;
#realHeight = 0;
constructor(params) {
super({ ...params, name: "inkEditor" });
this.color = params.color || "CanvasText";
@ -79,6 +83,8 @@ class InkEditor extends AnnotationEditor {
editor.#baseWidth = this.#baseWidth;
editor.#baseHeight = this.#baseHeight;
editor.#disableEditing = this.#disableEditing;
editor.#realWidth = this.#realWidth;
editor.#realHeight = this.#realHeight;
return editor;
}
@ -135,7 +141,7 @@ class InkEditor extends AnnotationEditor {
/** @inheritdoc */
disableEditMode() {
if (!this.isInEditMode()) {
if (!this.isInEditMode() || this.canvas === null) {
return;
}
@ -159,6 +165,20 @@ class InkEditor extends AnnotationEditor {
return this.paths.length === 0;
}
#getInitialBBox() {
const { width, height, rotation } = this.parent.viewport;
switch (rotation) {
case 90:
return [0, width, width, height];
case 180:
return [width, height, width, height];
case 270:
return [height, 0, width, height];
default:
return [0, 0, width, height];
}
}
/**
* Set line styles.
*/
@ -257,9 +277,10 @@ class InkEditor extends AnnotationEditor {
return;
}
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
const { ctx, height, width } = this;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, width, height);
ctx.clearRect(0, 0, width * parentWidth, height * parentHeight);
this.#updateTransform();
for (const path of this.bezierPath2D) {
ctx.stroke(path);
@ -390,15 +411,30 @@ class InkEditor extends AnnotationEditor {
return this.div;
}
super.render();
this.#createCanvas();
let baseX, baseY;
if (this.width) {
baseX = this.x;
baseY = this.y;
}
super.render();
this.div.classList.add("editing");
const [x, y, w, h] = this.#getInitialBBox();
this.setAt(x, y, 0, 0);
this.setDims(w, h);
this.#createCanvas();
if (this.width) {
// This editor was created in using copy (ctrl+c).
this.setAt(this.x + this.width, this.y + this.height);
this.setDims(this.width, this.height);
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
this.setAt(
baseX * parentWidth,
baseY * parentHeight,
this.width * parentWidth,
this.height * parentHeight
);
this.setDims(this.width * parentWidth, this.height * parentHeight);
this.#setCanvasDims();
this.#redraw();
this.div.classList.add("disabled");
@ -410,8 +446,9 @@ class InkEditor extends AnnotationEditor {
}
#setCanvasDims() {
this.canvas.width = this.width;
this.canvas.height = this.height;
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
this.canvas.width = this.width * parentWidth;
this.canvas.height = this.height * parentHeight;
this.#updateTransform();
}
@ -423,19 +460,28 @@ class InkEditor extends AnnotationEditor {
* @returns
*/
setDimensions(width, height) {
if (this.width === width && this.height === height) {
const roundedWidth = Math.round(width);
const roundedHeight = Math.round(height);
if (
this.#realWidth === roundedWidth &&
this.#realHeight === roundedHeight
) {
return;
}
this.#realWidth = roundedWidth;
this.#realHeight = roundedHeight;
this.canvas.style.visibility = "hidden";
if (this.#aspectRatio) {
height = Math.ceil(width / this.#aspectRatio);
this.div.style.height = `${height}px`;
this.setDims(width, height);
}
this.width = width;
this.height = height;
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
this.width = width / parentWidth;
this.height = height / parentHeight;
if (this.#disableEditing) {
const padding = this.#getPadding();
@ -682,8 +728,9 @@ class InkEditor extends AnnotationEditor {
const width = Math.ceil(padding + this.#baseWidth * this.scaleFactor);
const height = Math.ceil(padding + this.#baseHeight * this.scaleFactor);
this.width = width;
this.height = height;
const [parentWidth, parentHeight] = this.parent.viewportBaseDimensions;
this.width = width / parentWidth;
this.height = height / parentHeight;
this.#aspectRatio = width / height;
@ -704,16 +751,9 @@ class InkEditor extends AnnotationEditor {
/** @inheritdoc */
serialize() {
const rect = this.div.getBoundingClientRect();
const [x1, y1] = Util.applyTransform(
[this.x, this.y + rect.height],
this.parent.inverseViewportTransform
);
const [x2, y2] = Util.applyTransform(
[this.x + rect.width, this.y],
this.parent.inverseViewportTransform
);
const rect = this.getRect(0, 0);
const height =
this.rotation % 180 === 0 ? rect[3] - rect[1] : rect[2] - rect[0];
return {
annotationType: AnnotationEditorType.INK,
@ -723,10 +763,11 @@ class InkEditor extends AnnotationEditor {
this.scaleFactor / this.parent.scaleFactor,
this.translationX,
this.translationY,
y2 - y1
height
),
pageIndex: this.parent.pageIndex,
rect: [x1, y1, x2, y2],
rect,
rotation: this.rotation,
};
}
}