From f8f44329617e045c5610f6860efeb5919352e124 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 20 Nov 2023 20:03:34 +0100 Subject: [PATCH] [Editor] Add support for saving/printing a newly added Highlight annotation (bug 1865708) --- src/core/annotation.js | 109 ++++++++++++++++++++++++++++ src/shared/util.js | 1 + test/test_manifest.json | 133 +++++++++++++++++++++++++++++++++++ test/unit/annotation_spec.js | 93 +++++++++++++++++++++++- 4 files changed, 335 insertions(+), 1 deletion(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 6d78ea384..cb6d9d2be 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -354,6 +354,15 @@ class AnnotationFactory { ) ); break; + case AnnotationEditorType.HIGHLIGHT: + promises.push( + HighlightAnnotation.createNewAnnotation( + xref, + annotation, + dependencies + ) + ); + break; case AnnotationEditorType.INK: promises.push( InkAnnotation.createNewAnnotation(xref, annotation, dependencies) @@ -429,6 +438,18 @@ class AnnotationFactory { ) ); break; + case AnnotationEditorType.HIGHLIGHT: + promises.push( + HighlightAnnotation.createNewPrintAnnotation( + annotationGlobals, + xref, + annotation, + { + evaluatorOptions: options, + } + ) + ); + break; case AnnotationEditorType.INK: promises.push( InkAnnotation.createNewPrintAnnotation( @@ -4432,6 +4453,94 @@ class HighlightAnnotation extends MarkupAnnotation { this.data.popupRef = null; } } + + static createNewDict(annotation, xref, { apRef, ap }) { + const { color, opacity, rect, rotation, user, quadPoints } = annotation; + const highlight = new Dict(xref); + highlight.set("Type", Name.get("Annot")); + highlight.set("Subtype", Name.get("Highlight")); + highlight.set("CreationDate", `D:${getModificationDate()}`); + highlight.set("Rect", rect); + highlight.set("F", 4); + highlight.set("Border", [0, 0, 0]); + highlight.set("Rotate", rotation); + highlight.set("QuadPoints", quadPoints); + + // Color. + highlight.set( + "C", + Array.from(color, c => c / 255) + ); + + // Opacity. + highlight.set("CA", opacity); + + if (user) { + highlight.set( + "T", + isAscii(user) ? user : stringToUTF16String(user, /* bigEndian = */ true) + ); + } + + if (apRef || ap) { + const n = new Dict(xref); + highlight.set("AP", n); + n.set("N", apRef || ap); + } + + return highlight; + } + + static async createNewAppearanceStream(annotation, xref, params) { + const { color, rect, outlines, opacity } = annotation; + + const appearanceBuffer = [ + `${getPdfColor(color, /* isFill */ true)}`, + "/R0 gs", + ]; + + const buffer = []; + for (const outline of outlines) { + buffer.length = 0; + buffer.push( + `${numberToString(outline[0])} ${numberToString(outline[1])} m` + ); + for (let i = 2, ii = outline.length; i < ii; i += 2) { + buffer.push( + `${numberToString(outline[i])} ${numberToString(outline[i + 1])} l` + ); + } + buffer.push("h"); + appearanceBuffer.push(buffer.join("\n")); + } + appearanceBuffer.push("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 UnderlineAnnotation extends MarkupAnnotation { diff --git a/src/shared/util.js b/src/shared/util.js index 5a4874ffe..986931a05 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -72,6 +72,7 @@ const AnnotationEditorType = { DISABLE: -1, NONE: 0, FREETEXT: 3, + HIGHLIGHT: 9, STAMP: 13, INK: 15, }; diff --git a/test/test_manifest.json b/test/test_manifest.json index 99e412808..d3799e7b4 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -8228,5 +8228,138 @@ "md5": "71591f11ee717e12887f529c84d5ae89", "rounds": 1, "type": "highlight" + }, + { + "id": "tracemonkey-highlight-editor-print", + "file": "pdfs/tracemonkey.pdf", + "md5": "9a192d8b1a7dc652a19835f6f08098bd", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 9, + "color": [ + 255, + 240, + 102 + ], + "opacity": 0.4, + "quadPoints": [ + 80.53230785208449, + 696.385203481336, + 528.9234102584737, + 696.385203481336, + 80.53230785208449, + 716.3783153330397, + 528.9234102584737, + 716.3783153330397, + 263.70067924380567, + 676.5056819423341, + 346.3002317296901, + 676.5056819423341, + 263.70067924380567, + 696.4987937940379, + 346.3002317296901, + 696.4987937940379 + ], + "outlines": [ + [ + 79.866, + 695.5343999999999, + 79.866, + 717.2352, + 529.5636000000001, + 717.2352, + 529.5636000000001, + 695.5343999999999, + 346.94280000000003, + 695.5343999999999, + 346.94280000000003, + 675.6551999999999, + 263.0376, + 675.6551999999999, + 263.0376, + 695.5343999999999 + ] + ], + "pageIndex": 0, + "rect": [ + 79.866, + 675.6551999999999, + 529.5636000000001, + 717.2352 + ], + "rotation": 0 + } + } + }, + { + "id": "tracemonkey-highlight-editor-save-print", + "file": "pdfs/tracemonkey.pdf", + "md5": "9a192d8b1a7dc652a19835f6f08098bd", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "save": true, + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 9, + "color": [ + 249, + 108, + 147 + ], + "opacity": 0.4, + "quadPoints": [ + 80.53230785208449, + 696.385203481336, + 528.9234102584737, + 696.385203481336, + 80.53230785208449, + 716.3783153330397, + 528.9234102584737, + 716.3783153330397, + 263.70067924380567, + 676.5056819423341, + 346.3002317296901, + 676.5056819423341, + 263.70067924380567, + 696.4987937940379, + 346.3002317296901, + 696.4987937940379 + ], + "outlines": [ + [ + 79.866, + 695.5343999999999, + 79.866, + 717.2352, + 529.5636000000001, + 717.2352, + 529.5636000000001, + 695.5343999999999, + 346.94280000000003, + 695.5343999999999, + 346.94280000000003, + 675.6551999999999, + 263.0376, + 675.6551999999999, + 263.0376, + 695.5343999999999 + ] + ], + "pageIndex": 0, + "rect": [ + 79.866, + 675.6551999999999, + 529.5636000000001, + 717.2352 + ], + "rotation": 0 + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 4b421d2ee..a95cd6f92 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4554,7 +4554,7 @@ describe("annotation", function () { }); }); - describe("HightlightAnnotation", function () { + describe("HighlightAnnotation", function () { it("should set quadpoints to null if not defined", async function () { const highlightDict = new Dict(); highlightDict.set("Type", Name.get("Annot")); @@ -4619,6 +4619,97 @@ describe("annotation", function () { expect(data.annotationType).toEqual(AnnotationType.HIGHLIGHT); expect(data.quadPoints).toEqual(null); }); + + it("should create a new Highlight annotation", async function () { + partialEvaluator.xref = new XRefMock(); + const task = new WorkerTask("test 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], + quadPoints: [1, 2, 3, 4, 5, 6, 7], + outlines: [ + [8, 9, 10, 11], + [12, 13, 14, 15], + ], + }, + ] + ); + + const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)"); + expect(base).toEqual( + "1 0 obj\n" + + "<< /Type /Annot /Subtype /Highlight /CreationDate (date) /Rect [12 34 56 78] " + + "/F 4 /Border [0 0 0] /Rotate 0 /QuadPoints [1 2 3 4 5 6 7] /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 47 /Resources << /ExtGState << /R0 << /BM /Multiply>>>>>>>> stream\n" + + "0 g\n" + + "/R0 gs\n" + + "8 9 m\n" + + "10 11 l\n" + + "h\n" + + "12 13 m\n" + + "14 15 l\n" + + "h\n" + + "f*\n" + + "endstream\n" + + "endobj\n" + ); + }); + + it("should render a new Highlight annotation for printing", async function () { + partialEvaluator.xref = new XRefMock(); + const task = new WorkerTask("test 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], + quadPoints: [1, 2, 3, 4, 5, 6, 7], + outlines: [[8, 9, 10, 11]], + }, + ] + ) + )[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.eoFill, + OPS.endAnnotation, + ]); + }); }); describe("UnderlineAnnotation", function () {