Skip to content

Commit

Permalink
[Editor] Add few more info when saving ink data (thickness, opacity, …
Browse files Browse the repository at this point in the history
…...)

Fix the InkList entry: the coordinates were relative to the page and not
to the bounding box of the annotation.
  • Loading branch information
calixteman committed May 31, 2023
1 parent 071e6bc commit 74c62ea
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 301 deletions.
24 changes: 16 additions & 8 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4056,7 +4056,7 @@ class InkAnnotation extends MarkupAnnotation {
}

static createNewDict(annotation, xref, { apRef, ap }) {
const { paths, rect, rotation } = annotation;
const { color, opacity, paths, rect, rotation, thickness } = annotation;
const ink = new Dict(xref);
ink.set("Type", Name.get("Annot"));
ink.set("Subtype", Name.get("Ink"));
Expand All @@ -4067,9 +4067,22 @@ class InkAnnotation extends MarkupAnnotation {
paths.map(p => p.points)
);
ink.set("F", 4);
ink.set("Border", [0, 0, 0]);
ink.set("Rotate", rotation);

// Line thickness.
const bs = new Dict(xref);
ink.set("BS", bs);
bs.set("W", thickness);

// Color.
ink.set(
"C",
Array.from(color, c => c / 255)
);

// Opacity.
ink.set("CA", opacity);

const n = new Dict(xref);
ink.set("AP", n);

Expand Down Expand Up @@ -4123,14 +4136,9 @@ class InkAnnotation extends MarkupAnnotation {
appearanceStreamDict.set("FormType", 1);
appearanceStreamDict.set("Subtype", Name.get("Form"));
appearanceStreamDict.set("Type", Name.get("XObject"));
appearanceStreamDict.set("BBox", [0, 0, w, h]);
appearanceStreamDict.set("BBox", rect);
appearanceStreamDict.set("Length", appearance.length);

if (rotation) {
const matrix = getRotationMatrix(rotation, w, h);
appearanceStreamDict.set("Matrix", matrix);
}

if (opacity !== 1) {
const resources = new Dict(xref);
const extGState = new Dict(xref);
Expand Down
228 changes: 107 additions & 121 deletions src/display/editor/ink.js
Original file line number Diff line number Diff line change
Expand Up @@ -910,137 +910,123 @@ class InkEditor extends AnnotationEditor {
return path2D;
}

static #toPDFCoordinates(points, rect, rotation) {
const [blX, blY, trX, trY] = rect;

switch (rotation) {
case 0:
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] += blX;
points[i + 1] = trY - points[i + 1];
}
break;
case 90:
for (let i = 0, ii = points.length; i < ii; i += 2) {
const x = points[i];
points[i] = points[i + 1] + blX;
points[i + 1] = x + blY;
}
break;
case 180:
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = trX - points[i];
points[i + 1] += blY;
}
break;
case 270:
for (let i = 0, ii = points.length; i < ii; i += 2) {
const x = points[i];
points[i] = trX - points[i + 1];
points[i + 1] = trY - x;
}
break;
default:
throw new Error("Invalid rotation");
}
return points;
}

static #fromPDFCoordinates(points, rect, rotation) {
const [blX, blY, trX, trY] = rect;

switch (rotation) {
case 0:
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] -= blX;
points[i + 1] = trY - points[i + 1];
}
break;
case 90:
for (let i = 0, ii = points.length; i < ii; i += 2) {
const x = points[i];
points[i] = points[i + 1] - blY;
points[i + 1] = x - blX;
}
break;
case 180:
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = trX - points[i];
points[i + 1] -= blY;
}
break;
case 270:
for (let i = 0, ii = points.length; i < ii; i += 2) {
const x = points[i];
points[i] = trY - points[i + 1];
points[i + 1] = trX - x;
}
break;
default:
throw new Error("Invalid rotation");
}
return points;
}

/**
* Transform and serialize the paths.
* @param {number} s - scale factor
* @param {number} tx - abscissa of the translation
* @param {number} ty - ordinate of the translation
* @param {number} h - height of the bounding box
* @param {Array<number>} rect - the rectangle cotaining the paths
*/
#serializePaths(s, tx, ty, h) {
const NUMBER_OF_POINTS_ON_BEZIER_CURVE = 4;
#serializePaths(s, tx, ty, rect) {
const paths = [];
const padding = this.thickness / 2;
let buffer, points;

const shiftX = s * tx + padding;
const shiftY = s * ty + padding;
for (const bezier of this.paths) {
buffer = [];
points = [];
for (let i = 0, ii = bezier.length; i < ii; i++) {
const [first, control1, control2, second] = bezier[i];
const p10 = s * (first[0] + tx) + padding;
const p11 = h - s * (first[1] + ty) - padding;
const p20 = s * (control1[0] + tx) + padding;
const p21 = h - s * (control1[1] + ty) - padding;
const p30 = s * (control2[0] + tx) + padding;
const p31 = h - s * (control2[1] + ty) - padding;
const p40 = s * (second[0] + tx) + padding;
const p41 = h - s * (second[1] + ty) - padding;

if (i === 0) {
const buffer = [];
const points = [];
for (let j = 0, jj = bezier.length; j < jj; j++) {
const [first, control1, control2, second] = bezier[j];
const p10 = s * first[0] + shiftX;
const p11 = s * first[1] + shiftY;
const p20 = s * control1[0] + shiftX;
const p21 = s * control1[1] + shiftY;
const p30 = s * control2[0] + shiftX;
const p31 = s * control2[1] + shiftY;
const p40 = s * second[0] + shiftX;
const p41 = s * second[1] + shiftY;

if (j === 0) {
buffer.push(p10, p11);
points.push(p10, p11);
}
buffer.push(p20, p21, p30, p31, p40, p41);
this.#extractPointsOnBezier(
p10,
p11,
p20,
p21,
p30,
p31,
p40,
p41,
NUMBER_OF_POINTS_ON_BEZIER_CURVE,
points
);
points.push(p20, p21);
if (j === jj - 1) {
points.push(p40, p41);
}
}
paths.push({ bezier: buffer, points });
paths.push({
bezier: InkEditor.#toPDFCoordinates(buffer, rect, this.rotation),
points: InkEditor.#toPDFCoordinates(points, rect, this.rotation),
});
}

return paths;
}

/**
* Extract n-1 points from the cubic Bezier curve.
* @param {number} p10
* @param {number} p11
* @param {number} p20
* @param {number} p21
* @param {number} p30
* @param {number} p31
* @param {number} p40
* @param {number} p41
* @param {number} n
* @param {Array<number>} points
* @returns {undefined}
*/
#extractPointsOnBezier(p10, p11, p20, p21, p30, p31, p40, p41, n, points) {
// If we can save few points thanks to the flatness we must do it.
if (this.#isAlmostFlat(p10, p11, p20, p21, p30, p31, p40, p41)) {
points.push(p40, p41);
return;
}

// Apply the de Casteljau's algorithm in order to get n points belonging
// to the Bezier's curve:
// https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm

// The first point is the last point of the previous Bezier curve
// so no need to push the first point.
for (let i = 1; i < n - 1; i++) {
const t = i / n;
const mt = 1 - t;

let q10 = t * p10 + mt * p20;
let q11 = t * p11 + mt * p21;

let q20 = t * p20 + mt * p30;
let q21 = t * p21 + mt * p31;

const q30 = t * p30 + mt * p40;
const q31 = t * p31 + mt * p41;

q10 = t * q10 + mt * q20;
q11 = t * q11 + mt * q21;

q20 = t * q20 + mt * q30;
q21 = t * q21 + mt * q31;

q10 = t * q10 + mt * q20;
q11 = t * q11 + mt * q21;

points.push(q10, q11);
}

points.push(p40, p41);
}

/**
* Check if a cubic Bezier curve is almost flat.
* @param {number} p10
* @param {number} p11
* @param {number} p20
* @param {number} p21
* @param {number} p30
* @param {number} p31
* @param {number} p40
* @param {number} p41
* @returns {boolean}
*/
#isAlmostFlat(p10, p11, p20, p21, p30, p31, p40, p41) {
// For reference:
// https://jeremykun.com/tag/bezier-curves/
const tol = 10;

const ax = (3 * p20 - 2 * p10 - p40) ** 2;
const ay = (3 * p21 - 2 * p11 - p41) ** 2;
const bx = (3 * p30 - p10 - 2 * p40) ** 2;
const by = (3 * p31 - p11 - 2 * p41) ** 2;

return Math.max(ax, bx) + Math.max(ay, by) <= tol;
}

/**
* Get the bounding box containing all the paths.
* @returns {Array<number>}
Expand Down Expand Up @@ -1161,18 +1147,21 @@ class InkEditor extends AnnotationEditor {
editor.#realWidth = Math.round(width);
editor.#realHeight = Math.round(height);

for (const { bezier } of data.paths) {
const { paths, rect, rotation } = data;

for (let { bezier } of paths) {
bezier = InkEditor.#fromPDFCoordinates(bezier, rect, rotation);
const path = [];
editor.paths.push(path);
let p0 = scaleFactor * (bezier[0] - padding);
let p1 = scaleFactor * (height - bezier[1] - padding);
let p1 = scaleFactor * (bezier[1] - padding);
for (let i = 2, ii = bezier.length; i < ii; i += 6) {
const p10 = scaleFactor * (bezier[i] - padding);
const p11 = scaleFactor * (height - bezier[i + 1] - padding);
const p11 = scaleFactor * (bezier[i + 1] - padding);
const p20 = scaleFactor * (bezier[i + 2] - padding);
const p21 = scaleFactor * (height - bezier[i + 3] - padding);
const p21 = scaleFactor * (bezier[i + 3] - padding);
const p30 = scaleFactor * (bezier[i + 4] - padding);
const p31 = scaleFactor * (height - bezier[i + 5] - padding);
const p31 = scaleFactor * (bezier[i + 5] - padding);
path.push([
[p0, p1],
[p10, p11],
Expand Down Expand Up @@ -1201,9 +1190,6 @@ class InkEditor extends AnnotationEditor {
}

const rect = this.getRect(0, 0);
const height =
this.rotation % 180 === 0 ? rect[3] - rect[1] : rect[2] - rect[0];

const color = AnnotationEditor._colorManager.convert(this.ctx.strokeStyle);

return {
Expand All @@ -1215,7 +1201,7 @@ class InkEditor extends AnnotationEditor {
this.scaleFactor / this.parentScale,
this.translationX,
this.translationY,
height
rect
),
pageIndex: this.pageIndex,
rect,
Expand Down
Loading

0 comments on commit 74c62ea

Please sign in to comment.