From 36aae436bfea8db6d5384b53e91b76d97e281f6e Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 8 Jun 2022 20:05:25 +0200 Subject: [PATCH] [editor] Add support for saving newly added Ink --- src/core/annotation.js | 105 +++++++++++++++++++++++++++++++-- src/core/default_appearance.js | 11 ++-- test/unit/annotation_spec.js | 62 ++++++++++++++++++- 3 files changed, 169 insertions(+), 9 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 542fdc5c5..bc78df6b9 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -273,6 +273,17 @@ class AnnotationFactory { ) ); break; + case AnnotationEditorType.INK: + promises.push( + InkAnnotation.createNewAnnotation( + xref, + evaluator, + task, + annotation, + results, + dependencies + ) + ); } } @@ -720,12 +731,18 @@ class Annotation { let str = ""; if (this.backgroundColor) { - str = `${getPdfColor(this.backgroundColor)} ${rect} f `; + str = `${getPdfColor( + this.backgroundColor, + /* isFill */ true + )} ${rect} f `; } if (this.borderColor) { const borderWidth = this.borderStyle.width || 1; - str += `${borderWidth} w ${getPdfColor(this.borderColor)} ${rect} S `; + str += `${borderWidth} w ${getPdfColor( + this.borderColor, + /* isFill */ false + )} ${rect} S `; } return str; @@ -2945,7 +2962,7 @@ class FreeTextAnnotation extends MarkupAnnotation { freetext.set("Subtype", Name.get("FreeText")); freetext.set("CreationDate", `D:${getModificationDate()}`); freetext.set("Rect", rect); - const da = `/Helv ${fontSize} Tf ${getPdfColor(color)}`; + const da = `/Helv ${fontSize} Tf ${getPdfColor(color, /* isFill */ true)}`; freetext.set("DA", da); freetext.set("Contents", value); freetext.set("F", 4); @@ -3008,7 +3025,8 @@ class FreeTextAnnotation extends MarkupAnnotation { `0 0 ${numberToString(w)} ${numberToString(h)} re W n`, `BT`, `1 0 0 1 0 ${numberToString(h + lineDescent)} Tm 0 Tc ${getPdfColor( - color + color, + /* isFill */ true )}`, `/Helv ${numberToString(newFontSize)} Tf`, ]; @@ -3412,6 +3430,85 @@ class InkAnnotation extends MarkupAnnotation { }); } } + + static async createNewAnnotation( + xref, + evaluator, + task, + annotation, + results, + others + ) { + const inkRef = xref.getNewRef(); + 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", annotation.rect); + ink.set( + "InkList", + annotation.paths.map(p => p.points) + ); + ink.set("F", 4); + ink.set("Border", [0, 0, 0]); + ink.set("Rotate", 0); + + const [x1, y1, x2, y2] = annotation.rect; + const w = x2 - x1; + const h = y2 - y1; + + const appearanceBuffer = [ + `${annotation.thickness} w`, + `${getPdfColor(annotation.color, /* isFill */ false)}`, + ]; + const buffer = []; + for (const { bezier } of annotation.paths) { + buffer.length = 0; + buffer.push( + `${numberToString(bezier[0])} ${numberToString(bezier[1])} m` + ); + for (let i = 2, ii = bezier.length; i < ii; i += 6) { + const curve = bezier + .slice(i, i + 6) + .map(numberToString) + .join(" "); + buffer.push(`${curve} c`); + } + buffer.push("S"); + appearanceBuffer.push(buffer.join("\n")); + } + 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", [0, 0, w, h]); + appearanceStreamDict.set("Length", appearance.length); + + const ap = new StringStream(appearance); + ap.dict = appearanceStreamDict; + + buffer.length = 0; + const apRef = xref.getNewRef(); + let transform = xref.encrypt + ? xref.encrypt.createCipherTransform(apRef.num, apRef.gen) + : null; + writeObject(apRef, ap, buffer, transform); + others.push({ ref: apRef, data: buffer.join("") }); + + const n = new Dict(xref); + n.set("N", apRef); + ink.set("AP", n); + + buffer.length = 0; + transform = xref.encrypt + ? xref.encrypt.createCipherTransform(inkRef.num, inkRef.gen) + : null; + writeObject(inkRef, ink, buffer, transform); + + results.push({ ref: inkRef, data: buffer.join("") }); + } } class HighlightAnnotation extends MarkupAnnotation { diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index 82cc91b92..df1865485 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -82,21 +82,24 @@ function parseDefaultAppearance(str) { return new DefaultAppearanceEvaluator(str).parse(); } -function getPdfColor(color) { +function getPdfColor(color, isFill) { if (color[0] === color[1] && color[1] === color[2]) { const gray = color[0] / 255; - return `${numberToString(gray)} g`; + return `${numberToString(gray)} ${isFill ? "g" : "G"}`; } return ( Array.from(color) .map(c => numberToString(c / 255)) - .join(" ") + " rg" + .join(" ") + ` ${isFill ? "rg" : "RG"}` ); } // Create default appearance string from some information. function createDefaultAppearance({ fontSize, fontName, fontColor }) { - return `/${escapePDFName(fontName)} ${fontSize} Tf ${getPdfColor(fontColor)}`; + return `/${escapePDFName(fontName)} ${fontSize} Tf ${getPdfColor( + fontColor, + /* isFill */ true + )}`; } export { createDefaultAppearance, getPdfColor, parseDefaultAppearance }; diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 7239bca8b..e4d96e15f 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -3859,7 +3859,7 @@ describe("annotation", function () { }); describe("FreeTextAnnotation", () => { - it("should create an new FreeText annotation", async () => { + it("should create a new FreeText annotation", async () => { partialEvaluator.xref = new XRefMock(); const task = new WorkerTask("test FreeText creation"); const data = await AnnotationFactory.saveNewAnnotations( @@ -3968,6 +3968,66 @@ describe("annotation", function () { { x: 4, y: 5 }, ]); }); + + it("should create a new Ink annotation", async function () { + partialEvaluator.xref = new XRefMock(); + const task = new WorkerTask("test Ink creation"); + const data = await AnnotationFactory.saveNewAnnotations( + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.INK, + rect: [12, 34, 56, 78], + thickness: 1, + color: [0, 0, 0], + paths: [ + { + bezier: [ + 10, 11, 12, 13, 14, 15, 16, 17, 22, 23, 24, 25, 26, 27, + ], + points: [1, 2, 3, 4, 5, 6, 7, 8], + }, + { + bezier: [ + 910, 911, 912, 913, 914, 915, 916, 917, 922, 923, 924, 925, + 926, 927, + ], + points: [91, 92, 93, 94, 95, 96, 97, 98], + }, + ], + }, + ] + ); + + 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 [[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" + + "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 121>> stream\n" + + "1 w\n" + + "0 G\n" + + "10 11 m\n" + + "12 13 14 15 16 17 c\n" + + "22 23 24 25 26 27 c\n" + + "S\n" + + "910 911 m\n" + + "912 913 914 915 916 917 c\n" + + "922 923 924 925 926 927 c\n" + + "S\n" + + "endstream\n" + + "\n" + + "endobj\n" + ); + }); }); describe("HightlightAnnotation", function () {