1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 14:48:08 +02:00

Merge pull request #19093 from calixteman/refactor_ink

[Editor] Add a new base class to allow to add a drawing in the SVG layer.
This commit is contained in:
calixteman 2024-11-28 16:54:21 +01:00 committed by GitHub
commit ee0df62bc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 2560 additions and 1377 deletions

View file

@ -4466,7 +4466,7 @@ class InkAnnotation extends MarkupAnnotation {
ink.set("Subtype", Name.get("Ink"));
ink.set("CreationDate", `D:${getModificationDate()}`);
ink.set("Rect", rect);
ink.set("InkList", outlines?.points || paths.map(p => p.points));
ink.set("InkList", outlines?.points || paths.points);
ink.set("F", 4);
ink.set("Rotate", rotation);
@ -4523,28 +4523,29 @@ class InkAnnotation extends MarkupAnnotation {
appearanceBuffer.push("/R0 gs");
}
const buffer = [];
for (const { bezier } of paths) {
buffer.length = 0;
buffer.push(
`${numberToString(bezier[0])} ${numberToString(bezier[1])} m`
);
if (bezier.length === 2) {
buffer.push(
`${numberToString(bezier[0])} ${numberToString(bezier[1])} l S`
);
} else {
for (let i = 2, ii = bezier.length; i < ii; i += 6) {
const curve = bezier
.slice(i, i + 6)
.map(numberToString)
.join(" ");
buffer.push(`${curve} c`);
for (const outline of paths.lines) {
for (let i = 0, ii = outline.length; i < ii; i += 6) {
if (isNaN(outline[i])) {
appearanceBuffer.push(
`${numberToString(outline[i + 4])} ${numberToString(
outline[i + 5]
)} m`
);
} else {
const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6);
appearanceBuffer.push(
[c1x, c1y, c2x, c2y, x, y].map(numberToString).join(" ") + " c"
);
}
buffer.push("S");
}
appearanceBuffer.push(buffer.join("\n"));
if (outline.length === 6) {
appearanceBuffer.push(
`${numberToString(outline[4])} ${numberToString(outline[5])} l`
);
}
}
appearanceBuffer.push("S");
const appearance = appearanceBuffer.join("\n");
const appearanceStreamDict = new Dict(xref);
@ -4587,18 +4588,17 @@ class InkAnnotation extends MarkupAnnotation {
`${numberToString(outline[4])} ${numberToString(outline[5])} m`
);
for (let i = 6, ii = outline.length; i < ii; i += 6) {
if (isNaN(outline[i]) || outline[i] === null) {
if (isNaN(outline[i])) {
appearanceBuffer.push(
`${numberToString(outline[i + 4])} ${numberToString(
outline[i + 5]
)} l`
);
} else {
const curve = outline
.slice(i, i + 6)
.map(numberToString)
.join(" ");
appearanceBuffer.push(`${curve} c`);
const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6);
appearanceBuffer.push(
[c1x, c1y, c2x, c2y, x, y].map(numberToString).join(" ") + " c"
);
}
}
appearanceBuffer.push("h f");

View file

@ -183,11 +183,18 @@ class DrawLayer {
this.updateProperties(id, properties);
}
updateProperties(elementOrId, { root, bbox, rootClass, path }) {
updateProperties(elementOrId, properties) {
if (!properties) {
return;
}
const { root, bbox, rootClass, path } = properties;
const element =
typeof elementOrId === "number"
? this.#mapping.get(elementOrId)
: elementOrId;
if (!element) {
return;
}
if (root) {
this.#updateProperties(element, root);
}
@ -207,6 +214,19 @@ class DrawLayer {
}
}
updateParent(id, layer) {
if (layer === this) {
return;
}
const root = this.#mapping.get(id);
if (!root) {
return;
}
layer.#parent.append(root);
this.#mapping.delete(id);
layer.#mapping.set(id, root);
}
remove(id) {
this.#toUpdate.delete(id);
if (this.#parent === null) {

View file

@ -72,10 +72,10 @@ class AnnotationEditorLayer {
#hadPointerDown = false;
#isCleaningUp = false;
#isDisabling = false;
#drawingAC = null;
#textLayer = null;
#textSelectionAC = null;
@ -160,12 +160,9 @@ class AnnotationEditorLayer {
this.disableClick();
return;
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();
this.enableClick();
break;
case AnnotationEditorType.HIGHLIGHT:
this.enableTextSelection();
@ -193,30 +190,6 @@ class AnnotationEditorLayer {
return textLayer === this.#textLayer?.div;
}
addInkEditorIfNeeded(isCommitting) {
if (this.#uiManager.getMode() !== AnnotationEditorType.INK) {
// We don't want to add an ink editor if we're not in ink mode!
return;
}
if (!isCommitting) {
// We're removing an editor but an empty one can already exist so in this
// case we don't need to create a new one.
for (const editor of this.#editors.values()) {
if (editor.isEmpty()) {
editor.setInBackground();
return;
}
}
}
const editor = this.createAndAddNewEditor(
{ offsetX: 0, offsetY: 0 },
/* isCentered = */ false
);
editor.setInBackground();
}
/**
* Set the editing state.
* @param {boolean} isEditing
@ -233,6 +206,10 @@ class AnnotationEditorLayer {
this.#uiManager.addCommands(params);
}
cleanUndoStack(type) {
this.#uiManager.cleanUndoStack(type);
}
toggleDrawing(enabled = false) {
this.div.classList.toggle("drawing", !enabled);
}
@ -482,10 +459,6 @@ class AnnotationEditorLayer {
this.#uiManager.removeEditor(editor);
editor.div.remove();
editor.isAttachedToDOM = false;
if (!this.#isCleaningUp) {
this.addInkEditorIfNeeded(/* isCommitting = */ false);
}
}
/**
@ -766,6 +739,13 @@ class AnnotationEditorLayer {
}
this.#hadPointerDown = false;
if (
this.#currentEditorType?.isDrawer &&
this.#currentEditorType.supportMultipleDrawings
) {
return;
}
if (!this.#allowClick) {
this.#allowClick = true;
return;
@ -808,10 +788,48 @@ class AnnotationEditorLayer {
this.#hadPointerDown = true;
if (this.#currentEditorType?.isDrawer) {
this.startDrawingSession(event);
return;
}
const editor = this.#uiManager.getActive();
this.#allowClick = !editor || editor.isEmpty();
}
startDrawingSession(event) {
this.div.focus();
if (this.#drawingAC) {
this.#currentEditorType.startDrawing(this, this.#uiManager, false, event);
return;
}
this.#uiManager.unselectAll();
this.#drawingAC = new AbortController();
const signal = this.#uiManager.combinedSignal(this.#drawingAC);
this.div.addEventListener(
"blur",
({ relatedTarget }) => {
if (relatedTarget && !this.div.contains(relatedTarget)) {
this.commitOrRemove();
}
},
{ signal }
);
this.#uiManager.disableUserSelect(true);
this.#currentEditorType.startDrawing(this, this.#uiManager, false, event);
}
endDrawingSession() {
if (!this.#drawingAC) {
return;
}
this.#drawingAC.abort();
this.#drawingAC = null;
this.#uiManager.disableUserSelect(false);
this.#currentEditorType.endDrawing();
}
/**
*
* @param {AnnotationEditor} editor
@ -828,10 +846,26 @@ class AnnotationEditorLayer {
return true;
}
commitOrRemove() {
if (this.#drawingAC) {
this.endDrawingSession();
return true;
}
return false;
}
onScaleChanging() {
if (!this.#drawingAC) {
return;
}
this.#currentEditorType.onScaleChangingWhenDrawing(this);
}
/**
* Destroy the main editor.
*/
destroy() {
this.commitOrRemove();
if (this.#uiManager.getActive()?.parent === this) {
// We need to commit the current editor before destroying the layer.
this.#uiManager.commitOrRemove();
@ -858,13 +892,11 @@ class AnnotationEditorLayer {
// When we're cleaning up, some editors are removed but we don't want
// to add a new one which will induce an addition in this.#editors, hence
// an infinite loop.
this.#isCleaningUp = true;
for (const editor of this.#editors.values()) {
if (editor.isEmpty()) {
editor.remove();
}
}
this.#isCleaningUp = false;
}
/**
@ -896,6 +928,7 @@ class AnnotationEditorLayer {
const oldRotation = this.viewport.rotation;
const rotation = viewport.rotation;
this.viewport = viewport;
setLayerDimensions(this.div, { rotation });
if (oldRotation !== rotation) {
@ -903,7 +936,6 @@ class AnnotationEditorLayer {
editor.rotate(rotation);
}
}
this.addInkEditorIfNeeded(/* isCommitting = */ false);
}
/**

852
src/display/editor/draw.js Normal file
View file

@ -0,0 +1,852 @@
/* 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, unreachable } from "../../shared/util.js";
import { noContextMenu, stopEvent } from "../display_utils.js";
import { AnnotationEditor } from "./editor.js";
class DrawingOptions {
#svgProperties = Object.create(null);
updateProperty(name, value) {
this[name] = value;
this.updateSVGProperty(name, value);
}
updateProperties(properties) {
if (!properties) {
return;
}
for (const [name, value] of Object.entries(properties)) {
this.updateProperty(name, value);
}
}
updateSVGProperty(name, value) {
this.#svgProperties[name] = value;
}
toSVGProperties() {
const root = this.#svgProperties;
this.#svgProperties = Object.create(null);
return { root };
}
reset() {
this.#svgProperties = Object.create(null);
}
updateAll(options = this) {
this.updateProperties(options);
}
clone() {
unreachable("Not implemented");
}
}
/**
* Basic draw editor.
*/
class DrawingEditor extends AnnotationEditor {
#drawOutlines = null;
#mustBeCommitted;
_drawId = null;
static _currentDrawId = -1;
static _currentDraw = null;
static _currentDrawingOptions = null;
static _currentParent = null;
static _INNER_MARGIN = 3;
constructor(params) {
super(params);
this.#mustBeCommitted = params.mustBeCommitted || false;
if (params.drawOutlines) {
this.#createDrawOutlines(params);
this.#addToDrawLayer();
}
}
#createDrawOutlines({ drawOutlines, drawId, drawingOptions }) {
this.#drawOutlines = drawOutlines;
this._drawingOptions ||= drawingOptions;
if (drawId >= 0) {
this._drawId = drawId;
// We need to redraw the drawing because we changed the coordinates to be
// in the box coordinate system.
this.parent.drawLayer.finalizeDraw(
drawId,
drawOutlines.defaultProperties
);
} else {
// We create a new drawing.
this._drawId = this.#createDrawing(drawOutlines, this.parent);
}
this.#updateBbox(drawOutlines.box);
}
#createDrawing(drawOutlines, parent) {
const { id } = parent.drawLayer.draw(
DrawingEditor._mergeSVGProperties(
this._drawingOptions.toSVGProperties(),
drawOutlines.defaultSVGProperties
),
/* isPathUpdatable = */ false,
/* hasClip = */ false
);
return id;
}
static _mergeSVGProperties(p1, p2) {
const p1Keys = new Set(Object.keys(p1));
for (const [key, value] of Object.entries(p2)) {
if (p1Keys.has(key)) {
Object.assign(p1[key], value);
} else {
p1[key] = value;
}
}
return p1;
}
/**
* @param {Object} options
* @return {DrawingOptions} the default options to use for a new editor.
*/
static getDefaultDrawingOptions(_options) {
unreachable("Not implemented");
}
/**
* @return {Map<AnnotationEditorParamsType, string>} a map between the
* parameter types and the name of the options.
*/
// eslint-disable-next-line getter-return
static get typesMap() {
unreachable("Not implemented");
}
static get isDrawer() {
return true;
}
/**
* @returns {boolean} `true` if several drawings can be added to the
* annotation.
*/
static get supportMultipleDrawings() {
return false;
}
/** @inheritdoc */
static updateDefaultParams(type, value) {
const propertyName = this.typesMap.get(type);
if (propertyName) {
this._defaultDrawingOptions.updateProperty(propertyName, value);
}
if (this._currentParent) {
this._currentDraw.updateProperty(propertyName, value);
this._currentParent.drawLayer.updateProperties(
this._currentDrawId,
this._defaultDrawingOptions.toSVGProperties()
);
}
}
/** @inheritdoc */
updateParams(type, value) {
const propertyName = this.constructor.typesMap.get(type);
if (propertyName) {
this._updateProperty(type, propertyName, value);
}
}
/** @inheritdoc */
static get defaultPropertiesToUpdate() {
const properties = [];
const options = this._defaultDrawingOptions;
for (const [type, name] of this.typesMap) {
properties.push([type, options[name]]);
}
return properties;
}
/** @inheritdoc */
get propertiesToUpdate() {
const properties = [];
const { _drawingOptions } = this;
for (const [type, name] of this.constructor.typesMap) {
properties.push([type, _drawingOptions[name]]);
}
return properties;
}
/**
* Update a property and make this action undoable.
* @param {string} color
*/
_updateProperty(type, name, value) {
const options = this._drawingOptions;
const savedValue = options[name];
const setter = val => {
options.updateProperty(name, val);
const bbox = this.#drawOutlines.updateProperty(name, val);
if (bbox) {
this.#updateBbox(bbox);
}
this.parent?.drawLayer.updateProperties(
this._drawId,
options.toSVGProperties()
);
};
this.addCommands({
cmd: setter.bind(this, value),
undo: setter.bind(this, savedValue),
post: this._uiManager.updateUI.bind(this._uiManager, this),
mustExec: true,
type,
overwriteIfSameType: true,
keepUndo: true,
});
}
/** @inheritdoc */
_onResizing() {
this.parent?.drawLayer.updateProperties(
this._drawId,
DrawingEditor._mergeSVGProperties(
this.#drawOutlines.getPathResizingSVGProperties(
this.#convertToDrawSpace()
),
{
bbox: this.#rotateBox(),
}
)
);
}
/** @inheritdoc */
_onResized() {
this.parent?.drawLayer.updateProperties(
this._drawId,
DrawingEditor._mergeSVGProperties(
this.#drawOutlines.getPathResizedSVGProperties(
this.#convertToDrawSpace()
),
{
bbox: this.#rotateBox(),
}
)
);
}
/** @inheritdoc */
_onTranslating(x, y) {
this.parent?.drawLayer.updateProperties(this._drawId, {
bbox: this.#rotateBox(x, y),
});
}
/** @inheritdoc */
_onTranslated() {
this.parent?.drawLayer.updateProperties(
this._drawId,
DrawingEditor._mergeSVGProperties(
this.#drawOutlines.getPathTranslatedSVGProperties(
this.#convertToDrawSpace(),
this.parentDimensions
),
{
bbox: this.#rotateBox(),
}
)
);
}
_onStartDragging() {
this.parent?.drawLayer.updateProperties(this._drawId, {
rootClass: {
moving: true,
},
});
}
_onStopDragging() {
this.parent?.drawLayer.updateProperties(this._drawId, {
rootClass: {
moving: false,
},
});
}
/** @inheritdoc */
commit() {
super.commit();
this.disableEditMode();
this.disableEditing();
}
/** @inheritdoc */
disableEditing() {
super.disableEditing();
this.div.classList.toggle("disabled", true);
}
/** @inheritdoc */
enableEditing() {
super.enableEditing();
this.div.classList.toggle("disabled", false);
}
/** @inheritdoc */
getBaseTranslation() {
// The editor itself doesn't have any CSS border (we're drawing one
// ourselves in using SVG).
return [0, 0];
}
/** @inheritdoc */
get isResizable() {
return true;
}
/** @inheritdoc */
onceAdded() {
if (!this.annotationElementId) {
this.parent.addUndoableEditor(this);
}
this._isDraggable = true;
if (this.#mustBeCommitted) {
this.#mustBeCommitted = false;
this.commit();
this.parent.setSelected(this);
this.div.focus();
}
}
/** @inheritdoc */
remove() {
this.#cleanDrawLayer();
super.remove();
}
/** @inheritdoc */
rebuild() {
if (!this.parent) {
return;
}
super.rebuild();
if (this.div === null) {
return;
}
this.#addToDrawLayer();
this.#updateBbox(this.#drawOutlines.box);
if (!this.isAttachedToDOM) {
// At some point this editor was removed and we're rebuilding it,
// hence we must add it to its parent.
this.parent.add(this);
}
}
setParent(parent) {
let mustBeSelected = false;
if (this.parent && !parent) {
this._uiManager.removeShouldRescale(this);
this.#cleanDrawLayer();
} else if (parent) {
this._uiManager.addShouldRescale(this);
this.#addToDrawLayer(parent);
// If mustBeSelected is true it means that this editor was selected
// when its parent has been destroyed, hence we must select it again.
mustBeSelected =
!this.parent && this.div?.classList.contains("selectedEditor");
}
super.setParent(parent);
if (mustBeSelected) {
// We select it after the parent has been set.
this.select();
}
}
#cleanDrawLayer() {
if (this._drawId === null || !this.parent) {
return;
}
this.parent.drawLayer.remove(this._drawId);
this._drawId = null;
// All the SVG properties must be reset in order to make it possible to
// undo.
this._drawingOptions.reset();
}
#addToDrawLayer(parent = this.parent) {
if (this._drawId !== null && this.parent === parent) {
return;
}
if (this._drawId !== null) {
// The parent has changed, we need to move the drawing to the new parent.
this.parent.drawLayer.updateParent(this._drawId, parent.drawLayer);
return;
}
this._drawingOptions.updateAll();
this._drawId = this.#createDrawing(this.#drawOutlines, parent);
}
#convertToParentSpace([x, y, width, height]) {
const {
parentDimensions: [pW, pH],
rotation,
} = this;
switch (rotation) {
case 90:
return [y, 1 - x, width * (pH / pW), height * (pW / pH)];
case 180:
return [1 - x, 1 - y, width, height];
case 270:
return [1 - y, x, width * (pH / pW), height * (pW / pH)];
default:
return [x, y, width, height];
}
}
#convertToDrawSpace() {
const {
x,
y,
width,
height,
parentDimensions: [pW, pH],
rotation,
} = this;
switch (rotation) {
case 90:
return [1 - y, x, width * (pW / pH), height * (pH / pW)];
case 180:
return [1 - x, 1 - y, width, height];
case 270:
return [y, 1 - x, width * (pW / pH), height * (pH / pW)];
default:
return [x, y, width, height];
}
}
#updateBbox(bbox) {
[this.x, this.y, this.width, this.height] =
this.#convertToParentSpace(bbox);
if (this.div) {
this.fixAndSetPosition();
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(this.width * parentWidth, this.height * parentHeight);
}
this._onResized();
}
#rotateBox() {
// We've to deal with two rotations: the rotation of the annotation and the
// rotation of the parent page.
// When the page is rotated, all the layers are just rotated thanks to CSS
// but there is a notable exception: the canvas wrapper.
// The canvas wrapper is not rotated but the dimensions are (or not) swapped
// and the page is redrawn with the rotation applied to the canvas.
// The drawn layer is under the canvas wrapper and is not rotated so we have
// to "manually" rotate the coordinates.
//
// The coordinates (this.x, this.y) correspond to the top-left corner of
// the editor after it has been rotated in the page coordinate system.
const {
x,
y,
width,
height,
rotation,
parentRotation,
parentDimensions: [pW, pH],
} = this;
switch ((rotation * 4 + parentRotation) / 90) {
case 1:
// 0 -> 90
return [1 - y - height, x, height, width];
case 2:
// 0 -> 180
return [1 - x - width, 1 - y - height, width, height];
case 3:
// 0 -> 270
return [y, 1 - x - width, height, width];
case 4:
// 90 -> 0
return [
x,
y - width * (pW / pH),
height * (pH / pW),
width * (pW / pH),
];
case 5:
// 90 -> 90
return [1 - y, x, width * (pW / pH), height * (pH / pW)];
case 6:
// 90 -> 180
return [
1 - x - height * (pH / pW),
1 - y,
height * (pH / pW),
width * (pW / pH),
];
case 7:
// 90 -> 270
return [
y - width * (pW / pH),
1 - x - height * (pH / pW),
width * (pW / pH),
height * (pH / pW),
];
case 8:
// 180 -> 0
return [x - width, y - height, width, height];
case 9:
// 180 -> 90
return [1 - y, x - width, height, width];
case 10:
// 180 -> 180
return [1 - x, 1 - y, width, height];
case 11:
// 180 -> 270
return [y - height, 1 - x, height, width];
case 12:
// 270 -> 0
return [
x - height * (pH / pW),
y,
height * (pH / pW),
width * (pW / pH),
];
case 13:
// 270 -> 90
return [
1 - y - width * (pW / pH),
x - height * (pH / pW),
width * (pW / pH),
height * (pH / pW),
];
case 14:
// 270 -> 180
return [
1 - x,
1 - y - width * (pW / pH),
height * (pH / pW),
width * (pW / pH),
];
case 15:
// 270 -> 270
return [y, 1 - x, width * (pW / pH), height * (pH / pW)];
default:
// 0 -> 0
return [x, y, width, height];
}
}
/** @inheritdoc */
rotate() {
if (!this.parent) {
return;
}
this.parent.drawLayer.updateProperties(
this._drawId,
DrawingEditor._mergeSVGProperties(
{
bbox: this.#rotateBox(),
},
this.#drawOutlines.updateRotation(
(this.parentRotation - this.rotation + 360) % 360
)
)
);
}
onScaleChanging() {
if (!this.parent) {
return;
}
this.#updateBbox(
this.#drawOutlines.updateParentDimensions(
this.parentDimensions,
this.parent.scale
)
);
}
static onScaleChangingWhenDrawing() {}
/** @inheritdoc */
render() {
if (this.div) {
return this.div;
}
const div = super.render();
div.classList.add("draw");
const drawDiv = document.createElement("div");
div.append(drawDiv);
drawDiv.setAttribute("aria-hidden", "true");
drawDiv.className = "internal";
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(this.width * parentWidth, this.height * parentHeight);
this._uiManager.addShouldRescale(this);
this.disableEditing();
return div;
}
/**
* Create a new drawer instance.
* @param {number} x - The x coordinate of the event.
* @param {number} y - The y coordinate of the event.
* @param {number} parentWidth - The parent width.
* @param {number} parentHeight - The parent height.
* @param {number} rotation - The parent rotation.
*/
static createDrawerInstance(_x, _y, _parentWidth, _parentHeight, _rotation) {
unreachable("Not implemented");
}
static startDrawing(
parent,
uiManager,
_isLTR,
{ target, offsetX: x, offsetY: y }
) {
const {
viewport: { rotation },
} = parent;
const { width: parentWidth, height: parentHeight } =
target.getBoundingClientRect();
const ac = new AbortController();
const signal = parent.combinedSignal(ac);
window.addEventListener(
"pointerup",
e => {
ac.abort();
parent.toggleDrawing(true);
this._endDraw(e);
},
{ signal }
);
window.addEventListener(
"pointerdown",
stopEvent /* Avoid to have undesired clicks during drawing. */,
{
capture: true,
passive: false,
signal,
}
);
window.addEventListener("contextmenu", noContextMenu, { signal });
target.addEventListener("pointermove", this._drawMove.bind(this), {
signal,
});
parent.toggleDrawing();
if (this._currentDraw) {
parent.drawLayer.updateProperties(
this._currentDrawId,
this._currentDraw.startNew(x, y, parentWidth, parentHeight, rotation)
);
return;
}
uiManager.updateUIForDefaultProperties(this);
this._currentDraw = this.createDrawerInstance(
x,
y,
parentWidth,
parentHeight,
rotation
);
this._currentDrawingOptions = this.getDefaultDrawingOptions();
this._currentParent = parent;
({ id: this._currentDrawId } = parent.drawLayer.draw(
this._mergeSVGProperties(
this._currentDrawingOptions.toSVGProperties(),
this._currentDraw.defaultSVGProperties
),
/* isPathUpdatable = */ true,
/* hasClip = */ false
));
}
static _drawMove({ offsetX, offsetY }) {
this._currentParent.drawLayer.updateProperties(
this._currentDrawId,
this._currentDraw.add(offsetX, offsetY)
);
}
static _endDraw({ offsetX, offsetY }) {
const parent = this._currentParent;
parent.drawLayer.updateProperties(
this._currentDrawId,
this._currentDraw.end(offsetX, offsetY)
);
if (this.supportMultipleDrawings) {
const draw = this._currentDraw;
const drawId = this._currentDrawId;
const lastElement = draw.getLastElement();
parent.addCommands({
cmd: () => {
parent.drawLayer.updateProperties(
drawId,
draw.setLastElement(lastElement)
);
},
undo: () => {
parent.drawLayer.updateProperties(drawId, draw.removeLastElement());
},
mustExec: false,
type: AnnotationEditorParamsType.DRAW_STEP,
});
return;
}
this.endDrawing();
}
static endDrawing() {
const parent = this._currentParent;
if (!parent) {
return;
}
parent.toggleDrawing(true);
parent.cleanUndoStack(AnnotationEditorParamsType.DRAW_STEP);
if (!this._currentDraw.isEmpty()) {
const {
pageDimensions: [pageWidth, pageHeight],
scale,
} = parent;
parent.createAndAddNewEditor({ offsetX: 0, offsetY: 0 }, false, {
drawId: this._currentDrawId,
drawOutlines: this._currentDraw.getOutlines(
pageWidth * scale,
pageHeight * scale,
scale,
this._INNER_MARGIN
),
drawingOptions: this._currentDrawingOptions,
mustBeCommitted: true,
});
} else {
parent.drawLayer.remove(this._currentDrawId);
}
this._currentDrawId = -1;
this._currentDraw = null;
this._currentDrawingOptions = null;
this._currentParent = null;
}
/**
* Create the drawing options.
* @param {Object} _data
*/
createDrawingOptions(_data) {}
/**
* Deserialize the drawing outlines.
* @param {number} pageX - The x coordinate of the page.
* @param {number} pageY - The y coordinate of the page.
* @param {number} pageWidth - The width of the page.
* @param {number} pageHeight - The height of the page.
* @param {number} innerWidth - The inner width.
* @param {Object} data - The data to deserialize.
* @returns {Object} The deserialized outlines.
*/
static deserializeDraw(
_pageX,
_pageY,
_pageWidth,
_pageHeight,
_innerWidth,
_data
) {
unreachable("Not implemented");
}
/** @inheritdoc */
static async deserialize(data, parent, uiManager) {
const {
rawDims: { pageWidth, pageHeight, pageX, pageY },
} = parent.viewport;
const drawOutlines = this.deserializeDraw(
pageX,
pageY,
pageWidth,
pageHeight,
this._INNER_MARGIN,
data
);
const editor = await super.deserialize(data, parent, uiManager);
editor.createDrawingOptions(data);
editor.#createDrawOutlines({ drawOutlines });
editor.#addToDrawLayer();
editor.onScaleChanging();
editor.rotate();
return editor;
}
serializeDraw(isForCopying) {
const [pageX, pageY] = this.pageTranslation;
const [pageWidth, pageHeight] = this.pageDimensions;
return this.#drawOutlines.serialize(
[pageX, pageY, pageWidth, pageHeight],
isForCopying
);
}
/** @inheritdoc */
renderAnnotationElement(annotation) {
annotation.updateEdited({
rect: this.getRect(0, 0),
});
return null;
}
static canCreateNewEmptyEditor() {
return false;
}
}
export { DrawingEditor, DrawingOptions };

View file

@ -0,0 +1,853 @@
/* Copyright 2024 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 { Outline } from "./outline.js";
import { Util } from "../../../shared/util.js";
class InkDrawOutliner {
// The last 3 points of the line.
#last = new Float64Array(6);
#line;
#lines;
#rotation;
#thickness;
#points;
#lastSVGPath = "";
#lastIndex = 0;
#outlines = new InkDrawOutline();
#parentWidth;
#parentHeight;
constructor(x, y, parentWidth, parentHeight, rotation, thickness) {
this.#parentWidth = parentWidth;
this.#parentHeight = parentHeight;
this.#rotation = rotation;
this.#thickness = thickness;
[x, y] = this.#normalizePoint(x, y);
const line = (this.#line = [NaN, NaN, NaN, NaN, x, y]);
this.#points = [x, y];
this.#lines = [{ line, points: this.#points }];
this.#last.set(line, 0);
}
updateProperty(name, value) {
if (name === "stroke-width") {
this.#thickness = value;
}
}
#normalizePoint(x, y) {
return Outline._normalizePoint(
x,
y,
this.#parentWidth,
this.#parentHeight,
this.#rotation
);
}
isEmpty() {
return !this.#lines || this.#lines.length === 0;
}
add(x, y) {
// The point is in canvas coordinates which means that there is no rotation.
// It's the same as parent coordinates.
[x, y] = this.#normalizePoint(x, y);
const [x1, y1, x2, y2] = this.#last.subarray(2, 6);
const diffX = x - x2;
const diffY = y - y2;
const d = Math.hypot(this.#parentWidth * diffX, this.#parentHeight * diffY);
if (d <= 2) {
// The idea is to avoid garbage points around the last point.
// When the points are too close, it just leads to bad normal vectors and
// control points.
return null;
}
this.#points.push(x, y);
if (isNaN(x1)) {
// We've only one point.
this.#last.set([x2, y2, x, y], 2);
this.#line.push(NaN, NaN, NaN, NaN, x, y);
return {
path: {
d: this.toSVGPath(),
},
};
}
if (isNaN(this.#last[0])) {
// We've only two points.
this.#line.splice(6, 6);
}
this.#last.set([x1, y1, x2, y2, x, y], 0);
this.#line.push(
(x1 + 5 * x2) / 6,
(y1 + 5 * y2) / 6,
(5 * x2 + x) / 6,
(5 * y2 + y) / 6,
(x2 + x) / 2,
(y2 + y) / 2
);
return {
path: {
d: this.toSVGPath(),
},
};
}
end(x, y) {
const change = this.add(x, y);
if (change) {
return change;
}
if (this.#points.length === 2) {
// We've only one point.
return {
path: {
d: this.toSVGPath(),
},
};
}
return null;
}
startNew(x, y, parentWidth, parentHeight, rotation) {
this.#parentWidth = parentWidth;
this.#parentHeight = parentHeight;
this.#rotation = rotation;
[x, y] = this.#normalizePoint(x, y);
const line = (this.#line = [NaN, NaN, NaN, NaN, x, y]);
this.#points = [x, y];
const last = this.#lines.at(-1);
if (last) {
last.line = new Float32Array(last.line);
last.points = new Float32Array(last.points);
}
this.#lines.push({ line, points: this.#points });
this.#last.set(line, 0);
this.#lastIndex = 0;
this.toSVGPath();
return null;
}
getLastElement() {
return this.#lines.at(-1);
}
setLastElement(element) {
if (!this.#lines) {
return this.#outlines.setLastElement(element);
}
this.#lines.push(element);
this.#line = element.line;
this.#points = element.points;
this.#lastIndex = 0;
return {
path: {
d: this.toSVGPath(),
},
};
}
removeLastElement() {
if (!this.#lines) {
return this.#outlines.removeLastElement();
}
this.#lines.pop();
this.#lastSVGPath = "";
for (let i = 0, ii = this.#lines.length; i < ii; i++) {
const { line, points } = this.#lines[i];
this.#line = line;
this.#points = points;
this.#lastIndex = 0;
this.toSVGPath();
}
return {
path: {
d: this.#lastSVGPath,
},
};
}
toSVGPath() {
const firstX = Outline.svgRound(this.#line[4]);
const firstY = Outline.svgRound(this.#line[5]);
if (this.#points.length === 2) {
this.#lastSVGPath = `${this.#lastSVGPath} M ${firstX} ${firstY} Z`;
return this.#lastSVGPath;
}
if (this.#points.length <= 6) {
// We've 2 or 3 points.
const i = this.#lastSVGPath.lastIndexOf("M");
this.#lastSVGPath = `${this.#lastSVGPath.slice(0, i)} M ${firstX} ${firstY}`;
this.#lastIndex = 6;
}
if (this.#points.length === 4) {
const secondX = Outline.svgRound(this.#line[10]);
const secondY = Outline.svgRound(this.#line[11]);
this.#lastSVGPath = `${this.#lastSVGPath} L ${secondX} ${secondY}`;
this.#lastIndex = 12;
return this.#lastSVGPath;
}
const buffer = [];
if (this.#lastIndex === 0) {
buffer.push(`M ${firstX} ${firstY}`);
this.#lastIndex = 6;
}
for (let i = this.#lastIndex, ii = this.#line.length; i < ii; i += 6) {
const [c1x, c1y, c2x, c2y, x, y] = this.#line
.slice(i, i + 6)
.map(Outline.svgRound);
buffer.push(`C${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y}`);
}
this.#lastSVGPath += buffer.join(" ");
this.#lastIndex = this.#line.length;
return this.#lastSVGPath;
}
getOutlines(parentWidth, parentHeight, scale, innerMargin) {
const last = this.#lines.at(-1);
last.line = new Float32Array(last.line);
last.points = new Float32Array(last.points);
this.#outlines.build(
this.#lines,
parentWidth,
parentHeight,
scale,
this.#rotation,
this.#thickness,
innerMargin
);
// We reset everything: the drawing is done.
this.#last = null;
this.#line = null;
this.#lines = null;
this.#lastSVGPath = null;
return this.#outlines;
}
get defaultSVGProperties() {
return {
root: {
viewBox: "0 0 10000 10000",
},
rootClass: {
draw: true,
},
bbox: [0, 0, 1, 1],
};
}
}
class InkDrawOutline extends Outline {
#bbox;
#currentRotation = 0;
#innerMargin;
#lines;
#parentWidth;
#parentHeight;
#parentScale;
#rotation;
#thickness;
build(
lines,
parentWidth,
parentHeight,
parentScale,
rotation,
thickness,
innerMargin
) {
this.#parentWidth = parentWidth;
this.#parentHeight = parentHeight;
this.#parentScale = parentScale;
this.#rotation = rotation;
this.#thickness = thickness;
this.#innerMargin = innerMargin ?? 0;
this.#lines = lines;
this.#computeBbox();
}
setLastElement(element) {
this.#lines.push(element);
return {
path: {
d: this.toSVGPath(),
},
};
}
removeLastElement() {
this.#lines.pop();
return {
path: {
d: this.toSVGPath(),
},
};
}
toSVGPath() {
const buffer = [];
for (const { line } of this.#lines) {
buffer.push(`M${Outline.svgRound(line[4])} ${Outline.svgRound(line[5])}`);
if (line.length === 6) {
buffer.push("Z");
continue;
}
if (line.length === 12) {
buffer.push(
`L${Outline.svgRound(line[10])} ${Outline.svgRound(line[11])}`
);
continue;
}
for (let i = 6, ii = line.length; i < ii; i += 6) {
const [c1x, c1y, c2x, c2y, x, y] = line
.subarray(i, i + 6)
.map(Outline.svgRound);
buffer.push(`C${c1x} ${c1y} ${c2x} ${c2y} ${x} ${y}`);
}
}
return buffer.join("");
}
serialize([pageX, pageY, pageWidth, pageHeight], isForCopying) {
const serializedLines = [];
const serializedPoints = [];
const [x, y, width, height] = this.#getBBoxWithNoMargin();
let tx, ty, sx, sy, x1, y1, x2, y2, rescaleFn;
switch (this.#rotation) {
case 0:
rescaleFn = Outline._rescale;
tx = pageX;
ty = pageY + pageHeight;
sx = pageWidth;
sy = -pageHeight;
x1 = pageX + x * pageWidth;
y1 = pageY + (1 - y - height) * pageHeight;
x2 = pageX + (x + width) * pageWidth;
y2 = pageY + (1 - y) * pageHeight;
break;
case 90:
rescaleFn = Outline._rescaleAndSwap;
tx = pageX;
ty = pageY;
sx = pageWidth;
sy = pageHeight;
x1 = pageX + y * pageWidth;
y1 = pageY + x * pageHeight;
x2 = pageX + (y + height) * pageWidth;
y2 = pageY + (x + width) * pageHeight;
break;
case 180:
rescaleFn = Outline._rescale;
tx = pageX + pageWidth;
ty = pageY;
sx = -pageWidth;
sy = pageHeight;
x1 = pageX + (1 - x - width) * pageWidth;
y1 = pageY + y * pageHeight;
x2 = pageX + (1 - x) * pageWidth;
y2 = pageY + (y + height) * pageHeight;
break;
case 270:
rescaleFn = Outline._rescaleAndSwap;
tx = pageX + pageWidth;
ty = pageY + pageHeight;
sx = -pageWidth;
sy = -pageHeight;
x1 = pageX + (1 - y - height) * pageWidth;
y1 = pageY + (1 - x - width) * pageHeight;
x2 = pageX + (1 - y) * pageWidth;
y2 = pageY + (1 - x) * pageHeight;
break;
}
for (const { line, points } of this.#lines) {
serializedLines.push(
rescaleFn(
line,
tx,
ty,
sx,
sy,
isForCopying ? new Array(line.length) : null
)
);
serializedPoints.push(
rescaleFn(
points,
tx,
ty,
sx,
sy,
isForCopying ? new Array(points.length) : null
)
);
}
return {
lines: serializedLines,
points: serializedPoints,
rect: [x1, y1, x2, y2],
};
}
static deserialize(
pageX,
pageY,
pageWidth,
pageHeight,
innerMargin,
{ paths: { lines, points }, rotation, thickness }
) {
const newLines = [];
let tx, ty, sx, sy, rescaleFn;
switch (rotation) {
case 0:
rescaleFn = Outline._rescale;
tx = -pageX / pageWidth;
ty = pageY / pageHeight + 1;
sx = 1 / pageWidth;
sy = -1 / pageHeight;
break;
case 90:
rescaleFn = Outline._rescaleAndSwap;
tx = -pageY / pageHeight;
ty = -pageX / pageWidth;
sx = 1 / pageHeight;
sy = 1 / pageWidth;
break;
case 180:
rescaleFn = Outline._rescale;
tx = pageX / pageWidth + 1;
ty = -pageY / pageHeight;
sx = -1 / pageWidth;
sy = 1 / pageHeight;
break;
case 270:
rescaleFn = Outline._rescaleAndSwap;
tx = pageY / pageHeight + 1;
ty = pageX / pageWidth + 1;
sx = -1 / pageHeight;
sy = -1 / pageWidth;
break;
}
for (let i = 0, ii = lines.length; i < ii; i++) {
newLines.push({
line: rescaleFn(
lines[i].map(x => x ?? NaN),
tx,
ty,
sx,
sy
),
points: rescaleFn(
points[i].map(x => x ?? NaN),
tx,
ty,
sx,
sy
),
});
}
const outlines = new InkDrawOutline();
outlines.build(
newLines,
pageWidth,
pageHeight,
1,
rotation,
thickness,
innerMargin
);
return outlines;
}
#getMarginComponents(thickness = this.#thickness) {
const margin = this.#innerMargin + (thickness / 2) * this.#parentScale;
return this.#rotation % 180 === 0
? [margin / this.#parentWidth, margin / this.#parentHeight]
: [margin / this.#parentHeight, margin / this.#parentWidth];
}
#getBBoxWithNoMargin() {
const [x, y, width, height] = this.#bbox;
const [marginX, marginY] = this.#getMarginComponents(0);
return [
x + marginX,
y + marginY,
width - 2 * marginX,
height - 2 * marginY,
];
}
#computeBbox() {
const bbox = (this.#bbox = new Float32Array([
Infinity,
Infinity,
-Infinity,
-Infinity,
]));
for (const { line } of this.#lines) {
if (line.length <= 12) {
// We've only one or two points => no bezier curve.
for (let i = 4, ii = line.length; i < ii; i += 6) {
const [x, y] = line.subarray(i, i + 2);
bbox[0] = Math.min(bbox[0], x);
bbox[1] = Math.min(bbox[1], y);
bbox[2] = Math.max(bbox[2], x);
bbox[3] = Math.max(bbox[3], y);
}
continue;
}
let lastX = line[4],
lastY = line[5];
for (let i = 6, ii = line.length; i < ii; i += 6) {
const [c1x, c1y, c2x, c2y, x, y] = line.subarray(i, i + 6);
Util.bezierBoundingBox(lastX, lastY, c1x, c1y, c2x, c2y, x, y, bbox);
lastX = x;
lastY = y;
}
}
const [marginX, marginY] = this.#getMarginComponents();
bbox[0] = Math.min(1, Math.max(0, bbox[0] - marginX));
bbox[1] = Math.min(1, Math.max(0, bbox[1] - marginY));
bbox[2] = Math.min(1, Math.max(0, bbox[2] + marginX));
bbox[3] = Math.min(1, Math.max(0, bbox[3] + marginY));
bbox[2] -= bbox[0];
bbox[3] -= bbox[1];
}
get box() {
return this.#bbox;
}
updateProperty(name, value) {
if (name === "stroke-width") {
return this.#updateThickness(value);
}
return null;
}
#updateThickness(thickness) {
const [oldMarginX, oldMarginY] = this.#getMarginComponents();
this.#thickness = thickness;
const [newMarginX, newMarginY] = this.#getMarginComponents();
const [diffMarginX, diffMarginY] = [
newMarginX - oldMarginX,
newMarginY - oldMarginY,
];
const bbox = this.#bbox;
bbox[0] -= diffMarginX;
bbox[1] -= diffMarginY;
bbox[2] += 2 * diffMarginX;
bbox[3] += 2 * diffMarginY;
return bbox;
}
updateParentDimensions([width, height], scale) {
const [oldMarginX, oldMarginY] = this.#getMarginComponents();
this.#parentWidth = width;
this.#parentHeight = height;
this.#parentScale = scale;
const [newMarginX, newMarginY] = this.#getMarginComponents();
const diffMarginX = newMarginX - oldMarginX;
const diffMarginY = newMarginY - oldMarginY;
const bbox = this.#bbox;
bbox[0] -= diffMarginX;
bbox[1] -= diffMarginY;
bbox[2] += 2 * diffMarginX;
bbox[3] += 2 * diffMarginY;
return bbox;
}
updateRotation(rotation) {
this.#currentRotation = rotation;
return {
path: {
transform: this.rotationTransform,
},
};
}
get viewBox() {
return this.#bbox.map(Outline.svgRound).join(" ");
}
get defaultProperties() {
const [x, y] = this.#bbox;
return {
root: {
viewBox: this.viewBox,
},
path: {
"transform-origin": `${Outline.svgRound(x)} ${Outline.svgRound(y)}`,
},
};
}
get rotationTransform() {
const [, , width, height] = this.#bbox;
let a = 0,
b = 0,
c = 0,
d = 0,
e = 0,
f = 0;
switch (this.#currentRotation) {
case 90:
b = height / width;
c = -width / height;
e = width;
break;
case 180:
a = -1;
d = -1;
e = width;
f = height;
break;
case 270:
b = -height / width;
c = width / height;
f = height;
break;
default:
return "";
}
return `matrix(${a} ${b} ${c} ${d} ${Outline.svgRound(e)} ${Outline.svgRound(f)})`;
}
getPathResizingSVGProperties([newX, newY, newWidth, newHeight]) {
const [marginX, marginY] = this.#getMarginComponents();
const [x, y, width, height] = this.#bbox;
if (
Math.abs(width - marginX) <= Outline.PRECISION ||
Math.abs(height - marginY) <= Outline.PRECISION
) {
// Center the path in the new bounding box.
const tx = newX + newWidth / 2 - (x + width / 2);
const ty = newY + newHeight / 2 - (y + height / 2);
return {
path: {
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
transform: `${this.rotationTransform} translate(${tx} ${ty})`,
},
};
}
// We compute the following transform:
// 1. Translate the path to the origin (-marginX, -marginY).
// 2. Scale the path to the new size:
// ((newWidth - 2*marginX) / (bbox.width - 2*marginX),
// (newHeight - 2*marginY) / (bbox.height - 2*marginY)).
// 3. Translate the path back to its original position
// (marginX, marginY).
// 4. Scale the inverse of bbox scaling:
// (bbox.width / newWidth, bbox.height / newHeight).
const s1x = (newWidth - 2 * marginX) / (width - 2 * marginX);
const s1y = (newHeight - 2 * marginY) / (height - 2 * marginY);
const s2x = width / newWidth;
const s2y = height / newHeight;
return {
path: {
"transform-origin": `${Outline.svgRound(x)} ${Outline.svgRound(y)}`,
transform:
`${this.rotationTransform} scale(${s2x} ${s2y}) ` +
`translate(${Outline.svgRound(marginX)} ${Outline.svgRound(marginY)}) scale(${s1x} ${s1y}) ` +
`translate(${Outline.svgRound(-marginX)} ${Outline.svgRound(-marginY)})`,
},
};
}
getPathResizedSVGProperties([newX, newY, newWidth, newHeight]) {
const [marginX, marginY] = this.#getMarginComponents();
const bbox = this.#bbox;
const [x, y, width, height] = bbox;
bbox[0] = newX;
bbox[1] = newY;
bbox[2] = newWidth;
bbox[3] = newHeight;
if (
Math.abs(width - marginX) <= Outline.PRECISION ||
Math.abs(height - marginY) <= Outline.PRECISION
) {
// Center the path in the new bounding box.
const tx = newX + newWidth / 2 - (x + width / 2);
const ty = newY + newHeight / 2 - (y + height / 2);
for (const { line, points } of this.#lines) {
Outline._translate(line, tx, ty, line);
Outline._translate(points, tx, ty, points);
}
return {
root: {
viewBox: this.viewBox,
},
path: {
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
transform: this.rotationTransform || null,
d: this.toSVGPath(),
},
};
}
// We compute the following transform:
// 1. Translate the path to the origin (-(x + marginX), -(y + marginY)).
// 2. Scale the path to the new size:
// ((newWidth - 2*marginX) / (bbox.width - 2*marginX),
// (newHeight - 2*marginY) / (bbox.height - 2*marginY)).
// 3. Translate the path back to its new position
// (newX + marginX,y newY + marginY).
const s1x = (newWidth - 2 * marginX) / (width - 2 * marginX);
const s1y = (newHeight - 2 * marginY) / (height - 2 * marginY);
const tx = -s1x * (x + marginX) + newX + marginX;
const ty = -s1y * (y + marginY) + newY + marginY;
if (s1x !== 1 || s1y !== 1 || tx !== 0 || ty !== 0) {
for (const { line, points } of this.#lines) {
Outline._rescale(line, tx, ty, s1x, s1y, line);
Outline._rescale(points, tx, ty, s1x, s1y, points);
}
}
return {
root: {
viewBox: this.viewBox,
},
path: {
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
transform: this.rotationTransform || null,
d: this.toSVGPath(),
},
};
}
getPathTranslatedSVGProperties([newX, newY], parentDimensions) {
const [newParentWidth, newParentHeight] = parentDimensions;
const bbox = this.#bbox;
const tx = newX - bbox[0];
const ty = newY - bbox[1];
if (
this.#parentWidth === newParentWidth &&
this.#parentHeight === newParentHeight
) {
// We don't change the parent dimensions so it's a simple translation.
for (const { line, points } of this.#lines) {
Outline._translate(line, tx, ty, line);
Outline._translate(points, tx, ty, points);
}
} else {
const sx = this.#parentWidth / newParentWidth;
const sy = this.#parentHeight / newParentHeight;
this.#parentWidth = newParentWidth;
this.#parentHeight = newParentHeight;
for (const { line, points } of this.#lines) {
Outline._rescale(line, tx, ty, sx, sy, line);
Outline._rescale(points, tx, ty, sx, sy, points);
}
bbox[2] *= sx;
bbox[3] *= sy;
}
bbox[0] = newX;
bbox[1] = newY;
return {
root: {
viewBox: this.viewBox,
},
path: {
d: this.toSVGPath(),
"transform-origin": `${Outline.svgRound(newX)} ${Outline.svgRound(newY)}`,
},
};
}
get defaultSVGProperties() {
const bbox = this.#bbox;
return {
root: {
viewBox: this.viewBox,
},
rootClass: {
draw: true,
},
path: {
d: this.toSVGPath(),
"transform-origin": `${Outline.svgRound(bbox[0])} ${Outline.svgRound(bbox[1])}`,
transform: this.rotationTransform || null,
},
bbox,
};
}
}
export { InkDrawOutline, InkDrawOutliner };

View file

@ -16,6 +16,8 @@
import { unreachable } from "../../../shared/util.js";
class Outline {
static PRECISION = 1e-4;
/**
* @returns {string} The SVG path of the outline.
*/
@ -52,6 +54,49 @@ class Outline {
}
return dest;
}
static _translate(src, tx, ty, dest) {
dest ||= new Float32Array(src.length);
for (let i = 0, ii = src.length; i < ii; i += 2) {
dest[i] = tx + src[i];
dest[i + 1] = ty + src[i + 1];
}
return dest;
}
static svgRound(x) {
// 0.1234 will be 1234 and this way we economize 2 bytes per number.
// Of course, it makes sense only when the viewBox is [0 0 10000 10000].
// And it helps to avoid bugs like:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1929340
return Math.round(x * 10000);
}
static _normalizePoint(x, y, parentWidth, parentHeight, rotation) {
switch (rotation) {
case 90:
return [1 - y / parentWidth, x / parentHeight];
case 180:
return [1 - x / parentWidth, 1 - y / parentHeight];
case 270:
return [y / parentWidth, 1 - x / parentHeight];
default:
return [x / parentWidth, y / parentHeight];
}
}
static _normalizePagePoint(x, y, rotation) {
switch (rotation) {
case 90:
return [1 - y, x];
case 180:
return [1 - x, 1 - y];
case 270:
return [y, 1 - x];
default:
return [x, y];
}
}
}
export { Outline };

View file

@ -192,6 +192,10 @@ class AnnotationEditor {
return Object.getPrototypeOf(this).constructor._type;
}
static get isDrawer() {
return false;
}
static get _defaultLineColor() {
return shadow(
this,
@ -441,6 +445,8 @@ class AnnotationEditor {
this.x += x / width;
this.y += y / height;
this._onTranslating(this.x, this.y);
this.fixAndSetPosition();
}
@ -469,7 +475,10 @@ class AnnotationEditor {
drag(tx, ty) {
this.#initialPosition ||= [this.x, this.y];
const [parentWidth, parentHeight] = this.parentDimensions;
const {
div,
parentDimensions: [parentWidth, parentHeight],
} = this;
this.x += tx / parentWidth;
this.y += ty / parentHeight;
if (this.parent && (this.x < 0 || this.x > 1 || this.y < 0 || this.y > 1)) {
@ -496,11 +505,29 @@ class AnnotationEditor {
x += bx;
y += by;
this.div.style.left = `${(100 * x).toFixed(2)}%`;
this.div.style.top = `${(100 * y).toFixed(2)}%`;
this.div.scrollIntoView({ block: "nearest" });
const { style } = div;
style.left = `${(100 * x).toFixed(2)}%`;
style.top = `${(100 * y).toFixed(2)}%`;
this._onTranslating(x, y);
div.scrollIntoView({ block: "nearest" });
}
/**
* Called when the editor is being translated.
* @param {number} x - in page coordinates.
* @param {number} y - in page coordinates.
*/
_onTranslating(x, y) {}
/**
* Called when the editor has been translated.
* @param {number} x - in page coordinates.
* @param {number} y - in page coordinates.
*/
_onTranslated(x, y) {}
get _hasBeenMoved() {
return (
!!this.#initialPosition &&
@ -546,7 +573,10 @@ class AnnotationEditor {
* @param {number} [rotation] - the rotation of the page.
*/
fixAndSetPosition(rotation = this.rotation) {
const [pageWidth, pageHeight] = this.pageDimensions;
const {
div: { style },
pageDimensions: [pageWidth, pageHeight],
} = this;
let { x, y, width, height } = this;
width *= pageWidth;
height *= pageHeight;
@ -581,7 +611,6 @@ class AnnotationEditor {
x += bx;
y += by;
const { style } = this.div;
style.left = `${(100 * x).toFixed(2)}%`;
style.top = `${(100 * y).toFixed(2)}%`;
@ -659,9 +688,10 @@ class AnnotationEditor {
*/
setDims(width, height) {
const [parentWidth, parentHeight] = this.parentDimensions;
this.div.style.width = `${((100 * width) / parentWidth).toFixed(2)}%`;
const { style } = this.div;
style.width = `${((100 * width) / parentWidth).toFixed(2)}%`;
if (!this.#keepAspectRatio) {
this.div.style.height = `${((100 * height) / parentHeight).toFixed(2)}%`;
style.height = `${((100 * height) / parentHeight).toFixed(2)}%`;
}
}
@ -679,9 +709,7 @@ class AnnotationEditor {
style.width = `${((100 * parseFloat(width)) / parentWidth).toFixed(2)}%`;
}
if (!this.#keepAspectRatio && !heightPercent) {
style.height = `${((100 * parseFloat(height)) / parentHeight).toFixed(
2
)}%`;
style.height = `${((100 * parseFloat(height)) / parentHeight).toFixed(2)}%`;
}
}
@ -759,10 +787,12 @@ class AnnotationEditor {
{ passive: false, signal }
);
window.addEventListener("contextmenu", noContextMenu, { signal });
const savedX = this.x;
const savedY = this.y;
const savedWidth = this.width;
const savedHeight = this.height;
this.#savedDimensions = {
savedX: this.x,
savedY: this.y,
savedWidth: this.width,
savedHeight: this.height,
};
const savedParentCursor = this.parent.div.style.cursor;
const savedCursor = this.div.style.cursor;
this.div.style.cursor = this.parent.div.style.cursor =
@ -776,7 +806,7 @@ class AnnotationEditor {
this.parent.div.style.cursor = savedParentCursor;
this.div.style.cursor = savedCursor;
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
this.#addResizeToUndoStack();
};
window.addEventListener("pointerup", pointerUpCallback, { signal });
// If the user switches to another window (with alt+tab), then we end the
@ -784,7 +814,29 @@ class AnnotationEditor {
window.addEventListener("blur", pointerUpCallback, { signal });
}
#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight) {
#resize(x, y, width, height) {
this.width = width;
this.height = height;
this.x = x;
this.y = y;
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(parentWidth * width, parentHeight * height);
this.fixAndSetPosition();
this._onResized();
}
/**
* Called when the editor has been resized.
*/
_onResized() {}
#addResizeToUndoStack() {
if (!this.#savedDimensions) {
return;
}
const { savedX, savedY, savedWidth, savedHeight } = this.#savedDimensions;
this.#savedDimensions = null;
const newX = this.x;
const newY = this.y;
const newWidth = this.width;
@ -799,24 +851,8 @@ class AnnotationEditor {
}
this.addCommands({
cmd: () => {
this.width = newWidth;
this.height = newHeight;
this.x = newX;
this.y = newY;
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
this.fixAndSetPosition();
},
undo: () => {
this.width = savedWidth;
this.height = savedHeight;
this.x = savedX;
this.y = savedY;
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(parentWidth * savedWidth, parentHeight * savedHeight);
this.fixAndSetPosition();
},
cmd: this.#resize.bind(this, newX, newY, newWidth, newHeight),
undo: this.#resize.bind(this, savedX, savedY, savedWidth, savedHeight),
mustExec: true,
});
}
@ -960,8 +996,15 @@ class AnnotationEditor {
this.setDims(parentWidth * newWidth, parentHeight * newHeight);
this.fixAndSetPosition();
this._onResizing();
}
/**
* Called when the editor is being resized.
*/
_onResizing() {}
/**
* Called when the alt text dialog is closed.
*/
@ -1194,9 +1237,12 @@ class AnnotationEditor {
);
}
this._onStartDragging();
const pointerUpCallback = e => {
if (!this.#dragPointerId || this.#dragPointerId === e.pointerId) {
cancelDrag(e);
this._onStopDragging();
return;
}
stopEvent(e);
@ -1208,6 +1254,10 @@ class AnnotationEditor {
window.addEventListener("blur", pointerUpCallback, { signal });
}
_onStartDragging() {}
_onStopDragging() {}
moveInDOM() {
// Moving the editor in the DOM can be expensive, so we wait a bit before.
// It's important to not block the UI (for example when changing the font
@ -1226,6 +1276,7 @@ class AnnotationEditor {
this.x = x;
this.y = y;
this.fixAndSetPosition();
this._onTranslated();
}
/**
@ -1372,11 +1423,16 @@ class AnnotationEditor {
}
/**
* Rotate the editor.
* Rotate the editor when the page is rotated.
* @param {number} angle
*/
rotate(_angle) {}
/**
* Resize the editor when the page is resized.
*/
resize() {}
/**
* Serialize the editor when it has been deleted.
* @returns {Object}
@ -1622,11 +1678,7 @@ class AnnotationEditor {
#stopResizing() {
this.#isResizerEnabledForKeyboard = false;
this.#setResizerTabIndex(-1);
if (this.#savedDimensions) {
const { savedX, savedY, savedWidth, savedHeight } = this.#savedDimensions;
this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight);
this.#savedDimensions = null;
}
this.#addResizeToUndoStack();
}
_stopResizingWithKeyboard() {

File diff suppressed because it is too large Load diff

View file

@ -411,6 +411,21 @@ class CommandManager {
return this.#position < this.#commands.length - 1;
}
cleanType(type) {
if (this.#position === -1) {
return;
}
for (let i = this.#position; i >= 0; i--) {
if (this.#commands[i].type !== type) {
this.#commands.splice(i + 1, this.#position - i);
this.#position = i;
return;
}
}
this.#commands.length = 0;
this.#position = -1;
}
destroy() {
this.#commands = null;
}
@ -1034,6 +1049,7 @@ class AnnotationEditorUIManager {
for (const editor of this.#editorsToRescale) {
editor.onScaleChanging();
}
this.currentLayer?.onScaleChanging();
}
onRotationChanging({ pagesRotation }) {
@ -1931,6 +1947,10 @@ class AnnotationEditorUIManager {
}
}
updateUIForDefaultProperties(editorType) {
this.#dispatchUpdateUI(editorType.defaultPropertiesToUpdate);
}
/**
* Add or remove an editor the current selection.
* @param {AnnotationEditor} editor
@ -1957,6 +1977,7 @@ class AnnotationEditorUIManager {
* @param {AnnotationEditor} editor
*/
setSelected(editor) {
this.currentLayer?.commitOrRemove();
for (const ed of this.#selectedEditors) {
if (ed !== editor) {
ed.unselect();
@ -2044,6 +2065,10 @@ class AnnotationEditorUIManager {
});
}
cleanUndoStack(type) {
this.#commandManager.cleanType(type);
}
#isEmpty() {
if (this.#allEditors.size === 0) {
return true;
@ -2134,6 +2159,10 @@ class AnnotationEditorUIManager {
}
}
if (this.currentLayer?.commitOrRemove()) {
return;
}
if (!this.hasSelection) {
return;
}

View file

@ -94,6 +94,7 @@ const AnnotationEditorParamsType = {
HIGHLIGHT_THICKNESS: 33,
HIGHLIGHT_FREE: 34,
HIGHLIGHT_SHOW_ALL: 35,
DRAW_STEP: 41,
};
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.

View file

@ -649,7 +649,7 @@ class Driver {
if (task.annotationStorage) {
for (const annotation of Object.values(task.annotationStorage)) {
const { bitmapName, quadPoints } = annotation;
const { bitmapName, quadPoints, paths, outlines } = annotation;
if (bitmapName) {
promise = promise.then(async doc => {
const response = await fetch(
@ -687,6 +687,36 @@ class Driver {
// like IRL (in order to avoid bugs like bug 1907958).
annotation.quadPoints = new Float32Array(quadPoints);
}
if (paths) {
for (let i = 0, ii = paths.lines.length; i < ii; i++) {
paths.lines[i] = Float32Array.from(
paths.lines[i],
x => x ?? NaN
);
}
for (let i = 0, ii = paths.points.length; i < ii; i++) {
paths.points[i] = Float32Array.from(
paths.points[i],
x => x ?? NaN
);
}
}
if (outlines) {
if (Array.isArray(outlines)) {
for (let i = 0, ii = outlines.length; i < ii; i++) {
outlines[i] = Float32Array.from(outlines[i], x => x ?? NaN);
}
} else {
outlines.outline = Float32Array.from(
outlines.outline,
x => x ?? NaN
);
outlines.points = Float32Array.from(
outlines.points,
x => x ?? NaN
);
}
}
}
}

View file

@ -117,7 +117,7 @@ describe("Ink Editor", () => {
await commit(page);
const rectBefore = await getRect(page, ".inkEditor canvas");
const rectBefore = await getRect(page, ".canvasWrapper .draw");
for (let i = 0; i < 30; i++) {
await kbUndo(page);
@ -126,7 +126,7 @@ describe("Ink Editor", () => {
await waitForStorageEntries(page, 1);
}
const rectAfter = await getRect(page, ".inkEditor canvas");
const rectAfter = await getRect(page, ".canvasWrapper .draw");
expect(Math.round(rectBefore.x))
.withContext(`In ${browserName}`)
@ -453,4 +453,118 @@ describe("Ink Editor", () => {
);
});
});
describe("Drawing must unselect all", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that when we start to draw then the editors are unselected", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer");
let xStart = rect.x + 10;
const yStart = rect.y + 10;
for (let i = 0; i < 2; i++) {
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(xStart, yStart);
await page.mouse.down();
if (i === 1) {
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([]);
}
await page.mouse.move(xStart + 50, yStart + 50);
await page.mouse.up();
await awaitPromise(clickHandle);
await commit(page);
xStart += 70;
}
})
);
});
});
describe("Selected editor must be updated even if the page has been destroyed", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that the color has been changed", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToInk(page);
const rect = await getRect(page, ".annotationEditorLayer");
const x = rect.x + 20;
const y = rect.y + 20;
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.move(x + 50, y + 50);
await page.mouse.up();
await awaitPromise(clickHandle);
await commit(page);
const drawSelector = `.page[data-page-number = "1"] .canvasWrapper .draw`;
await page.waitForSelector(drawSelector, { visible: true });
let color = await page.evaluate(sel => {
const el = document.querySelector(sel);
return el.getAttribute("stroke");
}, drawSelector);
expect(color).toEqual("#000000");
const oneToFourteen = Array.from(new Array(13).keys(), n => n + 2);
for (const pageNumber of oneToFourteen) {
await scrollIntoView(
page,
`.page[data-page-number = "${pageNumber}"]`
);
}
const red = "#ff0000";
page.evaluate(value => {
window.PDFViewerApplication.eventBus.dispatch(
"switchannotationeditorparams",
{
source: null,
type: window.pdfjsLib.AnnotationEditorParamsType.INK_COLOR,
value,
}
);
}, red);
const fourteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
for (const pageNumber of fourteenToOne) {
await scrollIntoView(
page,
`.page[data-page-number = "${pageNumber}"]`
);
}
await page.waitForSelector(drawSelector, { visible: true });
color = await page.evaluate(sel => {
const el = document.querySelector(sel);
return el.getAttribute("stroke");
}, drawSelector);
expect(color).toEqual(red);
})
);
});
});
});

View file

@ -8244,24 +8244,44 @@
"color": [255, 0, 0],
"thickness": 3,
"opacity": 1,
"paths": [
{
"bezier": [
73, 560.2277710847244, 74.30408044851005, 561.5318515332344,
76.89681158113368, 557.7555609512324, 77.5, 557.2277710847244,
81.95407020558315, 553.3304596548392, 87.4811839685984,
550.8645311043504, 92.5, 547.7277710847244, 97.38795894206055,
544.6727967459365, 109.48854351637208, 540.2392275683522, 113.5,
"paths": {
"lines": [
[
null,
null,
null,
null,
73,
560.2277710847244,
74.30408044851005,
561.5318515332344,
76.89681158113368,
557.7555609512324,
77.5,
557.2277710847244,
81.95407020558315,
553.3304596548392,
87.4811839685984,
550.8645311043504,
92.5,
547.7277710847244,
97.38795894206055,
544.6727967459365,
109.48854351637208,
540.2392275683522,
113.5,
536.2277710847244
],
"points": [
]
],
"points": [
[
73, 560.2277710847244, 76.7257911988625, 558.1025687477292,
75.5128345111164, 559.4147224528562, 77.5, 557.2277710847244,
92.5, 547.7277710847244, 109.21378602219673, 539.2873735223628,
103.32868842191223, 542.3364518890394, 113.5, 536.2277710847244
]
}
],
]
},
"pageIndex": 0,
"rect": [71.5, 534.5, 115, 562],
"rotation": 0
@ -8330,22 +8350,37 @@
"color": [255, 0, 0],
"thickness": 1,
"opacity": 1,
"paths": [
{
"bezier": [
417.61538461538464, 520.3461538461538, 419.15384615384613,
520.3461538461538, 421.0769230769231, 520.3461538461538,
423.38461538461536, 520.3461538461538, 425.6923076923077,
520.3461538461538, 429.15384615384613, 519.9615384615385,
433.7692307692308, 519.1923076923076
],
"points": [
"paths": {
"lines": [
[
null,
null,
null,
null,
417.61538461538464,
520.3461538461538,
419.15384615384613,
520.3461538461538,
421.0769230769231,
520.3461538461538,
423.38461538461536,
520.3461538461538,
425.6923076923077,
520.3461538461538,
429.15384615384613,
519.9615384615385,
433.7692307692308,
519.1923076923076
]
],
"points": [
[
417.61538461538464, 520.3461538461538, 419.15384615384613,
520.3461538461538, 425.6923076923077, 520.3461538461538,
433.7692307692308, 519.1923076923076
]
}
],
]
},
"pageIndex": 0,
"rect": [
417.11538461538464, 510.46153846153845, 434.42307692307696,
@ -8358,22 +8393,37 @@
"color": [0, 255, 0],
"thickness": 1,
"opacity": 1,
"paths": [
{
"bezier": [
449.92307692307696, 526.6538461538462, 449.92307692307696,
527.423076923077, 449.6346153846154, 528.8653846153846,
449.0576923076924, 530.9807692307693, 448.4807692307693,
533.0961538461539, 447.8076923076924, 536.6538461538462,
447.0384615384616, 541.6538461538462
],
"points": [
"paths": {
"lines": [
[
null,
null,
null,
null,
449.92307692307696,
526.6538461538462,
449.92307692307696,
527.423076923077,
449.6346153846154,
528.8653846153846,
449.0576923076924,
530.9807692307693,
448.4807692307693,
533.0961538461539,
447.8076923076924,
536.6538461538462,
447.0384615384616,
541.6538461538462
]
],
"points": [
[
449.92307692307696, 526.6538461538462, 449.92307692307696,
527.423076923077, 448.4807692307693, 533.0961538461539,
447.0384615384616, 541.6538461538462
]
}
],
]
},
"pageIndex": 0,
"rect": [
446.5384615384616, 526.1538461538462, 456.92307692307696,
@ -8386,22 +8436,37 @@
"color": [0, 0, 255],
"thickness": 1,
"opacity": 1,
"paths": [
{
"bezier": [
482.8461538461538, 511.6538461538462, 482.07692307692304,
511.6538461538462, 480.53846153846155, 511.6538461538462,
478.23076923076917, 511.6538461538462, 475.9230769230769,
511.6538461538462, 472.46153846153845, 511.6538461538462,
467.8461538461538, 511.6538461538462
],
"points": [
"paths": {
"lines": [
[
null,
null,
null,
null,
482.8461538461538,
511.6538461538462,
482.07692307692304,
511.6538461538462,
480.53846153846155,
511.6538461538462,
478.23076923076917,
511.6538461538462,
475.9230769230769,
511.6538461538462,
472.46153846153845,
511.6538461538462,
467.8461538461538,
511.6538461538462
]
],
"points": [
[
482.8461538461538, 511.6538461538462, 482.07692307692304,
511.6538461538462, 475.9230769230769, 511.6538461538462,
467.8461538461538, 511.6538461538462
]
}
],
]
},
"pageIndex": 0,
"rect": [
467.1923076923077, 511.1538461538462, 483.3461538461538,
@ -8414,22 +8479,37 @@
"color": [0, 255, 255],
"thickness": 1,
"opacity": 1,
"paths": [
{
"bezier": [
445.9230769230769, 509.3846153846154, 445.5384615384615,
509.3846153846154, 445.15384615384613, 508.1346153846154,
444.7692307692307, 505.6346153846154, 444.38461538461536,
503.1346153846154, 443.23076923076917, 499.00000000000006,
441.30769230769226, 493.2307692307693
],
"points": [
"paths": {
"lines": [
[
null,
null,
null,
null,
445.9230769230769,
509.3846153846154,
445.5384615384615,
509.3846153846154,
445.15384615384613,
508.1346153846154,
444.7692307692307,
505.6346153846154,
444.38461538461536,
503.1346153846154,
443.23076923076917,
499.00000000000006,
441.30769230769226,
493.2307692307693
]
],
"points": [
[
445.9230769230769, 509.3846153846154, 445.5384615384615,
509.3846153846154, 444.38461538461536, 503.1346153846154,
441.30769230769226, 493.2307692307693
]
}
],
]
},
"pageIndex": 0,
"rect": [
436.03846153846155, 492.5769230769231, 446.4230769230769,
@ -9599,12 +9679,12 @@
"color": [53, 228, 47],
"thickness": 20,
"opacity": 1,
"paths": [
{
"bezier": [279.9183673469388, 477.0105263157895],
"points": [279.9183673469388, 477.0105263157895]
}
],
"paths": {
"lines": [
[null, null, null, null, 279.9183673469388, 477.0105263157895]
],
"points": [[279.9183673469388, 477.0105263157895]]
},
"pageIndex": 0,
"rect": [
269.9183673469388, 443.93684210526317, 312.9387755102041,

View file

@ -4448,21 +4448,54 @@ describe("annotation", function () {
thickness: 1,
opacity: 1,
color: [0, 0, 0],
paths: [
{
bezier: [
10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27,
paths: {
lines: [
[
NaN,
NaN,
NaN,
NaN,
10,
11,
12,
13,
14,
15,
16,
17,
22,
23,
24,
25,
26,
27,
],
points: [1, 2, 3, 4, 5, 6, 7, 8],
},
{
bezier: [
910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925,
926, 927,
[
NaN,
NaN,
NaN,
NaN,
910,
911,
912,
913,
914,
915,
916,
917,
922,
923,
924,
925,
926,
927,
],
points: [91, 92, 93, 94, 95, 96, 97, 98],
},
],
],
points: [
[1, 2, 3, 4, 5, 6, 7, 8],
[91, 92, 93, 94, 95, 96, 97, 98],
],
},
},
],
null,
@ -4482,13 +4515,12 @@ describe("annotation", function () {
const appearance = data[1].data;
expect(appearance).toEqual(
"2 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 129>> stream\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 127>> stream\n" +
"1 w 1 J 1 j\n" +
"0 G\n" +
"10 11 m\n" +
"12 13 14 15 16 17 c\n" +
"22 23 24 25 26 27 c\n" +
"S\n" +
"910 911 m\n" +
"912 913 914 915 916 917 c\n" +
"922 923 924 925 926 927 c\n" +
@ -4513,21 +4545,54 @@ describe("annotation", function () {
thickness: 1,
opacity: 0.12,
color: [0, 0, 0],
paths: [
{
bezier: [
10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27,
paths: {
lines: [
[
NaN,
NaN,
NaN,
NaN,
10,
11,
12,
13,
14,
15,
16,
17,
22,
23,
24,
25,
26,
27,
],
points: [1, 2, 3, 4, 5, 6, 7, 8],
},
{
bezier: [
910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925,
926, 927,
[
NaN,
NaN,
NaN,
NaN,
910,
911,
912,
913,
914,
915,
916,
917,
922,
923,
924,
925,
926,
927,
],
points: [91, 92, 93, 94, 95, 96, 97, 98],
},
],
],
points: [
[1, 2, 3, 4, 5, 6, 7, 8],
[91, 92, 93, 94, 95, 96, 97, 98],
],
},
},
],
null,
@ -4547,7 +4612,7 @@ describe("annotation", function () {
const appearance = data[1].data;
expect(appearance).toEqual(
"2 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 136 /Resources " +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 134 /Resources " +
"<< /ExtGState << /R0 << /CA 0.12 /Type /ExtGState>>>>>>>> stream\n" +
"1 w 1 J 1 j\n" +
"0 G\n" +
@ -4555,7 +4620,6 @@ describe("annotation", function () {
"10 11 m\n" +
"12 13 14 15 16 17 c\n" +
"22 23 24 25 26 27 c\n" +
"S\n" +
"910 911 m\n" +
"912 913 914 915 916 917 c\n" +
"922 923 924 925 926 927 c\n" +
@ -4581,13 +4645,10 @@ describe("annotation", function () {
thickness: 3,
opacity: 1,
color: [0, 255, 0],
paths: [
{
bezier: [1, 2, 3, 4, 5, 6, 7, 8],
// Useless in the printing case.
points: [1, 2, 3, 4, 5, 6, 7, 8],
},
],
paths: {
lines: [[NaN, NaN, NaN, NaN, 1, 2, 3, 4, 5, 6, 7, 8]],
points: [[1, 2, 3, 4, 5, 6, 7, 8]],
},
},
]
)

View file

@ -66,20 +66,22 @@
font-size: 0;
}
.textLayer.highlighting {
cursor: var(--editorFreeHighlight-editing-cursor);
.textLayer {
&.highlighting {
cursor: var(--editorFreeHighlight-editing-cursor);
&:not(.free) span {
cursor: var(--editorHighlight-editing-cursor);
&:not(.free) span {
cursor: var(--editorHighlight-editing-cursor);
&[role="img"] {
&[role="img"] {
cursor: var(--editorFreeHighlight-editing-cursor);
}
}
&.free span {
cursor: var(--editorFreeHighlight-editing-cursor);
}
}
&.free span {
cursor: var(--editorFreeHighlight-editing-cursor);
}
}
#viewerContainer.pdfPresentationMode:fullscreen,
@ -154,6 +156,11 @@
.annotationEditorLayer.inkEditing {
cursor: var(--editorInk-editing-cursor);
touch-action: none;
}
.annotationEditorLayer .draw {
box-sizing: border-box;
}
.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor) {

View file

@ -17,6 +17,10 @@
svg {
transform: none;
&.moving {
z-index: 100000;
}
&.highlight,
&.highlightOutline {
&[data-main-rotation="90"] {
@ -41,6 +45,23 @@
}
}
&.draw {
position: absolute;
mix-blend-mode: normal;
&[data-draw-rotation="90"] {
transform: rotate(90deg);
}
&[data-draw-rotation="180"] {
transform: rotate(180deg);
}
&[data-draw-rotation="270"] {
transform: rotate(270deg);
}
}
&.highlight {
--blend-mode: multiply;

View file

@ -300,7 +300,7 @@ See https://github.com/adobe-type-tools/cmap-resources
</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="0">
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="1" min="0.05" max="1" step="0.05" tabindex="0">
</div>
</div>
</div>