mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-23 08:38:06 +02:00
[Editor] Make ink annotation editable
This commit is contained in:
parent
97c7a8eb7a
commit
7e02c77250
15 changed files with 754 additions and 88 deletions
|
@ -4382,7 +4382,7 @@ class InkAnnotation extends MarkupAnnotation {
|
|||
const { dict, xref } = params;
|
||||
this.data.annotationType = AnnotationType.INK;
|
||||
this.data.inkLists = [];
|
||||
this.data.isEditable = !this.data.noHTML && this.data.it === "InkHighlight";
|
||||
this.data.isEditable = !this.data.noHTML;
|
||||
// We want to be able to add mouse listeners to the annotation.
|
||||
this.data.noHTML = false;
|
||||
this.data.opacity = dict.get("CA") || 1;
|
||||
|
@ -4459,17 +4459,30 @@ class InkAnnotation extends MarkupAnnotation {
|
|||
}
|
||||
|
||||
static createNewDict(annotation, xref, { apRef, ap }) {
|
||||
const { color, opacity, paths, outlines, rect, rotation, thickness } =
|
||||
annotation;
|
||||
const ink = new Dict(xref);
|
||||
const {
|
||||
oldAnnotation,
|
||||
color,
|
||||
opacity,
|
||||
paths,
|
||||
outlines,
|
||||
rect,
|
||||
rotation,
|
||||
thickness,
|
||||
user,
|
||||
} = annotation;
|
||||
const ink = oldAnnotation || new Dict(xref);
|
||||
ink.set("Type", Name.get("Annot"));
|
||||
ink.set("Subtype", Name.get("Ink"));
|
||||
ink.set("CreationDate", `D:${getModificationDate()}`);
|
||||
ink.set(oldAnnotation ? "M" : "CreationDate", `D:${getModificationDate()}`);
|
||||
ink.set("Rect", rect);
|
||||
ink.set("InkList", outlines?.points || paths.points);
|
||||
ink.set("F", 4);
|
||||
ink.set("Rotate", rotation);
|
||||
|
||||
if (user) {
|
||||
ink.set("T", stringToAsciiOrUTF16BE(user));
|
||||
}
|
||||
|
||||
if (outlines) {
|
||||
// Free highlight.
|
||||
// There's nothing about this in the spec, but it's used when highlighting
|
||||
|
@ -4524,12 +4537,15 @@ class InkAnnotation extends MarkupAnnotation {
|
|||
}
|
||||
|
||||
for (const outline of paths.lines) {
|
||||
for (let i = 0, ii = outline.length; i < ii; i += 6) {
|
||||
appearanceBuffer.push(
|
||||
`${numberToString(outline[4])} ${numberToString(outline[5])} m`
|
||||
);
|
||||
for (let i = 6, ii = outline.length; i < ii; i += 6) {
|
||||
if (isNaN(outline[i])) {
|
||||
appearanceBuffer.push(
|
||||
`${numberToString(outline[i + 4])} ${numberToString(
|
||||
outline[i + 5]
|
||||
)} m`
|
||||
)} l`
|
||||
);
|
||||
} else {
|
||||
const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6);
|
||||
|
@ -5006,7 +5022,6 @@ class StampAnnotation extends MarkupAnnotation {
|
|||
oldAnnotation ? "M" : "CreationDate",
|
||||
`D:${getModificationDate()}`
|
||||
);
|
||||
stamp.set("CreationDate", `D:${getModificationDate()}`);
|
||||
stamp.set("Rect", rect);
|
||||
stamp.set("F", 4);
|
||||
stamp.set("Border", [0, 0, 0]);
|
||||
|
|
|
@ -2792,6 +2792,8 @@ class CaretAnnotationElement extends AnnotationElement {
|
|||
}
|
||||
|
||||
class InkAnnotationElement extends AnnotationElement {
|
||||
#polylinesGroupElement = null;
|
||||
|
||||
#polylines = [];
|
||||
|
||||
constructor(parameters) {
|
||||
|
@ -2809,6 +2811,38 @@ class InkAnnotationElement extends AnnotationElement {
|
|||
: AnnotationEditorType.INK;
|
||||
}
|
||||
|
||||
#getTransform(rotation, rect) {
|
||||
// PDF coordinates are calculated from a bottom left origin, so
|
||||
// transform the polyline coordinates to a top left origin for the
|
||||
// SVG element.
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
return {
|
||||
transform: `rotate(90) translate(${-rect[0]},${rect[1]}) scale(1,-1)`,
|
||||
width: rect[3] - rect[1],
|
||||
height: rect[2] - rect[0],
|
||||
};
|
||||
case 180:
|
||||
return {
|
||||
transform: `rotate(180) translate(${-rect[2]},${rect[1]}) scale(1,-1)`,
|
||||
width: rect[2] - rect[0],
|
||||
height: rect[3] - rect[1],
|
||||
};
|
||||
case 270:
|
||||
return {
|
||||
transform: `rotate(270) translate(${-rect[2]},${rect[3]}) scale(1,-1)`,
|
||||
width: rect[3] - rect[1],
|
||||
height: rect[2] - rect[0],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
transform: `translate(${-rect[0]},${rect[3]}) scale(1,-1)`,
|
||||
width: rect[2] - rect[0],
|
||||
height: rect[3] - rect[1],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.classList.add(this.containerClassName);
|
||||
|
||||
|
@ -2817,47 +2851,31 @@ class InkAnnotationElement extends AnnotationElement {
|
|||
const {
|
||||
data: { rect, rotation, inkLists, borderStyle, popupRef },
|
||||
} = this;
|
||||
let { width, height } = getRectDims(rect);
|
||||
let transform;
|
||||
|
||||
// PDF coordinates are calculated from a bottom left origin, so
|
||||
// transform the polyline coordinates to a top left origin for the
|
||||
// SVG element.
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
transform = `rotate(90) translate(${-rect[0]},${rect[3] - height}) scale(1,-1)`;
|
||||
[width, height] = [height, width];
|
||||
break;
|
||||
case 180:
|
||||
transform = `rotate(180) translate(${-rect[0] - width},${rect[3] - height}) scale(1,-1)`;
|
||||
break;
|
||||
case 270:
|
||||
transform = `rotate(270) translate(${-rect[0] - width},${rect[3]}) scale(1,-1)`;
|
||||
[width, height] = [height, width];
|
||||
break;
|
||||
default:
|
||||
transform = `translate(${-rect[0]},${rect[3]}) scale(1,-1)`;
|
||||
break;
|
||||
}
|
||||
const { transform, width, height } = this.#getTransform(rotation, rect);
|
||||
|
||||
const svg = this.svgFactory.create(
|
||||
width,
|
||||
height,
|
||||
/* skipDimensions = */ true
|
||||
);
|
||||
const basePolyline = this.svgFactory.createElement(this.svgElementName);
|
||||
const g = (this.#polylinesGroupElement =
|
||||
this.svgFactory.createElement("svg:g"));
|
||||
svg.append(g);
|
||||
// Ensure that the 'stroke-width' is always non-zero, since otherwise it
|
||||
// won't be possible to open/close the popup (note e.g. issue 11122).
|
||||
basePolyline.setAttribute("stroke-width", borderStyle.width || 1);
|
||||
basePolyline.setAttribute("stroke", "transparent");
|
||||
basePolyline.setAttribute("fill", "transparent");
|
||||
basePolyline.setAttribute("transform", transform);
|
||||
g.setAttribute("stroke-width", borderStyle.width || 1);
|
||||
g.setAttribute("stroke-linecap", "round");
|
||||
g.setAttribute("stroke-linejoin", "round");
|
||||
g.setAttribute("stroke-miterlimit", 10);
|
||||
g.setAttribute("stroke", "transparent");
|
||||
g.setAttribute("fill", "transparent");
|
||||
g.setAttribute("transform", transform);
|
||||
|
||||
for (let i = 0, ii = inkLists.length; i < ii; i++) {
|
||||
const polyline = i < ii - 1 ? basePolyline.cloneNode() : basePolyline;
|
||||
const polyline = this.svgFactory.createElement(this.svgElementName);
|
||||
this.#polylines.push(polyline);
|
||||
polyline.setAttribute("points", inkLists[i].join(","));
|
||||
svg.append(polyline);
|
||||
g.append(polyline);
|
||||
}
|
||||
|
||||
if (!popupRef && this.hasPopupData) {
|
||||
|
@ -2870,6 +2888,29 @@ class InkAnnotationElement extends AnnotationElement {
|
|||
return this.container;
|
||||
}
|
||||
|
||||
updateEdited(params) {
|
||||
super.updateEdited(params);
|
||||
const { thickness, points, rect } = params;
|
||||
const g = this.#polylinesGroupElement;
|
||||
if (thickness >= 0) {
|
||||
g.setAttribute("stroke-width", thickness || 1);
|
||||
}
|
||||
if (points) {
|
||||
for (let i = 0, ii = this.#polylines.length; i < ii; i++) {
|
||||
this.#polylines[i].setAttribute("points", points[i].join(","));
|
||||
}
|
||||
}
|
||||
if (rect) {
|
||||
const { transform, width, height } = this.#getTransform(
|
||||
this.data.rotation,
|
||||
rect
|
||||
);
|
||||
const root = g.parentElement;
|
||||
root.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
||||
g.setAttribute("transform", transform);
|
||||
}
|
||||
}
|
||||
|
||||
getElementsToTriggerPopup() {
|
||||
return this.#polylines;
|
||||
}
|
||||
|
|
|
@ -108,14 +108,7 @@ class InkDrawOutliner {
|
|||
}
|
||||
|
||||
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
|
||||
);
|
||||
this.#line.push(...Outline.createBezierPoints(x1, y1, x2, y2, x, y));
|
||||
|
||||
return {
|
||||
path: {
|
||||
|
@ -485,6 +478,51 @@ class InkDrawOutline extends Outline {
|
|||
break;
|
||||
}
|
||||
|
||||
if (!lines) {
|
||||
lines = [];
|
||||
for (const point of points) {
|
||||
const len = point.length;
|
||||
if (len === 2) {
|
||||
lines.push(
|
||||
new Float32Array([NaN, NaN, NaN, NaN, point[0], point[1]])
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (len === 4) {
|
||||
lines.push(
|
||||
new Float32Array([
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
point[0],
|
||||
point[1],
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
point[2],
|
||||
point[3],
|
||||
])
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const line = new Float32Array(3 * (len - 2));
|
||||
lines.push(line);
|
||||
let [x1, y1, x2, y2] = point.subarray(0, 4);
|
||||
line.set([NaN, NaN, NaN, NaN, x1, y1], 0);
|
||||
for (let i = 4; i < len; i += 2) {
|
||||
const x = point[i];
|
||||
const y = point[i + 1];
|
||||
line.set(
|
||||
Outline.createBezierPoints(x1, y1, x2, y2, x, y),
|
||||
(i - 2) * 3
|
||||
);
|
||||
[x1, y1, x2, y2] = [x2, y2, x, y];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, ii = lines.length; i < ii; i++) {
|
||||
newLines.push({
|
||||
line: rescaleFn(
|
||||
|
|
|
@ -97,6 +97,17 @@ class Outline {
|
|||
return [x, y];
|
||||
}
|
||||
}
|
||||
|
||||
static createBezierPoints(x1, y1, x2, y2, x3, y3) {
|
||||
return [
|
||||
(x1 + 5 * x2) / 6,
|
||||
(y1 + 5 * y2) / 6,
|
||||
(5 * x2 + x3) / 6,
|
||||
(5 * y2 + y3) / 6,
|
||||
(x2 + x3) / 2,
|
||||
(y2 + y3) / 2,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export { Outline };
|
||||
|
|
|
@ -66,7 +66,7 @@ class AnnotationEditor {
|
|||
|
||||
#hasBeenClicked = false;
|
||||
|
||||
#initialPosition = null;
|
||||
#initialRect = null;
|
||||
|
||||
#isEditing = false;
|
||||
|
||||
|
@ -468,13 +468,13 @@ class AnnotationEditor {
|
|||
* @param {number} y - y-translation in page coordinates.
|
||||
*/
|
||||
translateInPage(x, y) {
|
||||
this.#initialPosition ||= [this.x, this.y];
|
||||
this.#initialRect ||= [this.x, this.y, this.width, this.height];
|
||||
this.#translate(this.pageDimensions, x, y);
|
||||
this.div.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
drag(tx, ty) {
|
||||
this.#initialPosition ||= [this.x, this.y];
|
||||
this.#initialRect ||= [this.x, this.y, this.width, this.height];
|
||||
const {
|
||||
div,
|
||||
parentDimensions: [parentWidth, parentHeight],
|
||||
|
@ -530,9 +530,16 @@ class AnnotationEditor {
|
|||
|
||||
get _hasBeenMoved() {
|
||||
return (
|
||||
!!this.#initialPosition &&
|
||||
(this.#initialPosition[0] !== this.x ||
|
||||
this.#initialPosition[1] !== this.y)
|
||||
!!this.#initialRect &&
|
||||
(this.#initialRect[0] !== this.x || this.#initialRect[1] !== this.y)
|
||||
);
|
||||
}
|
||||
|
||||
get _hasBeenResized() {
|
||||
return (
|
||||
!!this.#initialRect &&
|
||||
(this.#initialRect[2] !== this.width ||
|
||||
this.#initialRect[3] !== this.height)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -989,6 +996,7 @@ class AnnotationEditor {
|
|||
const newX = oppositeX - transfOppositePoint[0];
|
||||
const newY = oppositeY - transfOppositePoint[1];
|
||||
|
||||
this.#initialRect ||= [this.x, this.y, this.width, this.height];
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
this.x = newX;
|
||||
|
|
|
@ -138,11 +138,44 @@ class InkEditor extends DrawingEditor {
|
|||
|
||||
/** @inheritdoc */
|
||||
static async deserialize(data, parent, uiManager) {
|
||||
let initialData = null;
|
||||
if (data instanceof InkAnnotationElement) {
|
||||
return null;
|
||||
const {
|
||||
data: {
|
||||
inkLists,
|
||||
rect,
|
||||
rotation,
|
||||
id,
|
||||
color,
|
||||
opacity,
|
||||
borderStyle: { rawWidth: thickness },
|
||||
popupRef,
|
||||
},
|
||||
parent: {
|
||||
page: { pageNumber },
|
||||
},
|
||||
} = data;
|
||||
initialData = data = {
|
||||
annotationType: AnnotationEditorType.INK,
|
||||
color: Array.from(color),
|
||||
thickness,
|
||||
opacity,
|
||||
paths: { points: inkLists },
|
||||
boxes: null,
|
||||
pageIndex: pageNumber - 1,
|
||||
rect: rect.slice(0),
|
||||
rotation,
|
||||
id,
|
||||
deleted: false,
|
||||
popupRef,
|
||||
};
|
||||
}
|
||||
|
||||
return super.deserialize(data, parent, uiManager);
|
||||
const editor = await super.deserialize(data, parent, uiManager);
|
||||
editor.annotationElementId = data.id || null;
|
||||
editor._initialData = initialData;
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
|
@ -214,9 +247,41 @@ class InkEditor extends DrawingEditor {
|
|||
structTreeParentId: this._structTreeParentId,
|
||||
};
|
||||
|
||||
if (isForCopying) {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
if (this.annotationElementId && !this.#hasElementChanged(serialized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
serialized.id = this.annotationElementId;
|
||||
return serialized;
|
||||
}
|
||||
|
||||
#hasElementChanged(serialized) {
|
||||
const { color, thickness, opacity, pageIndex } = this._initialData;
|
||||
return (
|
||||
this._hasBeenMoved ||
|
||||
this._hasBeenResized ||
|
||||
serialized.color.some((c, i) => c !== color[i]) ||
|
||||
serialized.thickness !== thickness ||
|
||||
serialized.opacity !== opacity ||
|
||||
serialized.pageIndex !== pageIndex
|
||||
);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
renderAnnotationElement(annotation) {
|
||||
const { points, rect } = this.serializeDraw(/* isForCopying = */ false);
|
||||
annotation.updateEdited({
|
||||
rect,
|
||||
thickness: this._drawingOptions["stroke-width"],
|
||||
points,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { InkEditor };
|
||||
|
|
|
@ -921,19 +921,19 @@ class StampEditor extends AnnotationEditor {
|
|||
|
||||
#hasElementChanged(serialized) {
|
||||
const {
|
||||
rect,
|
||||
pageIndex,
|
||||
accessibilityData: { altText },
|
||||
} = this._initialData;
|
||||
|
||||
const isSameRect = serialized.rect.every(
|
||||
(x, i) => Math.abs(x - rect[i]) < 1
|
||||
);
|
||||
const isSamePageIndex = serialized.pageIndex === pageIndex;
|
||||
const isSameAltText = (serialized.accessibilityData?.alt || "") === altText;
|
||||
|
||||
return {
|
||||
isSame: isSameRect && isSamePageIndex && isSameAltText,
|
||||
isSame:
|
||||
!this._hasBeenMoved &&
|
||||
!this._hasBeenResized &&
|
||||
isSamePageIndex &&
|
||||
isSameAltText,
|
||||
isSameAltText,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue