From d64f334f98d4b7f1c2e09a731a63b68629c946f9 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 17 Jan 2024 16:42:35 +0100 Subject: [PATCH] [Editor] Add support for printing/saving free highlight annotations --- src/core/annotation.js | 135 +++++++++++--- src/core/writer.js | 4 +- src/display/editor/highlight.js | 1 + src/display/editor/outliner.js | 79 +++++--- test/test_manifest.json | 319 ++++++++++++++++++++++++++++++++ test/unit/annotation_spec.js | 119 ++++++++++++ 6 files changed, 611 insertions(+), 46 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index a01efdd9b048a..f7b5f92adbc77 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -355,13 +355,19 @@ class AnnotationFactory { ); break; case AnnotationEditorType.HIGHLIGHT: - promises.push( - HighlightAnnotation.createNewAnnotation( - xref, - annotation, - dependencies - ) - ); + if (annotation.quadPoints) { + promises.push( + HighlightAnnotation.createNewAnnotation( + xref, + annotation, + dependencies + ) + ); + } else { + promises.push( + InkAnnotation.createNewAnnotation(xref, annotation, dependencies) + ); + } break; case AnnotationEditorType.INK: promises.push( @@ -439,16 +445,29 @@ class AnnotationFactory { ); break; case AnnotationEditorType.HIGHLIGHT: - promises.push( - HighlightAnnotation.createNewPrintAnnotation( - annotationGlobals, - xref, - annotation, - { - evaluatorOptions: options, - } - ) - ); + if (annotation.quadPoints) { + promises.push( + HighlightAnnotation.createNewPrintAnnotation( + annotationGlobals, + xref, + annotation, + { + evaluatorOptions: options, + } + ) + ); + } else { + promises.push( + InkAnnotation.createNewPrintAnnotation( + annotationGlobals, + xref, + annotation, + { + evaluatorOptions: options, + } + ) + ); + } break; case AnnotationEditorType.INK: promises.push( @@ -4340,19 +4359,25 @@ class InkAnnotation extends MarkupAnnotation { } static createNewDict(annotation, xref, { apRef, ap }) { - const { color, opacity, paths, rect, rotation, thickness } = annotation; + const { color, opacity, paths, outlines, rect, rotation, thickness } = + annotation; const ink = new Dict(xref); ink.set("Type", Name.get("Annot")); ink.set("Subtype", Name.get("Ink")); ink.set("CreationDate", `D:${getModificationDate()}`); ink.set("Rect", rect); - ink.set( - "InkList", - paths.map(p => p.points) - ); + ink.set("InkList", outlines?.points || paths.map(p => p.points)); ink.set("F", 4); ink.set("Rotate", rotation); + if (outlines) { + // Free highlight. + // There's nothing about this in the spec, but it's used when highlighting + // in Edge's viewer. Acrobat takes into account this parameter to indicate + // that the Ink is used for highlighting. + ink.set("IT", Name.get("InkHighlight")); + } + // Line thickness. const bs = new Dict(xref); ink.set("BS", bs); @@ -4380,6 +4405,13 @@ class InkAnnotation extends MarkupAnnotation { } static async createNewAppearanceStream(annotation, xref, params) { + if (annotation.outlines) { + return this.createNewAppearanceStreamForHighlight( + annotation, + xref, + params + ); + } const { color, rect, paths, thickness, opacity } = annotation; const appearanceBuffer = [ @@ -4438,6 +4470,65 @@ class InkAnnotation extends MarkupAnnotation { return ap; } + + static async createNewAppearanceStreamForHighlight(annotation, xref, params) { + const { + color, + rect, + outlines: { outline }, + opacity, + } = annotation; + const appearanceBuffer = [ + `${getPdfColor(color, /* isFill */ true)}`, + "/R0 gs", + ]; + + appearanceBuffer.push( + `${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) { + 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`); + } + } + appearanceBuffer.push("h f"); + const appearance = appearanceBuffer.join("\n"); + + const appearanceStreamDict = new Dict(xref); + appearanceStreamDict.set("FormType", 1); + appearanceStreamDict.set("Subtype", Name.get("Form")); + appearanceStreamDict.set("Type", Name.get("XObject")); + appearanceStreamDict.set("BBox", rect); + appearanceStreamDict.set("Length", appearance.length); + + const resources = new Dict(xref); + const extGState = new Dict(xref); + resources.set("ExtGState", extGState); + appearanceStreamDict.set("Resources", resources); + const r0 = new Dict(xref); + extGState.set("R0", r0); + r0.set("BM", Name.get("Multiply")); + + if (opacity !== 1) { + r0.set("ca", opacity); + r0.set("Type", Name.get("ExtGState")); + } + + const ap = new StringStream(appearance); + ap.dict = appearanceStreamDict; + + return ap; + } } class HighlightAnnotation extends MarkupAnnotation { diff --git a/src/core/writer.js b/src/core/writer.js index 70560ea5b7a79..a75c88418439b 100644 --- a/src/core/writer.js +++ b/src/core/writer.js @@ -32,7 +32,7 @@ async function writeObject(ref, obj, buffer, { encrypt = null }) { await writeDict(obj, buffer, transform); } else if (obj instanceof BaseStream) { await writeStream(obj, buffer, transform); - } else if (Array.isArray(obj)) { + } else if (Array.isArray(obj) || ArrayBuffer.isView(obj)) { await writeArray(obj, buffer, transform); } buffer.push("\nendobj\n"); @@ -132,7 +132,7 @@ async function writeValue(value, buffer, transform) { buffer.push(`/${escapePDFName(value.name)}`); } else if (value instanceof Ref) { buffer.push(`${value.num} ${value.gen} R`); - } else if (Array.isArray(value)) { + } else if (Array.isArray(value) || ArrayBuffer.isView(value)) { await writeArray(value, buffer, transform); } else if (typeof value === "string") { if (transform) { diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index 4bdfa30dd89a3..48200534f9049 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -597,6 +597,7 @@ class HighlightEditor extends AnnotationEditor { annotationType: AnnotationEditorType.HIGHLIGHT, color, opacity: this.#opacity, + thickness: 2 * HighlightEditor._defaultThickness, quadPoints: this.#serializeBoxes(rect), outlines: this.#serializeOutlines(rect), pageIndex: this.pageIndex, diff --git a/src/display/editor/outliner.js b/src/display/editor/outliner.js index 90a3a1a888a82..02366fddc24ec 100644 --- a/src/display/editor/outliner.js +++ b/src/display/editor/outliner.js @@ -596,6 +596,12 @@ class FreeOutliner { const lastBottom = last.subarray(16, 18); const [layerX, layerY, layerWidth, layerHeight] = this.#box; + const points = new Float64Array(this.#points?.length ?? 0); + for (let i = 0, ii = points.length; i < ii; i += 2) { + points[i] = (this.#points[i] - layerX) / layerWidth; + points[i + 1] = (this.#points[i + 1] - layerY) / layerHeight; + } + if (isNaN(last[6]) && !this.isEmpty()) { // We've only two points. const outline = new Float64Array(24); @@ -628,7 +634,12 @@ class FreeOutliner { ], 0 ); - return new FreeHighlightOutline(outline, this.#innerMargin, isLTR); + return new FreeHighlightOutline( + outline, + points, + this.#innerMargin, + isLTR + ); } const outline = new Float64Array( @@ -675,7 +686,7 @@ class FreeOutliner { } } outline.set([NaN, NaN, NaN, NaN, bottom[4], bottom[5]], N); - return new FreeHighlightOutline(outline, this.#innerMargin, isLTR); + return new FreeHighlightOutline(outline, points, this.#innerMargin, isLTR); } } @@ -684,11 +695,14 @@ class FreeHighlightOutline extends Outline { #innerMargin; + #points; + #outline; - constructor(outline, innerMargin, isLTR) { + constructor(outline, points, innerMargin, isLTR) { super(); this.#outline = outline; + this.#points = points; this.#innerMargin = innerMargin; this.#computeMinMax(isLTR); @@ -697,6 +711,10 @@ class FreeHighlightOutline extends Outline { outline[i] = (outline[i] - x) / width; outline[i + 1] = (outline[i + 1] - y) / height; } + for (let i = 0, ii = points.length; i < ii; i += 2) { + points[i] = (points[i] - x) / width; + points[i + 1] = (points[i + 1] - y) / height; + } } toSVGPath() { @@ -717,36 +735,53 @@ class FreeHighlightOutline extends Outline { } serialize([blX, blY, trX, trY], rotation) { - const src = this.#outline; - const outline = new Float64Array(src.length); const width = trX - blX; const height = trY - blY; + let outline; + let points; switch (rotation) { case 0: - for (let i = 0, ii = src.length; i < ii; i += 2) { - outline[i] = blX + src[i] * width; - outline[i + 1] = trY - src[i + 1] * height; - } + outline = this.#rescale(this.#outline, blX, trY, width, -height); + points = this.#rescale(this.#points, blX, trY, width, -height); break; case 90: - for (let i = 0, ii = src.length; i < ii; i += 2) { - outline[i] = blX + src[i + 1] * width; - outline[i + 1] = blY + src[i] * height; - } + outline = this.#rescaleAndSwap(this.#outline, blX, blY, width, height); + points = this.#rescaleAndSwap(this.#points, blX, blY, width, height); break; case 180: - for (let i = 0, ii = src.length; i < ii; i += 2) { - outline[i] = trX - src[i] * width; - outline[i + 1] = blY + src[i + 1] * height; - } + outline = this.#rescale(this.#outline, trX, blY, -width, height); + points = this.#rescale(this.#points, trX, blY, -width, height); break; case 270: - for (let i = 0, ii = src.length; i < ii; i += 2) { - outline[i] = trX - src[i + 1] * width; - outline[i + 1] = trY - src[i] * height; - } + outline = this.#rescaleAndSwap( + this.#outline, + trX, + trY, + -width, + -height + ); + points = this.#rescaleAndSwap(this.#points, trX, trY, -width, -height); + break; + } + return { outline: Array.from(outline), points: [Array.from(points)] }; + } + + #rescale(src, tx, ty, sx, sy) { + const dest = new Float64Array(src.length); + for (let i = 0, ii = src.length; i < ii; i += 2) { + dest[i] = tx + src[i] * sx; + dest[i + 1] = ty + src[i + 1] * sy; + } + return dest; + } + + #rescaleAndSwap(src, tx, ty, sx, sy) { + const dest = new Float64Array(src.length); + for (let i = 0, ii = src.length; i < ii; i += 2) { + dest[i] = tx + src[i + 1] * sx; + dest[i + 1] = ty + src[i] * sy; } - return outline; + return dest; } #computeMinMax(isLTR) { diff --git a/test/test_manifest.json b/test/test_manifest.json index af735636ab76c..932d4674dcb93 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -9324,5 +9324,324 @@ "structTreeParentId": null } } + }, + { + "id": "tracemonkey-free-highlights", + "file": "pdfs/tracemonkey.pdf", + "md5": "9a192d8b1a7dc652a19835f6f08098bd", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 9, + "color": [83, 255, 188], + "opacity": 1, + "thickness": 20, + "quadPoints": null, + "outlines": { + "outline": [ + null, + null, + null, + null, + 74.55, + 697.13, + 78.51721472766205, + 696.5192209591247, + 79.29044208205713, + 696.4224516456007, + 80.05486887178773, + 696.3379903011609, + 80.81929566151834, + 696.253528956721, + 81.57617646110613, + 696.1784636392651, + 82.32551127055109, + 696.1127943487935, + 83.07484607999606, + 696.047125058322, + 83.81784340794434, + 695.9886636959667, + 84.55450325439585, + 695.9374102617279, + null, + null, + null, + null, + 85.65949302407317, + 695.8605301103697, + null, + null, + null, + null, + 87.03393143844572, + 715.8149755833261, + 85.48426194954816, + 715.9204201846449, + 84.87281761152917, + 715.9671985492615, + 84.26979673105028, + 716.0185774379673, + 83.66677585057136, + 716.0699563266733, + 83.07457849467843, + 716.1266344280523, + 82.4932046633715, + 716.1886117421047, + 81.9118308320646, + 716.2505890561571, + 81.34439691081282, + 716.3184503202297, + 80.7909028996162, + 716.3921955343225, + null, + null, + null, + null, + 77.95775603227756, + 716.8481332058944 + ], + "points": [ + [ + 76.25849755587352, 706.9926836109859, 86.14596273693202, + 705.2879914026346, 88.1916451881855, 705.2879914026346, + 90.23732763943899, 705.2879914026346, 92.28301009069247, + 705.2879914026346, 94.32869254194594, 705.2879914026346 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 73.94723907946947, 695.0685301103697, 87.64593143844571, + 717.6401332058945 + ], + "rotation": 0, + "structTreeParentId": null + }, + "pdfjs_internal_editor_1": { + "annotationType": 9, + "color": [128, 235, 255], + "opacity": 1, + "thickness": 20, + "quadPoints": null, + "outlines": { + "outline": [ + null, + null, + null, + null, + 260.9547742578311, + 674.9666484352592, + 265.0119754286617, + 674.7200369564051, + 265.85057115055446, + 674.6816601253416, + 266.8029560253001, + 674.6436779453497, + 267.7553409000457, + 674.6056957653577, + 268.641418670916, + 674.586241754074, + 269.46118933791087, + 674.5853159114982, + null, + null, + null, + null, + 270.6908453384032, + 674.5839271476348, + null, + null, + null, + null, + 270.6417128415015, + 694.586095213411, + 269.2921378713866, + 694.5754310173227, + 268.590908898233, + 694.5902107638636, + 267.72828093997174, + 694.624035935381, + 266.8656529817105, + 694.6578611068985, + 266.117024794879, + 694.6914601264726, + 265.48239637947734, + 694.7248329941037, + null, + null, + null, + null, + 262.2852690081439, + 694.9245750690663 + ], + "points": [ + [ + 261.6200216329875, 684.9456117521628, 271.8481748830951, + 684.263717601745, 273.8938055331166, 684.263717601745, + 276.6213130664787, 684.263717601745, 278.6669437165002, + 684.6046646769539 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 260.342789755033, 673.7919070919245, 271.30282984120134, + 695.7165951247766 + ], + "rotation": 90, + "structTreeParentId": null + }, + "pdfjs_internal_editor_2": { + "annotationType": 9, + "color": [255, 203, 230], + "opacity": 1, + "thickness": 20, + "quadPoints": null, + "outlines": { + "outline": [ + null, + null, + null, + null, + 350.1249530717911, + 692.6633944602168, + 345.38916289103776, + 693.0766494920856, + 344.5712435088917, + 693.1558187178291, + 343.92282969955727, + 693.2395419718531, + 343.2744158902228, + 693.3232652258769, + 342.6222941207318, + 693.408487931081, + 341.96646439108423, + 693.4952100874651, + null, + null, + null, + null, + 340.9827197966128, + 693.6252933220413, + null, + null, + null, + null, + 338.36438011302045, + 673.7956810233082, + 340.14908906785814, + 673.5603604426211, + 340.9899862433436, + 673.4556612067797, + 341.95789701237936, + 673.3403909673718, + 342.92580778141513, + 673.2251207279638, + 343.81937416539495, + 673.1375137282872, + 344.63859616431887, + 673.0775699683421, + null, + null, + null, + null, + 348.1346569560641, + 672.7609372902139 + ], + "points": [ + [ + 349.12980501392764, 682.7121658752152, 338.9013927576602, + 683.734981200226, 336.1738161559889, 683.734981200226, + 333.7871866295265, 684.4168580835666, 331.74150417827303, + 684.7577965252368 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 337.7523801130204, 671.9689372902138, 350.7369530717911, + 694.4172933220414 + ], + "rotation": 180, + "structTreeParentId": null + }, + "pdfjs_internal_editor_3": { + "annotationType": 9, + "color": [255, 79, 95], + "opacity": 1, + "thickness": 20, + "quadPoints": null, + "outlines": { + "outline": [ + null, + null, + null, + null, + 534.1504350586464, + 715.0615382740357, + 530.2182783647162, + 715.0615382740357, + 529.5364014813756, + 715.0615382740357, + 528.8545245980351, + 715.0615382740357, + 528.1726477146946, + 715.0615382740357, + 527.4339477577424, + 715.0615382740357, + 526.6384247271785, + 715.0615382740357, + null, + null, + null, + null, + 525.4451401813326, + 715.0615382740357, + null, + null, + null, + null, + 525.4451401813326, + 695.0593098617794, + 527.4339477577424, + 695.0593098617794, + 528.1726477146946, + 695.0593098617794, + 528.8545245980351, + 695.0593098617794, + 529.5364014813756, + 695.0593098617794, + 530.2182783647162, + 695.0593098617794, + 530.9001552480566, + 695.0593098617794, + null, + null, + null, + null, + 534.1504350586464, + 695.0593098617794 + ], + "points": [ + [ + 534.1504350586464, 705.0604240679075, 523.9222818085387, + 705.0604240679075, 521.8766511585172, 705.0604240679075, + 519.8310205084957, 705.0604240679075, 517.444451416804, + 705.0604240679075 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 524.8331556785345, 694.2672898060691, 534.7624195614445, + 715.853558329746 + ], + "rotation": 270, + "structTreeParentId": null + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 3153a4154c70e..e1bbd0d770c8e 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4708,6 +4708,125 @@ describe("annotation", function () { OPS.endAnnotation, ]); }); + + it("should create a new free Highlight annotation", async function () { + partialEvaluator.xref = new XRefMock(); + const task = new WorkerTask("test free Highlight creation"); + const data = await AnnotationFactory.saveNewAnnotations( + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.HIGHLIGHT, + rect: [12, 34, 56, 78], + rotation: 0, + opacity: 1, + color: [0, 0, 0], + thickness: 3.14, + quadPoints: null, + outlines: { + outline: Float64Array.from([ + NaN, + NaN, + 8, + 9, + 10, + 11, + NaN, + NaN, + 12, + 13, + 14, + 15, + ]), + points: [Float64Array.from([16, 17, 18, 19])], + }, + }, + ] + ); + + const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)"); + expect(base).toEqual( + "1 0 obj\n" + + "<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " + + "/InkList [[16 17 18 19]] /F 4 /Rotate 0 /IT /InkHighlight /BS << /W 3.14>> " + + "/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 [12 34 56 78] " + + "/Length 30 /Resources << /ExtGState << /R0 << /BM /Multiply>>>>>>>> " + + "stream\n" + + "0 g\n" + + "/R0 gs\n" + + "10 11 m\n" + + "14 15 l\n" + + "h f\n" + + "endstream\n" + + "endobj\n" + ); + }); + + it("should render a new free Highlight annotation for printing", async function () { + partialEvaluator.xref = new XRefMock(); + const task = new WorkerTask("test free Highlight printing"); + const highlightAnnotation = ( + await AnnotationFactory.printNewAnnotations( + annotationGlobalsMock, + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.HIGHLIGHT, + rect: [12, 34, 56, 78], + rotation: 0, + opacity: 0.5, + color: [0, 255, 0], + thickness: 3.14, + quadPoints: null, + outlines: { + outline: Float64Array.from([ + NaN, + NaN, + 8, + 9, + 10, + 11, + NaN, + NaN, + 12, + 13, + 14, + 15, + ]), + points: [Float64Array.from([16, 17, 18, 19])], + }, + }, + ] + ) + )[0]; + + const { opList } = await highlightAnnotation.getOperatorList( + partialEvaluator, + task, + RenderingIntentFlag.PRINT, + false, + null + ); + + expect(opList.argsArray.length).toEqual(6); + expect(opList.fnArray).toEqual([ + OPS.beginAnnotation, + OPS.setFillRGBColor, + OPS.setGState, + OPS.constructPath, + OPS.fill, + OPS.endAnnotation, + ]); + }); }); describe("UnderlineAnnotation", function () {