From fe2680fa9b2b438094517f061e996f1dd173636b Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Fri, 24 Jan 2025 14:47:54 +0100 Subject: [PATCH 1/5] Add support for line ending in FreeTextAnnotation callouts. --- src/core/annotation.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index c87f99f4b..7f724896d 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -969,6 +969,37 @@ class Annotation { this.color = getRgbColor(color); } + /** + * Set the line ending; should only be used with FreeText annotations. + * @param {Name} lineEnding - The line ending name. + */ + setLineEnding(lineEnding) { + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + throw new Error("Not implemented: setLineEnding"); + } + + this.lineEnding = "None"; // The default value. + + if (lineEnding instanceof Name) { + switch (lineEnding.name) { + case "None": + return; + case "Square": + case "Circle": + case "Diamond": + case "OpenArrow": + case "ClosedArrow": + case "Butt": + case "ROpenArrow": + case "RClosedArrow": + case "Slash": + this.lineEnding = lineEnding.name; + return; + } + } + warn(`Ignoring invalid lineEnding: ${lineEnding}`); + } + /** * Set the line endings; should only be used with specific annotation types. * @param {Array} lineEndings - The line endings array. @@ -3882,11 +3913,16 @@ class FreeTextAnnotation extends MarkupAnnotation { // We want to be able to add mouse listeners to the annotation. this.data.noHTML = false; - const { evaluatorOptions, xref } = params; + const { evaluatorOptions, xref, dict } = params; this.data.annotationType = AnnotationType.FREETEXT; this.setDefaultAppearance(params); this._hasAppearance = !!this.appearance; + if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { + this.setLineEnding(dict.getArray("LE")); + this.data.lineEnding = this.lineEnding; + } + if (this._hasAppearance) { const { fontColor, fontSize } = parseAppearanceStream( this.appearance, From edfad112dabce5077758afcd76f251cba6da47fd Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Fri, 24 Jan 2025 15:23:01 +0100 Subject: [PATCH 2/5] Add callout line informations to FreeTextAnnotation --- src/core/annotation.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 7f724896d..79fd83fe7 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -3919,8 +3919,12 @@ class FreeTextAnnotation extends MarkupAnnotation { this._hasAppearance = !!this.appearance; if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { - this.setLineEnding(dict.getArray("LE")); - this.data.lineEnding = this.lineEnding; + if (dict.has("CL")) { + this.setLineEnding(dict.getArray("LE")); + this.data.calloutLine = dict.getArray("CL"); + this.data.rectDifference = dict.getArray("RD"); + this.data.lineEnding = this.lineEnding; + } } if (this._hasAppearance) { From 8e40e4e864005a471d68028689b68a900ec4f51b Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Mon, 27 Jan 2025 09:01:18 +0100 Subject: [PATCH 3/5] Use common helper to lookup line ending value --- src/core/annotation.js | 44 +++--------------------------------------- src/core/core_utils.js | 24 ++++++++++++++++++++++- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 79fd83fe7..fec27fdd6 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -45,6 +45,7 @@ import { getParentToUpdate, getRotationMatrix, isNumberArray, + lookupLineEnding, lookupMatrix, lookupNormalRect, lookupRect, @@ -978,26 +979,7 @@ class Annotation { throw new Error("Not implemented: setLineEnding"); } - this.lineEnding = "None"; // The default value. - - if (lineEnding instanceof Name) { - switch (lineEnding.name) { - case "None": - return; - case "Square": - case "Circle": - case "Diamond": - case "OpenArrow": - case "ClosedArrow": - case "Butt": - case "ROpenArrow": - case "RClosedArrow": - case "Slash": - this.lineEnding = lineEnding.name; - return; - } - } - warn(`Ignoring invalid lineEnding: ${lineEnding}`); + this.lineEnding = lookupLineEnding(lineEnding, "None"); } /** @@ -1012,26 +994,7 @@ class Annotation { if (Array.isArray(lineEndings) && lineEndings.length === 2) { for (let i = 0; i < 2; i++) { - const obj = lineEndings[i]; - - if (obj instanceof Name) { - switch (obj.name) { - case "None": - continue; - case "Square": - case "Circle": - case "Diamond": - case "OpenArrow": - case "ClosedArrow": - case "Butt": - case "ROpenArrow": - case "RClosedArrow": - case "Slash": - this.lineEndings[i] = obj.name; - continue; - } - } - warn(`Ignoring invalid lineEnding: ${obj}`); + this.lineEndings[i] = lookupLineEnding(lineEndings[i], "None"); } } } @@ -3922,7 +3885,6 @@ class FreeTextAnnotation extends MarkupAnnotation { if (dict.has("CL")) { this.setLineEnding(dict.getArray("LE")); this.data.calloutLine = dict.getArray("CL"); - this.data.rectDifference = dict.getArray("RD"); this.data.lineEnding = this.lineEnding; } } diff --git a/src/core/core_utils.js b/src/core/core_utils.js index c92a0374d..5900580ae 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -23,7 +23,7 @@ import { Util, warn, } from "../shared/util.js"; -import { Dict, isName, Ref, RefSet } from "./primitives.js"; +import { Dict, isName, Name, Ref, RefSet } from "./primitives.js"; import { BaseStream } from "./base_stream.js"; const PDF_VERSION_REGEXP = /^[1-9]\.\d$/; @@ -300,6 +300,27 @@ function lookupNormalRect(arr, fallback) { return isNumberArray(arr, 4) ? Util.normalizeRect(arr) : fallback; } +// Returns the line ending style, or the fallback value if it's invalid. +function lookupLineEnding(value, fallback) { + if (value instanceof Name) { + switch (value.name) { + case "None": + case "Square": + case "Circle": + case "Diamond": + case "OpenArrow": + case "ClosedArrow": + case "Butt": + case "ROpenArrow": + case "RClosedArrow": + case "Slash": + return value.name; + } + } + + return fallback; +} + /** * AcroForm field names use an array like notation to refer to * repeated XFA elements e.g. foo.bar[nnn]. @@ -723,6 +744,7 @@ export { isNumberArray, isWhiteSpace, log2, + lookupLineEnding, lookupMatrix, lookupNormalRect, lookupRect, From e2bb200d0008d93a68d4d65b0bfb4b8a6773c6be Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Mon, 27 Jan 2025 09:03:49 +0100 Subject: [PATCH 4/5] Add validation for callout line in FreeText annotation --- src/core/annotation.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index fec27fdd6..f6007c5cc 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -3884,8 +3884,12 @@ class FreeTextAnnotation extends MarkupAnnotation { if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { if (dict.has("CL")) { this.setLineEnding(dict.getArray("LE")); - this.data.calloutLine = dict.getArray("CL"); this.data.lineEnding = this.lineEnding; + + const calloutLine = dict.getArray("CL"); + if (isNumberArray(calloutLine)) { + this.data.calloutLine = calloutLine; + } } } From b8266a2ecf69ed7bf65675109e39f65b25c58efa Mon Sep 17 00:00:00 2001 From: Edoardo Cavazza Date: Mon, 27 Jan 2025 10:20:25 +0100 Subject: [PATCH 5/5] Add unit tests for FreeTextCallout annotation --- src/core/annotation.js | 28 ++++++++++++--- test/unit/annotation_spec.js | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index f6007c5cc..0092720a9 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -3882,12 +3882,12 @@ class FreeTextAnnotation extends MarkupAnnotation { this._hasAppearance = !!this.appearance; if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { - if (dict.has("CL")) { - this.setLineEnding(dict.getArray("LE")); + if (this.data.it === "FreeTextCallout") { + this.setLineEnding(dict.get("LE")); this.data.lineEnding = this.lineEnding; const calloutLine = dict.getArray("CL"); - if (isNumberArray(calloutLine)) { + if (isNumberArray(calloutLine, 4) || isNumberArray(calloutLine, 6)) { this.data.calloutLine = calloutLine; } } @@ -3940,8 +3940,17 @@ class FreeTextAnnotation extends MarkupAnnotation { } static createNewDict(annotation, xref, { apRef, ap }) { - const { color, fontSize, oldAnnotation, rect, rotation, user, value } = - annotation; + const { + calloutLine, + color, + fontSize, + lineEnding, + oldAnnotation, + rect, + rotation, + user, + value, + } = annotation; const freetext = oldAnnotation || new Dict(xref); freetext.set("Type", Name.get("Annot")); freetext.set("Subtype", Name.get("FreeText")); @@ -3961,6 +3970,15 @@ class FreeTextAnnotation extends MarkupAnnotation { freetext.set("Border", [0, 0, 0]); freetext.set("Rotate", rotation); + if (calloutLine) { + freetext.set("IT", Name.get("FreeTextCallout")); + + freetext.set("CL", calloutLine); + if (lineEnding) { + freetext.set("LE", Name.get(lineEnding)); + } + } + if (user) { freetext.set("T", stringToAsciiOrUTF16BE(user)); } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index a7c968644..69c2cac70 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -4257,6 +4257,40 @@ describe("annotation", function () { ); }); + it("should create a new FreeTextCallout annotation", async () => { + const xref = (partialEvaluator.xref = new XRefMock()); + const task = new WorkerTask("test FreeText creation"); + const changes = new RefSetCache(); + await AnnotationFactory.saveNewAnnotations( + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.FREETEXT, + rect: [12, 34, 56, 78], + rotation: 0, + fontSize: 10, + color: [0, 0, 0], + calloutLine: [0, 0, 10, 56, 12, 56], + lineEnding: "OpenArrow", + value: "Hello PDF.js World!", + }, + ], + null, + changes + ); + const data = await writeChanges(changes, xref); + + const base = data[1].data.replace(/\(D:\d+\)/, "(date)"); + expect(base).toEqual( + "2 0 obj\n" + + "<< /Type /Annot /Subtype /FreeText /CreationDate (date) " + + "/Rect [12 34 56 78] /DA (/Helv 10 Tf 0 g) /Contents (Hello PDF.js World!) " + + "/F 4 /Border [0 0 0] /Rotate 0 /IT /FreeTextCallout /CL [0 0 10 56 12 56] /LE /OpenArrow /AP << /N 3 0 R>>>>\n" + + "endobj\n" + ); + }); + it("should render an added FreeText annotation for printing", async function () { partialEvaluator.xref = new XRefMock(); const task = new WorkerTask("test FreeText printing"); @@ -4384,6 +4418,38 @@ describe("annotation", function () { "World !", ]); }); + + it("should parse callout line from a FreeTextCallout annotation", async function () { + partialEvaluator.xref = new XRefMock(); + const task = new WorkerTask("test FreeTextCallout line parsing"); + const freetextAnnotation = ( + await AnnotationFactory.printNewAnnotations( + annotationGlobalsMock, + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.FREETEXT, + rect: [12, 34, 56, 78], + rotation: 0, + fontSize: 10, + color: [0, 0, 0], + calloutLine: [0, 0, 10, 56, 12, 56], + lineEnding: "OpenArrow", + value: "Hello PDF.js\nWorld !", + }, + ] + ) + )[0]; + + expect(freetextAnnotation.data.it).toEqual("FreeTextCallout"); + + expect(freetextAnnotation.data.calloutLine).toEqual([ + 0, 0, 10, 56, 12, 56, + ]); + + expect(freetextAnnotation.data.lineEnding).toEqual("OpenArrow"); + }); }); describe("InkAnnotation", function () {