diff --git a/src/core/annotation.js b/src/core/annotation.js index 23d383e93..aacd91843 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -45,6 +45,7 @@ import { getParentToUpdate, getRotationMatrix, isNumberArray, + lookupLineEnding, lookupMatrix, lookupNormalRect, lookupRect, @@ -983,6 +984,18 @@ 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 = lookupLineEnding(lineEnding, "None"); + } + /** * Set the line endings; should only be used with specific annotation types. * @param {Array} lineEndings - The line endings array. @@ -995,26 +1008,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"); } } } @@ -3880,11 +3874,23 @@ 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")) { + if (this.data.it === "FreeTextCallout") { + this.setLineEnding(dict.get("LE")); + this.data.lineEnding = this.lineEnding; + + const calloutLine = dict.getArray("CL"); + if (isNumberArray(calloutLine, 4) || isNumberArray(calloutLine, 6)) { + this.data.calloutLine = calloutLine; + } + } + } + if (this._hasAppearance) { const { fontColor, fontSize } = parseAppearanceStream( this.appearance, @@ -3932,8 +3938,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")); @@ -3953,6 +3968,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/src/core/core_utils.js b/src/core/core_utils.js index f099f219e..0ee19e9d2 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$/; @@ -304,6 +304,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]. @@ -727,6 +748,7 @@ export { isNumberArray, isWhiteSpace, log2, + lookupLineEnding, lookupMatrix, lookupNormalRect, lookupRect, diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 8124fe6cf..5b578a797 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 () {