diff --git a/src/core/annotation.js b/src/core/annotation.js index ad8166504..5520c61e9 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -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")); @@ -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); @@ -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); diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 2d4514c80..7072ad615 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -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} rect - the bounding box of the annotation */ - #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; + 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 (i === 0) { + 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} 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} @@ -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], @@ -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 { @@ -1215,7 +1201,7 @@ class InkEditor extends AnnotationEditor { this.scaleFactor / this.parentScale, this.translationX, this.translationY, - height + rect ), pageIndex: this.pageIndex, rect, diff --git a/test/test_manifest.json b/test/test_manifest.json index 72e2e102c..2a187a000 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6978,18 +6978,17 @@ "opacity": 1, "paths": [{ "bezier": [ - 1.5, 25.727771084724367, 2.8040804485100495, 27.031851533234402, - 5.396811581133676, 23.25556095123241, 6, 22.727771084724367, - 10.45407020558315, 18.830459654839103, 15.981183968598401, - 16.364531104350363, 21, 13.227771084724367, 25.88795894206055, - 10.172796745936523, 37.988543516372076, 5.739227568352277, 42, - 1.7277710847243668 + 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": [ - 1.5, 25.727771084724367, 5.225791198862495, 23.602568747729173, - 4.012834511116397, 24.914722452856147, 6, 22.727771084724367, 21, - 13.227771084724367, 37.71378602219673, 4.78737352236285, - 31.828688421912233, 7.836451889039392, 42, 1.7277710847243668 + 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, @@ -7057,101 +7056,113 @@ }, "pdfjs_internal_editor_21": { "annotationType": 15, - "color": [0, 0, 0], + "color": [255, 0, 0], "thickness": 1, "opacity": 1, "paths": [ { "bezier": [ - 0.5, 15.653846153846189, 0.5, 10.612792605955292, - 2.221156193659856, 5.960961418318131, 2.221156193659856, - 0.7371591421274406 + 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": [ - 0.5, 15.653846153846189, 2.221156193659856, 0.7371591421274406 + 417.61538461538464, 520.3461538461538, 419.15384615384613, + 520.3461538461538, 425.6923076923077, 520.3461538461538, + 433.7692307692308, 519.1923076923076 ] } ], "pageIndex": 0, "rect": [ - 416.53846153846155, 561.8076923076923, 419.41346388596753, - 577.9615384615385 + 417.11538461538464, 510.46153846153845, 434.42307692307696, + 520.8461538461538 ], "rotation": 0 }, "pdfjs_internal_editor_23": { "annotationType": 15, - "color": [0, 0, 0], + "color": [0, 255, 0], "thickness": 1, "opacity": 1, "paths": [ { "bezier": [ - 0.5, 18.538461538461547, 0.5, 12.869221974582576, - 3.9307267310416893, 5.207607308237302, 1.6538461538461537, - 0.653846153846164 + 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": [ - 0.5, 18.538461538461547, 2.434116685812059, 4.572198481030599, - 1.9307532933714027, 9.17784944259592, 1.6538461538461537, - 0.653846153846164 + 449.92307692307696, 526.6538461538462, 449.92307692307696, + 527.423076923077, 448.4807692307693, 533.0961538461539, + 447.0384615384616, 541.6538461538462 ] } ], "pageIndex": 0, "rect": [ - 390.00000000000006, 543.4615384615386, 409.0384615384616, - 546.9134638859676 + 446.5384615384616, 526.1538461538462, 456.92307692307696, + 542.3076923076924 ], "rotation": 90 }, "pdfjs_internal_editor_25": { "annotationType": 15, - "color": [0, 0, 0], + "color": [0, 0, 255], "thickness": 1, "opacity": 1, "paths": [ { "bezier": [ - 0.5, 24.307692307692264, 0.5, 16.218230266280443, - 1.6442331167367787, 8.976323168614734, 1.6442331167367787, - 0.8509134145882982 + 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": [ - 0.5, 24.307692307692264, 1.6442331167367787, 0.8509134145882982 + 482.8461538461538, 511.6538461538462, 482.07692307692304, + 511.6538461538462, 475.9230769230769, 511.6538461538462, + 467.8461538461538, 511.6538461538462 ] } ], "pageIndex": 0, "rect": [ - 422.7788438063401, 515.7692307692307, 425.07692307692304, - 540.576923076923 + 467.1923076923077, 511.1538461538462, 483.3461538461538, + 521.5384615384615 ], "rotation": 180 }, "pdfjs_internal_editor_27": { "annotationType": 15, - "color": [0, 0, 0], + "color": [0, 255, 255], "thickness": 1, "opacity": 1, "paths": [ { "bezier": [ - 0.5, 32.96153846153845, 4.262222952239026, 32.96153846153845, - 2.8076923076923075, 4.355429108316972, 2.8076923076923075, - 0.6538461538461462 + 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": [ - 0.5, 32.96153846153845, 2.9761779872739975, 7.263528385840449, - 3.0646797609357885, 18.195785915618856, 2.8076923076923075, - 0.6538461538461462 + 445.9230769230769, 509.3846153846154, 445.5384615384615, + 509.3846153846154, 444.38461538461536, 503.1346153846154, + 441.30769230769226, 493.2307692307693 ] } ], "pageIndex": 0, "rect": [ - 425.6538461538462, 553.7403822678785, 459.11538461538464, - 557.7692307692307 + 436.03846153846155, 492.5769230769231, 446.4230769230769, + 509.8846153846154 ], "rotation": 270 } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 5fbc477d4..dee019222 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4293,15 +4293,15 @@ describe("annotation", function () { expect(base).toEqual( "1 0 obj\n" + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + - "/InkList [[1 2 3 4 5 6 7 8] [91 92 93 94 95 96 97 98]] /F 4 /Border [0 0 0] " + - "/Rotate 0 /AP << /N 2 0 R>>>>\n" + + "/InkList [[1 2 3 4 5 6 7 8] [91 92 93 94 95 96 97 98]] /F 4 " + + "/Rotate 0 /BS << /W 1>> /C [0 0 0] /CA 1 /AP << /N 2 0 R>>>>\n" + "endobj\n" ); const appearance = data.dependencies[0].data; expect(appearance).toEqual( "2 0 obj\n" + - "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] /Length 129>> stream\n" + + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 129>> stream\n" + "1 w 1 J 1 j\n" + "0 G\n" + "10 11 m\n" + @@ -4354,15 +4354,15 @@ describe("annotation", function () { expect(base).toEqual( "1 0 obj\n" + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + - "/InkList [[1 2 3 4 5 6 7 8] [91 92 93 94 95 96 97 98]] /F 4 /Border [0 0 0] " + - "/Rotate 0 /AP << /N 2 0 R>>>>\n" + + "/InkList [[1 2 3 4 5 6 7 8] [91 92 93 94 95 96 97 98]] /F 4 " + + "/Rotate 0 /BS << /W 1>> /C [0 0 0] /CA 0.12 /AP << /N 2 0 R>>>>\n" + "endobj\n" ); const appearance = data.dependencies[0].data; expect(appearance).toEqual( "2 0 obj\n" + - "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [0 0 44 44] /Length 136 /Resources " + + "<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] /Length 136 /Resources " + "<< /ExtGState << /R0 << /CA 0.12 /Type /ExtGState>>>>>>>> stream\n" + "1 w 1 J 1 j\n" + "0 G\n" +