• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

adnsistemas / pdf-lib / #18

24 Mar 2026 08:15PM UTC coverage: 74.286% (+0.3%) from 74.001%
#18

push

David N. Abdala
Documentation change

2569 of 3981 branches covered (64.53%)

Branch coverage included in aggregate %.

7372 of 9401 relevant lines covered (78.42%)

297170.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.62
/src/api/form/PDFField.ts
1
import PDFDocument from '../PDFDocument';
54✔
2
import PDFFont from '../PDFFont';
3
import { AppearanceMapping } from './appearances';
4
import { Color, colorToComponents, setFillingColor } from '../colors';
54✔
5
import {
54✔
6
  Rotation,
7
  toDegrees,
8
  rotateRectangle,
9
  reduceRotation,
10
  adjustDimsForRotation,
11
  degrees,
12
} from '../rotations';
13

14
import {
54✔
15
  PDFRef,
16
  PDFWidgetAnnotation,
17
  PDFOperator,
18
  PDFName,
19
  PDFDict,
20
  MethodNotImplementedError,
21
  AcroFieldFlags,
22
  PDFAcroTerminal,
23
  AnnotationFlags,
24
} from '../../core';
25
import { assertIs, assertMultiple, assertOrUndefined } from '../../utils';
54✔
26
import { ImageAlignment } from '../image';
54✔
27
import PDFImage from '../PDFImage';
28
import { drawImage, rotateInPlace } from '../operations';
54✔
29
import { PDFClasses } from '../objects';
54✔
30

31
export interface FieldAppearanceOptions {
32
  x?: number;
33
  y?: number;
34
  width?: number;
35
  height?: number;
36
  textColor?: Color;
37
  backgroundColor?: Color;
38
  borderColor?: Color;
39
  borderWidth?: number;
40
  rotate?: Rotation;
41
  font?: PDFFont;
42
  hidden?: boolean;
43
}
44

45
export const assertFieldAppearanceOptions = (
54✔
46
  options?: FieldAppearanceOptions,
47
) => {
48
  assertOrUndefined(options?.x, 'options.x', ['number']);
26✔
49
  assertOrUndefined(options?.y, 'options.y', ['number']);
26✔
50
  assertOrUndefined(options?.width, 'options.width', ['number']);
26✔
51
  assertOrUndefined(options?.height, 'options.height', ['number']);
26✔
52
  assertOrUndefined(options?.textColor, 'options.textColor', [
26✔
53
    [Object, 'Color'],
54
  ]);
55
  assertOrUndefined(options?.backgroundColor, 'options.backgroundColor', [
26✔
56
    [Object, 'Color'],
57
  ]);
58
  assertOrUndefined(options?.borderColor, 'options.borderColor', [
26✔
59
    [Object, 'Color'],
60
  ]);
61
  assertOrUndefined(options?.borderWidth, 'options.borderWidth', ['number']);
26✔
62
  assertOrUndefined(options?.rotate, 'options.rotate', [[Object, 'Rotation']]);
26✔
63
};
64

65
/**
66
 * Represents a field of a [[PDFForm]].
67
 *
68
 * This class is effectively abstract. All fields in a [[PDFForm]] will
69
 * actually be an instance of a subclass of this class.
70
 *
71
 * Note that each field in a PDF is represented by a single field object.
72
 * However, a given field object may be rendered at multiple locations within
73
 * the document (across one or more pages). The rendering of a field is
74
 * controlled by its widgets. Each widget causes its field to be displayed at a
75
 * particular location in the document.
76
 *
77
 * Most of the time each field in a PDF has only a single widget, and thus is
78
 * only rendered once. However, if a field is rendered multiple times, it will
79
 * have multiple widgets - one for each location it is rendered.
80
 *
81
 * This abstraction of field objects and widgets is defined in the PDF
82
 * specification and dictates how PDF files store fields and where they are
83
 * to be rendered.
84
 */
85
export default class PDFField {
54✔
86
  static className = () => PDFClasses.PDFField;
54✔
87
  myClass(): PDFClasses {
88
    return PDFClasses.PDFField;
×
89
  }
90
  /** The low-level PDFAcroTerminal wrapped by this field. */
91
  readonly acroField: PDFAcroTerminal;
92

93
  /** The unique reference assigned to this field within the document. */
94
  readonly ref: PDFRef;
95

96
  /** The document to which this field belongs. */
97
  readonly doc: PDFDocument;
98

99
  protected constructor(
100
    acroField: PDFAcroTerminal,
101
    ref: PDFRef,
102
    doc: PDFDocument,
103
  ) {
104
    assertIs(acroField, 'acroField', [[PDFAcroTerminal, 'PDFAcroTerminal']]);
1,274✔
105
    assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]);
1,274✔
106
    assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]);
1,274✔
107

108
    this.acroField = acroField;
1,274✔
109
    this.ref = ref;
1,274✔
110
    this.doc = doc;
1,274✔
111
  }
112

113
  /**
114
   * Get the fully qualified name of this field. For example:
115
   * ```js
116
   * const fields = form.getFields()
117
   * fields.forEach(field => {
118
   *   const name = field.getName()
119
   *   console.log('Field name:', name)
120
   * })
121
   * ```
122
   * Note that PDF fields are structured as a tree. Each field is the
123
   * descendent of a series of ancestor nodes all the way up to the form node,
124
   * which is always the root of the tree. Each node in the tree (except for
125
   * the form node) has a partial name. Partial names can be composed of any
126
   * unicode characters except a period (`.`). The fully qualified name of a
127
   * field is composed of the partial names of all its ancestors joined
128
   * with periods. This means that splitting the fully qualified name on
129
   * periods and taking the last element of the resulting array will give you
130
   * the partial name of a specific field.
131
   * @returns The fully qualified name of this field.
132
   */
133
  getName(): string {
134
    return this.acroField.getFullyQualifiedName() ?? '';
633!
135
  }
136

137
  /**
138
   * Returns `true` if this field is read only. This means that PDF readers
139
   * will not allow users to interact with the field or change its value. See
140
   * [[PDFField.enableReadOnly]] and [[PDFField.disableReadOnly]].
141
   * For example:
142
   * ```js
143
   * const field = form.getField('some.field')
144
   * if (field.isReadOnly()) console.log('Read only is enabled')
145
   * ```
146
   * @returns Whether or not this is a read only field.
147
   */
148
  isReadOnly(): boolean {
149
    return this.acroField.hasFlag(AcroFieldFlags.ReadOnly);
5✔
150
  }
151

152
  /**
153
   * Prevent PDF readers from allowing users to interact with this field or
154
   * change its value. The field will not respond to mouse or keyboard input.
155
   * For example:
156
   * ```js
157
   * const field = form.getField('some.field')
158
   * field.enableReadOnly()
159
   * ```
160
   * Useful for fields whose values are computed, imported from a database, or
161
   * prefilled by software before being displayed to the user.
162
   */
163
  enableReadOnly() {
164
    this.acroField.setFlagTo(AcroFieldFlags.ReadOnly, true);
×
165
  }
166

167
  /**
168
   * Allow users to interact with this field and change its value in PDF
169
   * readers via mouse and keyboard input. For example:
170
   * ```js
171
   * const field = form.getField('some.field')
172
   * field.disableReadOnly()
173
   * ```
174
   */
175
  disableReadOnly() {
176
    this.acroField.setFlagTo(AcroFieldFlags.ReadOnly, false);
×
177
  }
178

179
  /**
180
   * Returns `true` if this field must have a value when the form is submitted.
181
   * See [[PDFField.enableRequired]] and [[PDFField.disableRequired]].
182
   * For example:
183
   * ```js
184
   * const field = form.getField('some.field')
185
   * if (field.isRequired()) console.log('Field is required')
186
   * ```
187
   * @returns Whether or not this field is required.
188
   */
189
  isRequired(): boolean {
190
    return this.acroField.hasFlag(AcroFieldFlags.Required);
5✔
191
  }
192

193
  /**
194
   * Require this field to have a value when the form is submitted.
195
   * For example:
196
   * ```js
197
   * const field = form.getField('some.field')
198
   * field.enableRequired()
199
   * ```
200
   */
201
  enableRequired() {
202
    this.acroField.setFlagTo(AcroFieldFlags.Required, true);
×
203
  }
204

205
  /**
206
   * Do not require this field to have a value when the form is submitted.
207
   * For example:
208
   * ```js
209
   * const field = form.getField('some.field')
210
   * field.disableRequired()
211
   * ```
212
   */
213
  disableRequired() {
214
    this.acroField.setFlagTo(AcroFieldFlags.Required, false);
×
215
  }
216

217
  /**
218
   * Returns `true` if this field's value should be exported when the form is
219
   * submitted. See [[PDFField.enableExporting]] and
220
   * [[PDFField.disableExporting]].
221
   * For example:
222
   * ```js
223
   * const field = form.getField('some.field')
224
   * if (field.isExported()) console.log('Exporting is enabled')
225
   * ```
226
   * @returns Whether or not this field's value should be exported.
227
   */
228
  isExported(): boolean {
229
    return !this.acroField.hasFlag(AcroFieldFlags.NoExport);
5✔
230
  }
231

232
  /**
233
   * Indicate that this field's value should be exported when the form is
234
   * submitted in a PDF reader. For example:
235
   * ```js
236
   * const field = form.getField('some.field')
237
   * field.enableExporting()
238
   * ```
239
   */
240
  enableExporting() {
241
    this.acroField.setFlagTo(AcroFieldFlags.NoExport, false);
×
242
  }
243

244
  /**
245
   * Indicate that this field's value should **not** be exported when the form
246
   * is submitted in a PDF reader. For example:
247
   * ```js
248
   * const field = form.getField('some.field')
249
   * field.disableExporting()
250
   * ```
251
   */
252
  disableExporting() {
253
    this.acroField.setFlagTo(AcroFieldFlags.NoExport, true);
×
254
  }
255

256
  /** @ignore */
257
  needsAppearancesUpdate(): boolean {
258
    throw new MethodNotImplementedError(
×
259
      this.constructor.name,
260
      'needsAppearancesUpdate',
261
    );
262
  }
263

264
  /** @ignore */
265
  defaultUpdateAppearances(_font: PDFFont) {
266
    throw new MethodNotImplementedError(
×
267
      this.constructor.name,
268
      'defaultUpdateAppearances',
269
    );
270
  }
271

272
  protected markAsDirty() {
273
    this.doc.getForm().markFieldAsDirty(this.ref);
29✔
274
  }
275

276
  protected markAsClean() {
277
    this.doc.getForm().markFieldAsClean(this.ref);
14✔
278
  }
279

280
  protected isDirty(): boolean {
281
    return this.doc.getForm().fieldIsDirty(this.ref);
44✔
282
  }
283

284
  protected createWidget(options: {
285
    x: number;
286
    y: number;
287
    width: number;
288
    height: number;
289
    textColor?: Color;
290
    backgroundColor?: Color;
291
    borderColor?: Color;
292
    borderWidth: number;
293
    rotate: Rotation;
294
    caption?: string;
295
    hidden?: boolean;
296
    page?: PDFRef;
297
  }): PDFWidgetAnnotation {
298
    const textColor = options.textColor;
26✔
299
    const backgroundColor = options.backgroundColor;
26✔
300
    const borderColor = options.borderColor;
26✔
301
    const borderWidth = options.borderWidth;
26✔
302
    const degreesAngle = toDegrees(options.rotate);
26✔
303
    const caption = options.caption;
26✔
304
    const x = options.x;
26✔
305
    const y = options.y;
26✔
306
    const width = options.width + borderWidth;
26✔
307
    const height = options.height + borderWidth;
26✔
308
    const hidden = Boolean(options.hidden);
26✔
309
    const pageRef = options.page;
26✔
310

311
    assertMultiple(degreesAngle, 'degreesAngle', 90);
26✔
312

313
    // Create a widget for this field
314
    const widget = PDFWidgetAnnotation.create(this.doc.context, this.ref);
26✔
315

316
    // Set widget properties
317
    const rect = rotateRectangle(
26✔
318
      { x, y, width, height },
319
      borderWidth,
320
      degreesAngle,
321
    );
322
    widget.setRectangle(rect);
26✔
323

324
    if (pageRef) widget.setP(pageRef);
26✔
325

326
    const ac = widget.getOrCreateAppearanceCharacteristics();
26✔
327
    if (backgroundColor) {
26✔
328
      ac.setBackgroundColor(colorToComponents(backgroundColor));
26✔
329
    }
330
    ac.setRotation(degreesAngle);
26✔
331
    if (caption) ac.setCaptions({ normal: caption });
26✔
332
    if (borderColor) ac.setBorderColor(colorToComponents(borderColor));
26✔
333

334
    const bs = widget.getOrCreateBorderStyle();
26✔
335
    if (borderWidth !== undefined) bs.setWidth(borderWidth);
26✔
336

337
    widget.setFlagTo(AnnotationFlags.Print, true);
26✔
338
    widget.setFlagTo(AnnotationFlags.Hidden, hidden);
26✔
339
    widget.setFlagTo(AnnotationFlags.Invisible, false);
26✔
340

341
    // Set acrofield properties
342
    if (textColor) {
26✔
343
      const da = this.acroField.getDefaultAppearance() ?? '';
26✔
344
      const newDa = da + '\n' + setFillingColor(textColor).toString();
26✔
345
      this.acroField.setDefaultAppearance(newDa);
26✔
346
    }
347

348
    return widget;
26✔
349
  }
350

351
  protected updateWidgetAppearanceWithFont(
352
    widget: PDFWidgetAnnotation,
353
    font: PDFFont,
354
    { normal, rollover, down }: AppearanceMapping<PDFOperator[]>,
355
  ) {
356
    this.updateWidgetAppearances(widget, {
32✔
357
      normal: this.createAppearanceStream(widget, normal, font),
358
      rollover: rollover && this.createAppearanceStream(widget, rollover, font),
32!
359
      down: down && this.createAppearanceStream(widget, down, font),
38✔
360
    });
361
  }
362

363
  protected updateOnOffWidgetAppearance(
364
    widget: PDFWidgetAnnotation,
365
    onValue: PDFName,
366
    {
367
      normal,
368
      rollover,
369
      down,
370
    }: AppearanceMapping<{ on: PDFOperator[]; off: PDFOperator[] }>,
371
  ) {
372
    this.updateWidgetAppearances(widget, {
19✔
373
      normal: this.createAppearanceDict(widget, normal, onValue),
374
      rollover:
375
        rollover && this.createAppearanceDict(widget, rollover, onValue),
19!
376
      down: down && this.createAppearanceDict(widget, down, onValue),
38✔
377
    });
378
  }
379

380
  protected updateWidgetAppearances(
381
    widget: PDFWidgetAnnotation,
382
    { normal, rollover, down }: AppearanceMapping<PDFRef | PDFDict>,
383
  ) {
384
    widget.setNormalAppearance(normal);
51✔
385

386
    if (rollover) {
51!
387
      widget.setRolloverAppearance(rollover);
×
388
    } else {
389
      widget.removeRolloverAppearance();
51✔
390
    }
391

392
    if (down) {
51✔
393
      widget.setDownAppearance(down);
25✔
394
    } else {
395
      widget.removeDownAppearance();
26✔
396
    }
397
  }
398

399
  // // TODO: Do we need to do this...?
400
  // private foo(font: PDFFont, dict: PDFDict) {
401
  //   if (!dict.lookup(PDFName.of('DR'))) {
402
  //     dict.set(PDFName.of('DR'), dict.context.obj({}));
403
  //   }
404
  //   const DR = dict.lookup(PDFName.of('DR'), PDFDict);
405

406
  //   if (!DR.lookup(PDFName.of('Font'))) {
407
  //     DR.set(PDFName.of('Font'), dict.context.obj({}));
408
  //   }
409
  //   const Font = DR.lookup(PDFName.of('Font'), PDFDict);
410

411
  //   Font.set(PDFName.of(font.name), font.ref);
412
  // }
413

414
  private createAppearanceStream(
415
    widget: PDFWidgetAnnotation,
416
    appearance: PDFOperator[],
417
    font?: PDFFont,
418
  ): PDFRef {
419
    const { context } = this.acroField.dict;
114✔
420
    const { width, height } = widget.getRectangle();
114✔
421

422
    // TODO: Do we need to do this...?
423
    // if (font) {
424
    //   this.foo(font, widget.dict);
425
    //   this.foo(font, this.doc.getForm().acroForm.dict);
426
    // }
427
    // END TODO
428

429
    const Resources = font && { Font: { [font.name]: font.ref } };
114✔
430
    const stream = context.formXObject(appearance, {
114✔
431
      Resources,
432
      BBox: context.obj([0, 0, width, height]),
433
      Matrix: context.obj([1, 0, 0, 1, 0, 0]),
434
    });
435
    const streamRef = context.register(stream);
114✔
436

437
    return streamRef;
114✔
438
  }
439

440
  /**
441
   * Create a FormXObject of the supplied image and add it to context.
442
   * The FormXObject size is calculated based on the widget (including
443
   * the alignment).
444
   * @param widget The widget that should display the image.
445
   * @param alignment The alignment of the image.
446
   * @param image The image that should be displayed.
447
   * @returns The ref for the FormXObject that was added to the context.
448
   */
449
  protected createImageAppearanceStream(
450
    widget: PDFWidgetAnnotation,
451
    image: PDFImage,
452
    alignment: ImageAlignment,
453
  ): PDFRef {
454
    // NOTE: This implementation doesn't handle image borders.
455
    // NOTE: Acrobat seems to resize the image (maybe even skewing its aspect
456
    //       ratio) to fit perfectly within the widget's rectangle. This method
457
    //       does not currently do that. Should there be an option for that?
458

459
    const { context } = this.acroField.dict;
×
460

461
    const rectangle = widget.getRectangle();
×
462
    const ap = widget.getAppearanceCharacteristics();
×
463
    const bs = widget.getBorderStyle();
×
464

465
    const borderWidth = bs?.getWidth() ?? 0;
×
466
    const rotation = reduceRotation(ap?.getRotation());
×
467

468
    const rotate = rotateInPlace({ ...rectangle, rotation });
×
469

470
    const adj = adjustDimsForRotation(rectangle, rotation);
×
471
    const imageDims = image.scaleToFit(
×
472
      adj.width - borderWidth * 2,
473
      adj.height - borderWidth * 2,
474
    );
475

476
    // Support borders on images and maybe other properties
477
    const options = {
×
478
      x: borderWidth,
479
      y: borderWidth,
480
      width: imageDims.width,
481
      height: imageDims.height,
482
      //
483
      rotate: degrees(0),
484
      xSkew: degrees(0),
485
      ySkew: degrees(0),
486
    };
487

488
    if (alignment === ImageAlignment.Center) {
×
489
      options.x += (adj.width - borderWidth * 2) / 2 - imageDims.width / 2;
×
490
      options.y += (adj.height - borderWidth * 2) / 2 - imageDims.height / 2;
×
491
    } else if (alignment === ImageAlignment.Right) {
×
492
      options.x = adj.width - borderWidth - imageDims.width;
×
493
      options.y = adj.height - borderWidth - imageDims.height;
×
494
    }
495

496
    const imageName = this.doc.context.addRandomSuffix('Image', 10);
×
497
    const appearance = [...rotate, ...drawImage(imageName, options)];
×
498
    ////////////
499

500
    const Resources = { XObject: { [imageName]: image.ref } };
×
501
    const stream = context.formXObject(appearance, {
×
502
      Resources,
503
      BBox: context.obj([0, 0, rectangle.width, rectangle.height]),
504
      Matrix: context.obj([1, 0, 0, 1, 0, 0]),
505
    });
506

507
    return context.register(stream);
×
508
  }
509

510
  private createAppearanceDict(
511
    widget: PDFWidgetAnnotation,
512
    appearance: { on: PDFOperator[]; off: PDFOperator[] },
513
    onValue: PDFName,
514
  ): PDFDict {
515
    const { context } = this.acroField.dict;
38✔
516

517
    const onStreamRef = this.createAppearanceStream(widget, appearance.on);
38✔
518
    const offStreamRef = this.createAppearanceStream(widget, appearance.off);
38✔
519

520
    const appearanceDict = context.obj({});
38✔
521
    appearanceDict.set(onValue, onStreamRef);
38✔
522
    appearanceDict.set(PDFName.of('Off'), offStreamRef);
38✔
523

524
    return appearanceDict;
38✔
525
  }
526
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc