Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 47 additions & 23 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
getParentToUpdate,
getRotationMatrix,
isNumberArray,
lookupLineEnding,
lookupMatrix,
lookupNormalRect,
lookupRect,
Expand Down Expand Up @@ -969,6 +970,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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't add an almost duplicate of an existing method, since both cases should use rather one common helper to reduce code-size.

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.
Expand All @@ -981,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");
}
}
}
Expand Down Expand Up @@ -3882,11 +3876,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,
Expand Down Expand Up @@ -3934,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"));
Expand All @@ -3955,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));
}
Expand Down
24 changes: 23 additions & 1 deletion src/core/core_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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$/;
Expand Down Expand Up @@ -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].
Expand Down Expand Up @@ -723,6 +744,7 @@ export {
isNumberArray,
isWhiteSpace,
log2,
lookupLineEnding,
lookupMatrix,
lookupNormalRect,
lookupRect,
Expand Down
66 changes: 66 additions & 0 deletions test/unit/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 () {
Expand Down