Skip to content

Commit 18b0baa

Browse files
committed
Stop automatically uppercasing annotation option keys / Do not mutate options passed to doc.annotate() and its convenience methods
1 parent ffc17b6 commit 18b0baa

3 files changed

Lines changed: 129 additions & 88 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
### Unreleased
44

55
- [BREAKING CHANGE] Restrict AcroForm options to documented mappings and explicit escape hatches.
6+
- [BREAKING CHANGE] Stop automatically uppercasing annotation option keys.
7+
- Do not mutate options passed to `doc.annotate()` and its convenience methods (link, note, strike, lineAnnotation, rectAnnotation, ellipseAnnotation, textAnnotation, fileAnnotation)
68

79
### [v0.19.1] - 2026-06-10
810

lib/mixins/annotations.js

Lines changed: 103 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,32 @@ import PDFAnnotationReference from '../structure_annotation';
22

33
export default {
44
annotate(x, y, w, h, options) {
5-
options.Type = 'Annot';
6-
options.Rect = this._convertRect(x, y, w, h);
7-
options.Border = [0, 0, 0];
8-
9-
if (options.Subtype === 'Link' && typeof options.F === 'undefined') {
10-
options.F = 1 << 2; // Print Annotation Flag
5+
const { structParent, color, ...annotations } = options;
6+
annotations.Type = 'Annot';
7+
annotations.Rect = this._convertRect(x, y, w, h);
8+
annotations.Border = [0, 0, 0];
9+
10+
if (
11+
annotations.Subtype === 'Link' &&
12+
typeof annotations.F === 'undefined'
13+
) {
14+
annotations.F = 1 << 2; // Print Annotation Flag
1115
}
1216

13-
if (options.Subtype !== 'Link') {
14-
if (options.C == null) {
15-
options.C = this._normalizeColor(options.color || [0, 0, 0]);
17+
if (annotations.Subtype !== 'Link') {
18+
if (annotations.C == null) {
19+
annotations.C = this._normalizeColor(color || [0, 0, 0]);
1620
}
17-
} // convert colors
18-
delete options.color;
19-
20-
if (typeof options.Dest === 'string') {
21-
options.Dest = new String(options.Dest);
2221
}
2322

24-
const structParent = options.structParent;
25-
delete options.structParent;
26-
27-
// Capitalize keys
28-
for (let key in options) {
29-
const val = options[key];
30-
options[key[0].toUpperCase() + key.slice(1)] = val;
23+
if (typeof annotations.Dest === 'string') {
24+
annotations.Dest = new String(annotations.Dest);
3125
}
3226

33-
const ref = this.ref(options);
27+
const ref = this.ref(annotations);
3428
this.page.annotations.push(ref);
3529

36-
if (structParent && typeof structParent.add === 'function') {
30+
if (typeof structParent?.add === 'function') {
3731
const annotRef = new PDFAnnotationReference(ref);
3832
structParent.add(annotRef);
3933
}
@@ -42,125 +36,146 @@ export default {
4236
return this;
4337
},
4438

45-
note(x, y, w, h, contents, options = {}) {
46-
options.Subtype = 'Text';
47-
options.Contents = new String(contents);
48-
if (options.Name == null) {
49-
options.Name = 'Comment';
39+
note(x, y, w, h, contents, options) {
40+
const annotationOptions = {
41+
...options,
42+
Subtype: 'Text',
43+
Contents: new String(contents),
44+
};
45+
if (annotationOptions.Name == null) {
46+
annotationOptions.Name = 'Comment';
5047
}
51-
if (options.color == null) {
52-
options.color = [243, 223, 92];
48+
if (annotationOptions.color == null) {
49+
annotationOptions.color = [243, 223, 92];
5350
}
54-
return this.annotate(x, y, w, h, options);
51+
52+
return this.annotate(x, y, w, h, annotationOptions);
5553
},
5654

57-
goTo(x, y, w, h, name, options = {}) {
58-
options.Subtype = 'Link';
59-
options.A = this.ref({
60-
S: 'GoTo',
61-
D: new String(name),
62-
});
63-
options.A.end();
64-
return this.annotate(x, y, w, h, options);
55+
goTo(x, y, w, h, name, options) {
56+
const annotateOptions = {
57+
...options,
58+
Subtype: 'Link',
59+
A: this.ref({ S: 'GoTo', D: new String(name) }),
60+
};
61+
annotateOptions.A.end();
62+
return this.annotate(x, y, w, h, annotateOptions);
6563
},
6664

67-
link(x, y, w, h, url, options = {}) {
68-
options.Subtype = 'Link';
65+
link(x, y, w, h, url, options) {
66+
const annotateOptions = { ...options, Subtype: 'Link' };
6967

7068
if (typeof url === 'number') {
7169
// Link to a page in the document (the page must already exist)
7270
const pages = this._root.data.Pages.data;
7371
if (url >= 0 && url < pages.Kids.length) {
74-
options.A = this.ref({
72+
annotateOptions.A = this.ref({
7573
S: 'GoTo',
7674
D: [pages.Kids[url], 'XYZ', null, null, null],
7775
});
78-
options.A.end();
76+
annotateOptions.A.end();
7977
} else {
8078
throw new Error(`The document has no page ${url}`);
8179
}
8280
} else {
8381
// Link to an external url
84-
options.A = this.ref({
82+
annotateOptions.A = this.ref({
8583
S: 'URI',
8684
URI: new String(url),
8785
});
88-
options.A.end();
86+
annotateOptions.A.end();
8987
}
9088

91-
if (options.structParent && !options.Contents) {
92-
options.Contents = new String('');
89+
if (annotateOptions.structParent && !annotateOptions.Contents) {
90+
annotateOptions.Contents = new String('');
9391
}
9492

95-
return this.annotate(x, y, w, h, options);
93+
return this.annotate(x, y, w, h, annotateOptions);
9694
},
9795

98-
_markup(x, y, w, h, options = {}) {
96+
_markup(x, y, w, h, options) {
9997
const [x1, y1, x2, y2] = this._convertRect(x, y, w, h);
100-
options.QuadPoints = [x1, y2, x2, y2, x1, y1, x2, y1];
101-
options.Contents = new String();
102-
return this.annotate(x, y, w, h, options);
98+
return this.annotate(x, y, w, h, {
99+
...options,
100+
QuadPoints: [x1, y2, x2, y2, x1, y1, x2, y1],
101+
Contents: new String(),
102+
});
103103
},
104104

105-
highlight(x, y, w, h, options = {}) {
106-
options.Subtype = 'Highlight';
107-
if (options.color == null) {
108-
options.color = [241, 238, 148];
105+
highlight(x, y, w, h, options) {
106+
const annotationOptions = { ...options, Subtype: 'Highlight' };
107+
if (annotationOptions.color == null) {
108+
annotationOptions.color = [241, 238, 148];
109109
}
110-
return this._markup(x, y, w, h, options);
110+
return this._markup(x, y, w, h, annotationOptions);
111111
},
112112

113-
underline(x, y, w, h, options = {}) {
114-
options.Subtype = 'Underline';
115-
return this._markup(x, y, w, h, options);
113+
underline(x, y, w, h, options) {
114+
const annotationOptions = { ...options, Subtype: 'Underline' };
115+
return this._markup(x, y, w, h, annotationOptions);
116116
},
117117

118-
strike(x, y, w, h, options = {}) {
119-
options.Subtype = 'StrikeOut';
120-
return this._markup(x, y, w, h, options);
118+
strike(x, y, w, h, options) {
119+
const annotationOptions = { ...options, Subtype: 'StrikeOut' };
120+
return this._markup(x, y, w, h, annotationOptions);
121121
},
122122

123-
lineAnnotation(x1, y1, x2, y2, options = {}) {
124-
options.Subtype = 'Line';
125-
options.Contents = new String();
126-
options.L = [x1, this.page.height - y1, x2, this.page.height - y2];
127-
return this.annotate(x1, y1, x2, y2, options);
123+
lineAnnotation(x1, y1, x2, y2, options) {
124+
const annotationOptions = {
125+
...options,
126+
Subtype: 'Line',
127+
Contents: new String(),
128+
L: [x1, this.page.height - y1, x2, this.page.height - y2],
129+
};
130+
return this.annotate(x1, y1, x2, y2, annotationOptions);
128131
},
129132

130-
rectAnnotation(x, y, w, h, options = {}) {
131-
options.Subtype = 'Square';
132-
options.Contents = new String();
133-
return this.annotate(x, y, w, h, options);
133+
rectAnnotation(x, y, w, h, options) {
134+
const annotationOptions = {
135+
...options,
136+
Subtype: 'Square',
137+
Contents: new String(),
138+
};
139+
return this.annotate(x, y, w, h, annotationOptions);
134140
},
135141

136-
ellipseAnnotation(x, y, w, h, options = {}) {
137-
options.Subtype = 'Circle';
138-
options.Contents = new String();
139-
return this.annotate(x, y, w, h, options);
142+
ellipseAnnotation(x, y, w, h, options) {
143+
const annotationOptions = {
144+
...options,
145+
Subtype: 'Circle',
146+
Contents: new String(),
147+
};
148+
return this.annotate(x, y, w, h, annotationOptions);
140149
},
141150

142-
textAnnotation(x, y, w, h, text, options = {}) {
143-
options.Subtype = 'FreeText';
144-
options.Contents = new String(text);
145-
options.DA = new String();
146-
return this.annotate(x, y, w, h, options);
151+
textAnnotation(x, y, w, h, text, options) {
152+
const annotationOptions = {
153+
...options,
154+
Subtype: 'FreeText',
155+
Contents: new String(text),
156+
DA: new String(),
157+
};
158+
return this.annotate(x, y, w, h, annotationOptions);
147159
},
148160

149-
fileAnnotation(x, y, w, h, file = {}, options = {}) {
161+
fileAnnotation(x, y, w, h, file = {}, options) {
150162
// create hidden file
151163
const filespec = this.file(file.src, Object.assign({ hidden: true }, file));
152164

153-
options.Subtype = 'FileAttachment';
154-
options.FS = filespec;
165+
const annotationOptions = {
166+
...options,
167+
Subtype: 'FileAttachment',
168+
FS: filespec,
169+
};
155170

156171
// add description from filespec unless description (Contents) has already been set
157-
if (options.Contents) {
158-
options.Contents = new String(options.Contents);
172+
if (annotationOptions.Contents) {
173+
annotationOptions.Contents = new String(annotationOptions.Contents);
159174
} else if (filespec.data.Desc) {
160-
options.Contents = filespec.data.Desc;
175+
annotationOptions.Contents = new String(filespec.data.Desc);
161176
}
162177

163-
return this.annotate(x, y, w, h, options);
178+
return this.annotate(x, y, w, h, annotationOptions);
164179
},
165180

166181
_convertRect(x1, y1, w, h) {

tests/unit/annotations.spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,30 @@ describe('Annotations', () => {
101101
});
102102
});
103103

104+
describe('note', () => {
105+
test.each([
106+
['null', null],
107+
['undefined', undefined],
108+
])('uses the default icon when Name is %s', (_label, Name) => {
109+
const docData = logData(document);
110+
111+
document.note(10, 30, 30, 30, 'Text of note', { Name });
112+
113+
const dataStr = docData.join('\n');
114+
expect(dataStr).toContain('/Subtype /Text');
115+
expect(dataStr).toContain('/Name /Comment');
116+
expect(dataStr).not.toContain(`/Name ${Name}`);
117+
});
118+
119+
test('does not mutate caller options', () => {
120+
const options = {};
121+
122+
document.note(10, 30, 30, 30, 'Text of note', options);
123+
124+
expect(options).toEqual({});
125+
});
126+
});
127+
104128
describe('fileAnnotation', () => {
105129
test('creating a fileAnnotation', () => {
106130
const docData = logData(document);

0 commit comments

Comments
 (0)