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

adnsistemas / pdf-lib / #20

25 Mar 2026 02:58PM UTC coverage: 74.304%. Remained the same
#20

push

adnsistemas
Change in Maps to use strings, instead of objects, as keys

2575 of 3988 branches covered (64.57%)

Branch coverage included in aggregate %.

47 of 49 new or added lines in 3 files covered. (95.92%)

6 existing lines in 2 files now uncovered.

7381 of 9411 relevant lines covered (78.43%)

297523.91 hits per line

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

81.66
/src/api/PDFDocument.ts
1
import {
54✔
2
  parse as parseHtml,
3
  HTMLElement,
4
  NodeType,
5
} from 'node-html-better-parser';
6
import Embeddable from './Embeddable';
7
import {
54✔
8
  EncryptedPDFError,
9
  FontkitNotRegisteredError,
10
  ForeignPageError,
11
  RemovePageFromEmptyDocumentError,
12
} from './errors';
13
import PDFEmbeddedPage from './PDFEmbeddedPage';
54✔
14
import PDFFont from './PDFFont';
54✔
15
import PDFImage from './PDFImage';
54✔
16
import PDFPage from './PDFPage';
54✔
17
import PDFForm from './form/PDFForm';
54✔
18
import { PageSizes } from './sizes';
54✔
19
import { StandardFonts } from './StandardFonts';
20
import {
54✔
21
  CustomFontEmbedder,
22
  CustomFontSubsetEmbedder,
23
  JpegEmbedder,
24
  PageBoundingBox,
25
  PageEmbeddingMismatchedContextError,
26
  PDFArray,
27
  PDFCatalog,
28
  PDFContext,
29
  PDFDict,
30
  decodePDFRawStream,
31
  PDFStream,
32
  PDFRawStream,
33
  PDFHexString,
34
  PDFName,
35
  PDFObjectCopier,
36
  PDFPageEmbedder,
37
  PDFPageLeaf,
38
  PDFPageTree,
39
  PDFParser,
40
  PDFStreamWriter,
41
  PDFString,
42
  PDFWriter,
43
  PngEmbedder,
44
  StandardFontEmbedder,
45
  UnexpectedObjectTypeError,
46
} from '../core';
47
import {
54✔
48
  ParseSpeeds,
49
  AttachmentOptions,
50
  SaveOptions,
51
  Base64SaveOptions,
52
  LoadOptions,
53
  CreateOptions,
54
  EmbedFontOptions,
55
  SetTitleOptions,
56
  IncrementalSaveOptions,
57
} from './PDFDocumentOptions';
58
import PDFObject from '../core/objects/PDFObject';
59
import PDFRef from '../core/objects/PDFRef';
60
import { Fontkit } from '../types/fontkit';
61
import { TransformationMatrix } from '../types/matrix';
62
import {
54✔
63
  assertIs,
64
  assertIsOneOfOrUndefined,
65
  assertOrUndefined,
66
  assertRange,
67
  Cache,
68
  canBeConvertedToUint8Array,
69
  encodeToBase64,
70
  isStandardFont,
71
  pluckIndices,
72
  range,
73
  toUint8Array,
74
} from '../utils';
75
import FileEmbedder, { AFRelationship } from '../core/embedders/FileEmbedder';
54✔
76
import PDFEmbeddedFile from './PDFEmbeddedFile';
54✔
77
import PDFJavaScript from './PDFJavaScript';
54✔
78
import JavaScriptEmbedder from '../core/embedders/JavaScriptEmbedder';
54✔
79
import { CipherTransformFactory } from '../core/crypto';
54✔
80
import PDFSvg from './PDFSvg';
54✔
81
import PDFSecurity, { SecurityOptions } from '../core/security/PDFSecurity';
54✔
82
import { IncrementalDocumentSnapshot } from './snapshot';
54✔
83
import type { DocumentSnapshot } from './snapshot';
84
import { isPDFInstance, PDFClasses } from './objects';
54✔
85

86
export type BasePDFAttachment = {
87
  name: string;
88
  data: Uint8Array;
89
  mimeType: string | undefined;
90
  afRelationship: AFRelationship | undefined;
91
  description: string | undefined;
92
  creationDate: Date | undefined;
93
  modificationDate: Date | undefined;
94
};
95

96
export type SavedPDFAttachment = BasePDFAttachment & {
97
  embeddedFileDict: PDFDict;
98
  specRef: PDFRef;
99
};
100

101
export type UnsavedPDFAttachment = BasePDFAttachment & {
102
  pdfEmbeddedFile: PDFEmbeddedFile;
103
};
104

105
export type PDFAttachment = UnsavedPDFAttachment | SavedPDFAttachment;
106

107
export type PDFObjectVersions = {
108
  ref: PDFRef;
109
  actual: PDFObject | undefined;
110
  previous: PDFObject[];
111
};
112

113
/**
114
 * Represents a PDF document.
115
 */
116
export default class PDFDocument {
54✔
117
  /**
118
   * Load an existing [[PDFDocument]]. The input data can be provided in
119
   * multiple formats:
120
   *
121
   * | Type          | Contents                                               |
122
   * | ------------- | ------------------------------------------------------ |
123
   * | `string`      | A base64 encoded string (or data URI) containing a PDF |
124
   * | `Uint8Array`  | The raw bytes of a PDF                                 |
125
   * | `ArrayBuffer` | The raw bytes of a PDF                                 |
126
   *
127
   * For example:
128
   * ```js
129
   * import { PDFDocument } from 'pdf-lib'
130
   *
131
   * // pdf=string
132
   * const base64 =
133
   *  'JVBERi0xLjcKJYGBgYEKCjUgMCBvYmoKPDwKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbm' +
134
   *  'd0aCAxMDQKPj4Kc3RyZWFtCniccwrhMlAAwaJ0Ln2P1Jyy1JLM5ERdc0MjCwUjE4WQNC4Q' +
135
   *  '6cNlCFZkqGCqYGSqEJLLZWNuYGZiZmbkYuZsZmlmZGRgZmluDCQNzc3NTM2NzdzMXMxMjQ' +
136
   *  'ztFEKyuEK0uFxDuAAOERdVCmVuZHN0cmVhbQplbmRvYmoKCjYgMCBvYmoKPDwKL0ZpbHRl' +
137
   *  'ciAvRmxhdGVEZWNvZGUKL1R5cGUgL09ialN0bQovTiA0Ci9GaXJzdCAyMAovTGVuZ3RoID' +
138
   *  'IxNQo+PgpzdHJlYW0KeJxVj9GqwjAMhu/zFHkBzTo3nCCCiiKIHPEICuJF3cKoSCu2E8/b' +
139
   *  '20wPIr1p8v9/8kVhgilmGfawX2CGaVrgcAi0/bsy0lrX7IGWpvJ4iJYEN3gEmrrGBlQwGs' +
140
   *  'HHO9VBX1wNrxAqMX87RBD5xpJuddqwd82tjAHxzV1U5LPgy52DKXWnr1Lheg+j/c/pzGVr' +
141
   *  'iqV0VlwZPXGPCJjElw/ybkwUmeoWgxesDXGhHJC/D/iikp1Av80ptKU0FdBEe25pPihAM1' +
142
   *  'u6ytgaaWfs2Hrz35CJT1+EWmAKZW5kc3RyZWFtCmVuZG9iagoKNyAwIG9iago8PAovU2l6' +
143
   *  'ZSA4Ci9Sb290IDIgMCBSCi9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9UeXBlIC9YUmVmCi9MZW' +
144
   *  '5ndGggMzgKL1cgWyAxIDIgMiBdCi9JbmRleCBbIDAgOCBdCj4+CnN0cmVhbQp4nBXEwREA' +
145
   *  'EBAEsCwz3vrvRmOOyyOoGhZdutHN2MT55fIAVocD+AplbmRzdHJlYW0KZW5kb2JqCgpzdG' +
146
   *  'FydHhyZWYKNTEwCiUlRU9G'
147
   *
148
   * const dataUri = 'data:application/pdf;base64,' + base64
149
   *
150
   * const pdfDoc1 = await PDFDocument.load(base64)
151
   * const pdfDoc2 = await PDFDocument.load(dataUri)
152
   *
153
   * // pdf=Uint8Array
154
   * import fs from 'fs'
155
   * const uint8Array = fs.readFileSync('with_update_sections.pdf')
156
   * const pdfDoc3 = await PDFDocument.load(uint8Array)
157
   *
158
   * // pdf=ArrayBuffer
159
   * const url = 'https://pdf-lib.js.org/assets/with_update_sections.pdf'
160
   * const arrayBuffer = await fetch(url).then(res => res.arrayBuffer())
161
   * const pdfDoc4 = await PDFDocument.load(arrayBuffer)
162
   *
163
   * ```
164
   *
165
   * @param pdf The input data containing a PDF document.
166
   * @param options The options to be used when loading the document.
167
   * @returns Resolves with a document loaded from the input.
168
   */
169
  static async load(
170
    pdf: string | Uint8Array | ArrayBuffer,
171
    options: LoadOptions = {},
91✔
172
  ) {
173
    const {
174
      ignoreEncryption = false,
144✔
175
      parseSpeed = ParseSpeeds.Slow,
137✔
176
      throwOnInvalidObject = false,
146✔
177
      warnOnInvalidObjects = false,
148✔
178
      updateMetadata = true,
147✔
179
      capNumbers = false,
148✔
180
      password,
181
      forIncrementalUpdate = false,
108✔
182
      preserveObjectsVersions = false,
143✔
183
    } = options;
148✔
184

185
    assertIs(pdf, 'pdf', ['string', Uint8Array, ArrayBuffer]);
148✔
186
    assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']);
148✔
187
    assertIs(parseSpeed, 'parseSpeed', ['number']);
148✔
188
    assertIs(throwOnInvalidObject, 'throwOnInvalidObject', ['boolean']);
148✔
189
    assertIs(warnOnInvalidObjects, 'warnOnInvalidObjects', ['boolean']);
148✔
190
    assertIs(password, 'password', ['string', 'undefined']);
148✔
191
    assertIs(forIncrementalUpdate, 'forIncrementalUpdate', ['boolean']);
148✔
192
    assertIs(preserveObjectsVersions, 'preserveObjectsVersions', ['boolean']);
148✔
193

194
    const bytes = toUint8Array(pdf);
148✔
195
    const context = await PDFParser.forBytesWithOptions(
148✔
196
      bytes,
197
      parseSpeed,
198
      throwOnInvalidObject,
199
      undefined,
200
      capNumbers,
201
      undefined,
202
      forIncrementalUpdate,
203
      preserveObjectsVersions,
204
    ).parseDocument();
205
    if (
147✔
206
      !!context.lookup(context.trailerInfo.Encrypt) &&
153✔
207
      password !== undefined
208
    ) {
209
      // Decrypt
210
      const fileIds = context.lookup(context.trailerInfo.ID, PDFArray);
1✔
211
      const encryptDict = context.lookup(context.trailerInfo.Encrypt, PDFDict);
1✔
212
      const decryptedContext = await PDFParser.forBytesWithOptions(
1✔
213
        bytes,
214
        parseSpeed,
215
        throwOnInvalidObject,
216
        warnOnInvalidObjects,
217
        capNumbers,
218
        new CipherTransformFactory(
219
          encryptDict,
220
          (fileIds.get(0) as PDFHexString).asBytes(),
221
          password,
222
        ),
223
        forIncrementalUpdate,
224
        preserveObjectsVersions,
225
      ).parseDocument();
226
      const pdfDoc = new PDFDocument(decryptedContext, true, updateMetadata);
1✔
227
      if (forIncrementalUpdate) pdfDoc.takeSnapshot();
1!
228
      return pdfDoc;
1✔
229
    } else {
230
      const pdfDoc = new PDFDocument(context, ignoreEncryption, updateMetadata);
146✔
231
      if (forIncrementalUpdate) pdfDoc.takeSnapshot();
143✔
232
      return pdfDoc;
143✔
233
    }
234
  }
235

236
  /**
237
   * Create a new [[PDFDocument]].
238
   * @returns Resolves with the newly created document.
239
   */
240
  static async create(options: CreateOptions = {}) {
52✔
241
    const { updateMetadata = true } = options;
57✔
242

243
    const context = PDFContext.create();
57✔
244
    const pageTree = PDFPageTree.withContext(context);
57✔
245
    const pageTreeRef = context.register(pageTree);
57✔
246
    const catalog = PDFCatalog.withContextAndPages(context, pageTreeRef);
57✔
247
    context.trailerInfo.Root = context.register(catalog);
57✔
248

249
    return new PDFDocument(context, false, updateMetadata);
57✔
250
  }
251
  static className = () => PDFClasses.PDFDocument;
54✔
252

253
  myClass(): PDFClasses {
254
    return PDFClasses.PDFDocument;
×
255
  }
256

257
  /** The low-level context of this document. */
258
  readonly context: PDFContext;
259

260
  /** The catalog of this document. */
261
  readonly catalog: PDFCatalog;
262

263
  /** Whether or not this document is encrypted. */
264
  readonly isEncrypted: boolean;
265

266
  /** The default word breaks used in PDFPage.drawText */
267
  defaultWordBreaks: string[] = [' '];
204✔
268

269
  private fontkit?: Fontkit;
270
  private pageCount: number | undefined;
271
  private readonly pageCache: Cache<PDFPage[]>;
272
  private readonly pageMap: Map<PDFPageLeaf, PDFPage>;
273
  private readonly formCache: Cache<PDFForm>;
274
  private readonly fonts: PDFFont[];
275
  private readonly images: PDFImage[];
276
  private readonly embeddedPages: PDFEmbeddedPage[];
277
  private readonly embeddedFiles: PDFEmbeddedFile[];
278
  private readonly javaScripts: PDFJavaScript[];
279

280
  private constructor(
281
    context: PDFContext,
282
    ignoreEncryption: boolean,
283
    updateMetadata: boolean,
284
  ) {
285
    assertIs(context, 'context', [[PDFContext, 'PDFContext']]);
204✔
286
    assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']);
204✔
287

288
    this.context = context;
204✔
289
    this.catalog = context.lookup(context.trailerInfo.Root) as PDFCatalog;
204✔
290

291
    if (!!context.lookup(context.trailerInfo.Encrypt) && context.isDecrypted) {
204✔
292
      // context.delete(context.trailerInfo.Encrypt);
293
      delete context.trailerInfo.Encrypt;
1✔
294
    }
295
    this.isEncrypted = !!context.lookup(context.trailerInfo.Encrypt);
204✔
296

297
    this.pageCache = Cache.populatedBy(this.computePages);
204✔
298
    this.pageMap = new Map();
204✔
299
    this.formCache = Cache.populatedBy(this.getOrCreateForm);
204✔
300
    this.fonts = [];
204✔
301
    this.images = [];
204✔
302
    this.embeddedPages = [];
204✔
303
    this.embeddedFiles = [];
204✔
304
    this.javaScripts = [];
204✔
305

306
    if (!ignoreEncryption && this.isEncrypted) throw new EncryptedPDFError();
204✔
307

308
    if (updateMetadata) this.updateInfoDict();
201✔
309
  }
310

311
  /**
312
   * Register a fontkit instance. This must be done before custom fonts can
313
   * be embedded. See [here](https://github.com/Hopding/pdf-lib/tree/master#fontkit-installation)
314
   * for instructions on how to install and register a fontkit instance.
315
   *
316
   * > You do **not** need to call this method to embed standard fonts.
317
   *
318
   * For example:
319
   * ```js
320
   * import { PDFDocument } from 'pdf-lib'
321
   * import fontkit from '@pdf-lib/fontkit'
322
   *
323
   * const pdfDoc = await PDFDocument.create()
324
   * pdfDoc.registerFontkit(fontkit)
325
   * ```
326
   *
327
   * @param fontkit The fontkit instance to be registered.
328
   */
329
  registerFontkit(fontkit: Fontkit): void {
330
    this.fontkit = fontkit;
3✔
331
  }
332

333
  /**
334
   * Get the [[PDFForm]] containing all interactive fields for this document.
335
   * For example:
336
   * ```js
337
   * const form = pdfDoc.getForm()
338
   * const fields = form.getFields()
339
   * fields.forEach(field => {
340
   *   const type = field.constructor.name
341
   *   const name = field.getName()
342
   *   console.log(`${type}: ${name}`)
343
   * })
344
   * ```
345
   * @returns The form for this document.
346
   */
347
  getForm(): PDFForm {
348
    const form = this.formCache.access();
151✔
349
    if (form.hasXFA()) {
151✔
350
      console.warn(
1✔
351
        'Removing XFA form data as pdf-lib does not support reading or writing XFA',
352
      );
353
      form.deleteXFA();
1✔
354
    }
355
    return form;
151✔
356
  }
357

358
  /**
359
   * Get this document's title metadata. The title appears in the
360
   * "Document Properties" section of most PDF readers. For example:
361
   * ```js
362
   * const title = pdfDoc.getTitle()
363
   * ```
364
   * @returns A string containing the title of this document, if it has one.
365
   */
366
  getTitle(): string | undefined {
367
    const title = this.getInfoDict().lookup(PDFName.Title);
10✔
368
    if (!title) return undefined;
10✔
369
    assertIsLiteralOrHexString(title);
9✔
370
    return title.decodeText();
9✔
371
  }
372

373
  /**
374
   * Get this document's author metadata. The author appears in the
375
   * "Document Properties" section of most PDF readers. For example:
376
   * ```js
377
   * const author = pdfDoc.getAuthor()
378
   * ```
379
   * @returns A string containing the author of this document, if it has one.
380
   */
381
  getAuthor(): string | undefined {
382
    const author = this.getInfoDict().lookup(PDFName.Author);
9✔
383
    if (!author) return undefined;
9✔
384
    assertIsLiteralOrHexString(author);
8✔
385
    return author.decodeText();
8✔
386
  }
387

388
  /**
389
   * Get this document's subject metadata. The subject appears in the
390
   * "Document Properties" section of most PDF readers. For example:
391
   * ```js
392
   * const subject = pdfDoc.getSubject()
393
   * ```
394
   * @returns A string containing the subject of this document, if it has one.
395
   */
396
  getSubject(): string | undefined {
397
    const subject = this.getInfoDict().lookup(PDFName.Subject);
7✔
398
    if (!subject) return undefined;
7✔
399
    assertIsLiteralOrHexString(subject);
6✔
400
    return subject.decodeText();
6✔
401
  }
402

403
  /**
404
   * Get this document's keywords metadata. The keywords appear in the
405
   * "Document Properties" section of most PDF readers. For example:
406
   * ```js
407
   * const keywords = pdfDoc.getKeywords()
408
   * ```
409
   * @returns A string containing the keywords of this document, if it has any.
410
   */
411
  getKeywords(): string | undefined {
412
    const keywords = this.getInfoDict().lookup(PDFName.Keywords);
3✔
413
    if (!keywords) return undefined;
3✔
414
    assertIsLiteralOrHexString(keywords);
2✔
415
    return keywords.decodeText();
2✔
416
  }
417

418
  /**
419
   * Get this document's creator metadata. The creator appears in the
420
   * "Document Properties" section of most PDF readers. For example:
421
   * ```js
422
   * const creator = pdfDoc.getCreator()
423
   * ```
424
   * @returns A string containing the creator of this document, if it has one.
425
   */
426
  getCreator(): string | undefined {
427
    const creator = this.getInfoDict().lookup(PDFName.Creator);
7✔
428
    if (!creator) return undefined;
7!
429
    assertIsLiteralOrHexString(creator);
7✔
430
    return creator.decodeText();
7✔
431
  }
432

433
  /**
434
   * Get this document's producer metadata. The producer appears in the
435
   * "Document Properties" section of most PDF readers. For example:
436
   * ```js
437
   * const producer = pdfDoc.getProducer()
438
   * ```
439
   * @returns A string containing the producer of this document, if it has one.
440
   */
441
  getProducer(): string | undefined {
442
    const producer = this.getInfoDict().lookup(PDFName.Producer);
7✔
443
    if (!producer) return undefined;
7!
444
    assertIsLiteralOrHexString(producer);
7✔
445
    return producer.decodeText();
7✔
446
  }
447

448
  /**
449
   * Get this document's language metadata. The language appears in the
450
   * "Document Properties" section of most PDF readers. For example:
451
   * ```js
452
   * const language = pdfDoc.getLanguage()
453
   * ```
454
   * @returns A string containing the RFC 3066 _Language-Tag_ of this document,
455
   *          if it has one.
456
   */
457
  getLanguage(): string | undefined {
458
    const language = this.catalog.get(PDFName.of('Lang'));
5✔
459
    if (!language) return undefined;
5✔
460
    assertIsLiteralOrHexString(language);
3✔
461
    return language.decodeText();
3✔
462
  }
463

464
  /**
465
   * Get this document's creation date metadata. The creation date appears in
466
   * the "Document Properties" section of most PDF readers. For example:
467
   * ```js
468
   * const creationDate = pdfDoc.getCreationDate()
469
   * ```
470
   * @returns A Date containing the creation date of this document,
471
   *          if it has one.
472
   */
473
  getCreationDate(): Date | undefined {
474
    const creationDate = this.getInfoDict().lookup(PDFName.CreationDate);
7✔
475
    if (!creationDate) return undefined;
7!
476
    assertIsLiteralOrHexString(creationDate);
7✔
477
    return creationDate.decodeDate();
7✔
478
  }
479

480
  /**
481
   * Get this document's modification date metadata. The modification date
482
   * appears in the "Document Properties" section of most PDF readers.
483
   * For example:
484
   * ```js
485
   * const modification = pdfDoc.getModificationDate()
486
   * ```
487
   * @returns A Date containing the modification date of this document,
488
   *          if it has one.
489
   */
490
  getModificationDate(): Date | undefined {
491
    const modificationDate = this.getInfoDict().lookup(PDFName.ModDate);
6✔
492
    if (!modificationDate) return undefined;
6!
493
    assertIsLiteralOrHexString(modificationDate);
6✔
494
    return modificationDate.decodeDate();
6✔
495
  }
496

497
  /**
498
   * Set this document's title metadata. The title will appear in the
499
   * "Document Properties" section of most PDF readers. For example:
500
   * ```js
501
   * pdfDoc.setTitle('🥚 The Life of an Egg 🍳')
502
   * ```
503
   *
504
   * To display the title in the window's title bar, set the
505
   * `showInWindowTitleBar` option to `true` (works for _most_ PDF readers).
506
   * For example:
507
   * ```js
508
   * pdfDoc.setTitle('🥚 The Life of an Egg 🍳', { showInWindowTitleBar: true })
509
   * ```
510
   *
511
   * @param title The title of this document.
512
   * @param options The options to be used when setting the title.
513
   */
514
  setTitle(title: string, options?: SetTitleOptions): void {
515
    assertIs(title, 'title', ['string']);
10✔
516
    const key = PDFName.of('Title');
10✔
517
    this.getInfoDict().set(key, PDFHexString.fromText(title));
10✔
518

519
    // Indicate that readers should display the title rather than the filename
520
    if (options?.showInWindowTitleBar) {
10✔
521
      const prefs = this.catalog.getOrCreateViewerPreferences();
1✔
522
      prefs.setDisplayDocTitle(true);
1✔
523
    }
524
  }
525

526
  /**
527
   * Set this document's author metadata. The author will appear in the
528
   * "Document Properties" section of most PDF readers. For example:
529
   * ```js
530
   * pdfDoc.setAuthor('Humpty Dumpty')
531
   * ```
532
   * @param author The author of this document.
533
   */
534
  setAuthor(author: string): void {
535
    assertIs(author, 'author', ['string']);
5✔
536
    const key = PDFName.of('Author');
5✔
537
    this.getInfoDict().set(key, PDFHexString.fromText(author));
5✔
538
  }
539

540
  /**
541
   * Set this document's subject metadata. The subject will appear in the
542
   * "Document Properties" section of most PDF readers. For example:
543
   * ```js
544
   * pdfDoc.setSubject('📘 An Epic Tale of Woe 📖')
545
   * ```
546
   * @param subject The subject of this document.
547
   */
548
  setSubject(subject: string): void {
549
    assertIs(subject, 'author', ['string']);
3✔
550
    const key = PDFName.of('Subject');
3✔
551
    this.getInfoDict().set(key, PDFHexString.fromText(subject));
3✔
552
  }
553

554
  /**
555
   * Set this document's keyword metadata. These keywords will appear in the
556
   * "Document Properties" section of most PDF readers. For example:
557
   * ```js
558
   * pdfDoc.setKeywords(['eggs', 'wall', 'fall', 'king', 'horses', 'men'])
559
   * ```
560
   * @param keywords An array of keywords associated with this document.
561
   */
562
  setKeywords(keywords: string[]): void {
563
    assertIs(keywords, 'keywords', [Array]);
2✔
564
    const key = PDFName.of('Keywords');
2✔
565
    this.getInfoDict().set(key, PDFHexString.fromText(keywords.join(' ')));
2✔
566
  }
567

568
  /**
569
   * Set this document's creator metadata. The creator will appear in the
570
   * "Document Properties" section of most PDF readers. For example:
571
   * ```js
572
   * pdfDoc.setCreator('PDF App 9000 🤖')
573
   * ```
574
   * @param creator The creator of this document.
575
   */
576
  setCreator(creator: string): void {
577
    assertIs(creator, 'creator', ['string']);
72✔
578
    const key = PDFName.of('Creator');
72✔
579
    this.getInfoDict().set(key, PDFHexString.fromText(creator));
72✔
580
  }
581

582
  /**
583
   * Set this document's producer metadata. The producer will appear in the
584
   * "Document Properties" section of most PDF readers. For example:
585
   * ```js
586
   * pdfDoc.setProducer('PDF App 9000 🤖')
587
   * ```
588
   * @param producer The producer of this document.
589
   */
590
  setProducer(producer: string): void {
591
    assertIs(producer, 'creator', ['string']);
198✔
592
    const key = PDFName.of('Producer');
198✔
593
    this.getInfoDict().set(key, PDFHexString.fromText(producer));
198✔
594
  }
595

596
  /**
597
   * Set this document's language metadata. The language will appear in the
598
   * "Document Properties" section of some PDF readers. For example:
599
   * ```js
600
   * pdfDoc.setLanguage('en-us')
601
   * ```
602
   *
603
   * @param language An RFC 3066 _Language-Tag_ denoting the language of this
604
   *                 document, or an empty string if the language is unknown.
605
   */
606
  setLanguage(language: string): void {
607
    assertIs(language, 'language', ['string']);
3✔
608
    const key = PDFName.of('Lang');
3✔
609
    this.catalog.set(key, PDFString.of(language));
3✔
610
  }
611

612
  /**
613
   * Set this document's creation date metadata. The creation date will appear
614
   * in the "Document Properties" section of most PDF readers. For example:
615
   * ```js
616
   * pdfDoc.setCreationDate(new Date())
617
   * ```
618
   * @param creationDate The date this document was created.
619
   */
620
  setCreationDate(creationDate: Date): void {
621
    assertIs(creationDate, 'creationDate', [[Date, 'Date']]);
58✔
622
    const key = PDFName.of('CreationDate');
58✔
623
    this.getInfoDict().set(key, PDFString.fromDate(creationDate));
58✔
624
  }
625

626
  /**
627
   * Set this document's modification date metadata. The modification date will
628
   * appear in the "Document Properties" section of most PDF readers. For
629
   * example:
630
   * ```js
631
   * pdfDoc.setModificationDate(new Date())
632
   * ```
633
   * @param modificationDate The date this document was last modified.
634
   */
635
  setModificationDate(modificationDate: Date): void {
636
    assertIs(modificationDate, 'modificationDate', [[Date, 'Date']]);
198✔
637
    const key = PDFName.of('ModDate');
198✔
638
    this.getInfoDict().set(key, PDFString.fromDate(modificationDate));
198✔
639
  }
640

641
  /**
642
   * Get the number of pages contained in this document. For example:
643
   * ```js
644
   * const totalPages = pdfDoc.getPageCount()
645
   * ```
646
   * @returns The number of pages in this document.
647
   */
648
  getPageCount(): number {
649
    if (this.pageCount === undefined) this.pageCount = this.getPages().length;
189✔
650
    return this.pageCount;
189✔
651
  }
652

653
  /**
654
   * Get an array of all the pages contained in this document. The pages are
655
   * stored in the array in the same order that they are rendered in the
656
   * document. For example:
657
   * ```js
658
   * const pages = pdfDoc.getPages()
659
   * pages[0]   // The first page of the document
660
   * pages[2]   // The third page of the document
661
   * pages[197] // The 198th page of the document
662
   * ```
663
   * @returns An array of all the pages contained in this document.
664
   */
665
  getPages(): PDFPage[] {
666
    return this.pageCache.access();
162✔
667
  }
668

669
  /**
670
   * Get the page rendered at a particular `index` of the document. For example:
671
   * ```js
672
   * pdfDoc.getPage(0)   // The first page of the document
673
   * pdfDoc.getPage(2)   // The third page of the document
674
   * pdfDoc.getPage(197) // The 198th page of the document
675
   * ```
676
   * @returns The [[PDFPage]] rendered at the given `index` of the document.
677
   */
678
  getPage(index: number): PDFPage {
679
    const pages = this.getPages();
59✔
680
    assertRange(index, 'index', 0, pages.length - 1);
59✔
681
    return pages[index];
59✔
682
  }
683

684
  /**
685
   * Get an array of indices for all the pages contained in this document. The
686
   * array will contain a range of integers from
687
   * `0..pdfDoc.getPageCount() - 1`. For example:
688
   * ```js
689
   * const pdfDoc = await PDFDocument.create()
690
   * pdfDoc.addPage()
691
   * pdfDoc.addPage()
692
   * pdfDoc.addPage()
693
   *
694
   * const indices = pdfDoc.getPageIndices()
695
   * indices // => [0, 1, 2]
696
   * ```
697
   * @returns An array of indices for all pages contained in this document.
698
   */
699
  getPageIndices(): number[] {
700
    return range(0, this.getPageCount());
1✔
701
  }
702

703
  /**
704
   * Remove the page at a given index from this document. For example:
705
   * ```js
706
   * pdfDoc.removePage(0)   // Remove the first page of the document
707
   * pdfDoc.removePage(2)   // Remove the third page of the document
708
   * pdfDoc.removePage(197) // Remove the 198th page of the document
709
   * ```
710
   * Once a page has been removed, it will no longer be rendered at that index
711
   * in the document.
712
   * @param index The index of the page to be removed.
713
   */
714
  removePage(index: number): void {
715
    const pageCount = this.getPageCount();
5✔
716
    if (this.pageCount === 0) throw new RemovePageFromEmptyDocumentError();
5✔
717
    assertRange(index, 'index', 0, pageCount - 1);
4✔
718
    const page = this.getPage(index);
4✔
719
    this.catalog.removeLeafNode(index);
4✔
720
    this.pageCount = pageCount - 1;
4✔
721
    this.context.delete(page.ref);
4✔
722
  }
723

724
  /**
725
   * Add a page to the end of this document. This method accepts three
726
   * different value types for the `page` parameter:
727
   *
728
   * | Type               | Behavior                                                                            |
729
   * | ------------------ | ----------------------------------------------------------------------------------- |
730
   * | `undefined`        | Create a new page and add it to the end of this document                            |
731
   * | `[number, number]` | Create a new page with the given dimensions and add it to the end of this document  |
732
   * | `PDFPage`          | Add the existing page to the end of this document                                   |
733
   *
734
   * For example:
735
   * ```js
736
   * // page=undefined
737
   * const newPage = pdfDoc.addPage()
738
   *
739
   * // page=[number, number]
740
   * import { PageSizes } from 'pdf-lib'
741
   * const newPage1 = pdfDoc.addPage(PageSizes.A7)
742
   * const newPage2 = pdfDoc.addPage(PageSizes.Letter)
743
   * const newPage3 = pdfDoc.addPage([500, 750])
744
   *
745
   * // page=PDFPage
746
   * const pdfDoc1 = await PDFDocument.create()
747
   * const pdfDoc2 = await PDFDocument.load(...)
748
   * const [existingPage] = await pdfDoc1.copyPages(pdfDoc2, [0])
749
   * pdfDoc1.addPage(existingPage)
750
   * ```
751
   *
752
   * @param page Optionally, the desired dimensions or existing page.
753
   * @returns The newly created (or existing) page.
754
   */
755
  addPage(page?: PDFPage | [number, number]): PDFPage {
756
    assertIs(page, 'page', ['undefined', [PDFPage, 'PDFPage'], Array]);
49✔
757
    return this.insertPage(this.getPageCount(), page);
49✔
758
  }
759

760
  /**
761
   * Insert a page at a given index within this document. This method accepts
762
   * three different value types for the `page` parameter:
763
   *
764
   * | Type               | Behavior                                                                       |
765
   * | ------------------ | ------------------------------------------------------------------------------ |
766
   * | `undefined`        | Create a new page and insert it into this document                             |
767
   * | `[number, number]` | Create a new page with the given dimensions and insert it into this document   |
768
   * | `PDFPage`          | Insert the existing page into this document                                    |
769
   *
770
   * For example:
771
   * ```js
772
   * // page=undefined
773
   * const newPage = pdfDoc.insertPage(2)
774
   *
775
   * // page=[number, number]
776
   * import { PageSizes } from 'pdf-lib'
777
   * const newPage1 = pdfDoc.insertPage(2, PageSizes.A7)
778
   * const newPage2 = pdfDoc.insertPage(0, PageSizes.Letter)
779
   * const newPage3 = pdfDoc.insertPage(198, [500, 750])
780
   *
781
   * // page=PDFPage
782
   * const pdfDoc1 = await PDFDocument.create()
783
   * const pdfDoc2 = await PDFDocument.load(...)
784
   * const [existingPage] = await pdfDoc1.copyPages(pdfDoc2, [0])
785
   * pdfDoc1.insertPage(0, existingPage)
786
   * ```
787
   *
788
   * @param index The index at which the page should be inserted (zero-based).
789
   * @param page Optionally, the desired dimensions or existing page.
790
   * @returns The newly created (or existing) page.
791
   */
792
  insertPage(index: number, page?: PDFPage | [number, number]): PDFPage {
793
    const pageCount = this.getPageCount();
52✔
794
    assertRange(index, 'index', 0, pageCount);
52✔
795
    assertIs(page, 'page', ['undefined', [PDFPage, 'PDFPage'], Array]);
52✔
796
    if (!page || Array.isArray(page)) {
52✔
797
      const dims = Array.isArray(page) ? page : PageSizes.A4;
50✔
798
      page = PDFPage.create(this);
50✔
799
      page.setSize(...dims);
50✔
800
    } else if (page.doc !== this) {
2!
801
      throw new ForeignPageError();
×
802
    }
803

804
    const parentRef = this.catalog.insertLeafNode(page.ref, index);
52✔
805
    page.node.setParent(parentRef);
52✔
806

807
    this.pageMap.set(page.node, page);
52✔
808
    this.pageCache.invalidate();
52✔
809

810
    this.pageCount = pageCount + 1;
52✔
811

812
    return page;
52✔
813
  }
814

815
  /**
816
   * Copy pages from a source document into this document. Allows pages to be
817
   * copied between different [[PDFDocument]] instances. For example:
818
   * ```js
819
   * const pdfDoc = await PDFDocument.create()
820
   * const srcDoc = await PDFDocument.load(...)
821
   *
822
   * const copiedPages = await pdfDoc.copyPages(srcDoc, [0, 3, 89])
823
   * const [firstPage, fourthPage, ninetiethPage] = copiedPages;
824
   *
825
   * pdfDoc.addPage(fourthPage)
826
   * pdfDoc.insertPage(0, ninetiethPage)
827
   * pdfDoc.addPage(firstPage)
828
   * ```
829
   * @param srcDoc The document from which pages should be copied.
830
   * @param indices The indices of the pages that should be copied.
831
   * @returns Resolves with an array of pages copied into this document.
832
   */
833
  async copyPages(srcDoc: PDFDocument, indices: number[]): Promise<PDFPage[]> {
834
    assertIs(srcDoc, 'srcDoc', [[PDFDocument, 'PDFDocument']]);
1✔
835
    assertIs(indices, 'indices', [Array]);
1✔
836
    await srcDoc.flush();
1✔
837
    const copier = PDFObjectCopier.for(srcDoc.context, this.context);
1✔
838
    const srcPages = srcDoc.getPages();
1✔
839
    // Copy each page in a separate thread
840
    const copiedPages = indices
1✔
841
      .map((i) => srcPages[i])
2✔
842
      .map(async (page) => copier.copy(page.node))
2✔
843
      .map((p) =>
844
        p.then((copy) => PDFPage.of(copy, this.context.register(copy), this)),
2✔
845
      );
846
    return Promise.all(copiedPages);
1✔
847
  }
848

849
  /**
850
   * Get a copy of this document.
851
   *
852
   * For example:
853
   * ```js
854
   * const srcDoc = await PDFDocument.load(...)
855
   * const pdfDoc = await srcDoc.copy()
856
   * ```
857
   *
858
   * > **NOTE:**  This method won't copy all information over to the new
859
   * > document (acroforms, outlines, etc...).
860
   *
861
   * @returns Resolves with a copy this document.
862
   */
863
  async copy(): Promise<PDFDocument> {
864
    const pdfCopy = await PDFDocument.create();
1✔
865
    const contentPages = await pdfCopy.copyPages(this, this.getPageIndices());
1✔
866

867
    for (let idx = 0, len = contentPages.length; idx < len; idx++) {
1✔
868
      pdfCopy.addPage(contentPages[idx]);
2✔
869
    }
870

871
    if (this.getAuthor() !== undefined) {
1✔
872
      pdfCopy.setAuthor(this.getAuthor()!);
1✔
873
    }
874
    if (this.getCreationDate() !== undefined) {
1✔
875
      pdfCopy.setCreationDate(this.getCreationDate()!);
1✔
876
    }
877
    if (this.getCreator() !== undefined) {
1✔
878
      pdfCopy.setCreator(this.getCreator()!);
1✔
879
    }
880
    if (this.getLanguage() !== undefined) {
1!
881
      pdfCopy.setLanguage(this.getLanguage()!);
×
882
    }
883
    if (this.getModificationDate() !== undefined) {
1✔
884
      pdfCopy.setModificationDate(this.getModificationDate()!);
1✔
885
    }
886
    if (this.getProducer() !== undefined) {
1✔
887
      pdfCopy.setProducer(this.getProducer()!);
1✔
888
    }
889
    if (this.getSubject() !== undefined) {
1✔
890
      pdfCopy.setSubject(this.getSubject()!);
1✔
891
    }
892
    if (this.getTitle() !== undefined) {
1✔
893
      pdfCopy.setTitle(this.getTitle()!);
1✔
894
    }
895
    pdfCopy.defaultWordBreaks = this.defaultWordBreaks;
1✔
896

897
    return pdfCopy;
1✔
898
  }
899

900
  /**
901
   * Add JavaScript to this document. The supplied `script` is executed when the
902
   * document is opened. The `script` can be used to perform some operation
903
   * when the document is opened (e.g. logging to the console), or it can be
904
   * used to define a function that can be referenced later in a JavaScript
905
   * action. For example:
906
   * ```js
907
   * // Show "Hello World!" in the console when the PDF is opened
908
   * pdfDoc.addJavaScript(
909
   *   'main',
910
   *   'console.show(); console.println("Hello World!");'
911
   * );
912
   *
913
   * // Define a function named "foo" that can be called in JavaScript Actions
914
   * pdfDoc.addJavaScript(
915
   *   'foo',
916
   *   'function foo() { return "foo"; }'
917
   * );
918
   * ```
919
   * See the [JavaScript for Acrobat API Reference](https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf)
920
   * for details.
921
   * @param name The name of the script. Must be unique per document.
922
   * @param script The JavaScript to execute.
923
   */
924
  addJavaScript(name: string, script: string) {
925
    assertIs(name, 'name', ['string']);
3✔
926
    assertIs(script, 'script', ['string']);
3✔
927

928
    const embedder = JavaScriptEmbedder.for(script, name);
3✔
929

930
    const ref = this.context.nextRef();
3✔
931
    const javaScript = PDFJavaScript.of(ref, this, embedder);
3✔
932
    this.javaScripts.push(javaScript);
3✔
933
  }
934

935
  /**
936
   * Add an attachment to this document. Attachments are visible in the
937
   * "Attachments" panel of Adobe Acrobat and some other PDF readers. Any
938
   * type of file can be added as an attachment. This includes, but is not
939
   * limited to, `.png`, `.jpg`, `.pdf`, `.csv`, `.docx`, and `.xlsx` files.
940
   *
941
   * The input data can be provided in multiple formats:
942
   *
943
   * | Type          | Contents                                                       |
944
   * | ------------- | -------------------------------------------------------------- |
945
   * | `string`      | A base64 encoded string (or data URI) containing an attachment |
946
   * | `Uint8Array`  | The raw bytes of an attachment                                 |
947
   * | `ArrayBuffer` | The raw bytes of an attachment                                 |
948
   *
949
   * For example:
950
   * ```js
951
   * // attachment=string
952
   * await pdfDoc.attach('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD...', 'cat_riding_unicorn.jpg', {
953
   *   mimeType: 'image/jpeg',
954
   *   description: 'Cool cat riding a unicorn! 🦄🐈🕶️',
955
   *   creationDate: new Date('2019/12/01'),
956
   *   modificationDate: new Date('2020/04/19'),
957
   * })
958
   * await pdfDoc.attach('data:image/jpeg;base64,/9j/4AAQ...', 'cat_riding_unicorn.jpg', {
959
   *   mimeType: 'image/jpeg',
960
   *   description: 'Cool cat riding a unicorn! 🦄🐈🕶️',
961
   *   creationDate: new Date('2019/12/01'),
962
   *   modificationDate: new Date('2020/04/19'),
963
   * })
964
   *
965
   * // attachment=Uint8Array
966
   * import fs from 'fs'
967
   * const uint8Array = fs.readFileSync('cat_riding_unicorn.jpg')
968
   * await pdfDoc.attach(uint8Array, 'cat_riding_unicorn.jpg', {
969
   *   mimeType: 'image/jpeg',
970
   *   description: 'Cool cat riding a unicorn! 🦄🐈🕶️',
971
   *   creationDate: new Date('2019/12/01'),
972
   *   modificationDate: new Date('2020/04/19'),
973
   * })
974
   *
975
   * // attachment=ArrayBuffer
976
   * const url = 'https://pdf-lib.js.org/assets/cat_riding_unicorn.jpg'
977
   * const arrayBuffer = await fetch(url).then(res => res.arrayBuffer())
978
   * await pdfDoc.attach(arrayBuffer, 'cat_riding_unicorn.jpg', {
979
   *   mimeType: 'image/jpeg',
980
   *   description: 'Cool cat riding a unicorn! 🦄🐈🕶️',
981
   *   creationDate: new Date('2019/12/01'),
982
   *   modificationDate: new Date('2020/04/19'),
983
   * })
984
   * ```
985
   *
986
   * @param attachment The input data containing the file to be attached.
987
   * @param name The name of the file to be attached.
988
   * @returns Resolves when the attachment is complete.
989
   */
990
  async attach(
991
    attachment: string | Uint8Array | ArrayBuffer,
992
    name: string,
993
    options: AttachmentOptions = {},
×
994
  ): Promise<void> {
995
    assertIs(attachment, 'attachment', ['string', Uint8Array, ArrayBuffer]);
10✔
996
    assertIs(name, 'name', ['string']);
10✔
997
    assertOrUndefined(options.mimeType, 'mimeType', ['string']);
10✔
998
    assertOrUndefined(options.description, 'description', ['string']);
10✔
999
    assertOrUndefined(options.creationDate, 'options.creationDate', [Date]);
10✔
1000
    assertOrUndefined(options.modificationDate, 'options.modificationDate', [
10✔
1001
      Date,
1002
    ]);
1003
    assertIsOneOfOrUndefined(
10✔
1004
      options.afRelationship,
1005
      'options.afRelationship',
1006
      AFRelationship,
1007
    );
1008

1009
    const bytes = toUint8Array(attachment);
10✔
1010
    const embedder = FileEmbedder.for(bytes, name, options);
10✔
1011

1012
    const ref = this.context.nextRef();
10✔
1013
    const embeddedFile = PDFEmbeddedFile.of(ref, this, embedder);
10✔
1014
    this.embeddedFiles.push(embeddedFile);
10✔
1015
  }
1016

1017
  private getRawAttachments() {
1018
    if (!this.catalog.has(PDFName.of('Names'))) return [];
14!
1019
    const Names = this.catalog.lookup(PDFName.of('Names'), PDFDict);
14✔
1020

1021
    if (!Names.has(PDFName.of('EmbeddedFiles'))) return [];
14!
1022
    const EmbeddedFiles = Names.lookup(PDFName.of('EmbeddedFiles'), PDFDict);
14✔
1023

1024
    if (!EmbeddedFiles.has(PDFName.of('Names'))) return [];
14!
1025
    const EFNames = EmbeddedFiles.lookup(PDFName.of('Names'), PDFArray);
14✔
1026

1027
    const rawAttachments = [];
14✔
1028
    for (let idx = 0, len = EFNames.size(); idx < len; idx += 2) {
14✔
1029
      const fileName = EFNames.lookup(idx) as PDFHexString | PDFString;
27✔
1030
      const fileSpec = EFNames.lookup(idx + 1, PDFDict);
27✔
1031
      rawAttachments.push({
27✔
1032
        fileName,
1033
        fileSpec,
1034
        specRef: EFNames.get(idx + 1) as PDFRef,
1035
      });
1036
    }
1037

1038
    return rawAttachments;
14✔
1039
  }
1040

1041
  private getSavedAttachments(): SavedPDFAttachment[] {
1042
    const rawAttachments = this.getRawAttachments();
14✔
1043
    return rawAttachments.flatMap(({ fileName, fileSpec, specRef }) => {
14✔
1044
      const efDict = fileSpec.lookup(PDFName.of('EF'));
27✔
1045
      if (!isPDFInstance(efDict, PDFClasses.PDFDict)) return [];
27!
1046

1047
      const stream = (efDict as PDFDict).lookup(PDFName.of('F'));
27✔
1048
      if (!isPDFInstance(stream, PDFClasses.PDFStream)) return [];
27!
1049

1050
      const afr = fileSpec.lookup(PDFName.of('AFRelationship'));
27✔
1051
      const afRelationship = isPDFInstance(afr, PDFClasses.PDFName)
27✔
1052
        ? (afr as PDFName).toString().slice(1) // Remove leading slash
1053
        : isPDFInstance(afr, PDFClasses.PDFString)
24!
1054
          ? (afr as PDFString).decodeText()
1055
          : undefined;
1056

1057
      const embeddedFileDict = (stream as PDFStream).dict;
27✔
1058
      const subtype = embeddedFileDict.lookup(PDFName.of('Subtype'));
27✔
1059

1060
      const mimeType = isPDFInstance(subtype, PDFClasses.PDFName)
27!
1061
        ? (subtype as PDFName).toString().slice(1)
1062
        : isPDFInstance(subtype, PDFClasses.PDFString)
×
1063
          ? (subtype as PDFString).decodeText()
1064
          : undefined;
1065

1066
      const paramsDict = embeddedFileDict.lookup(PDFName.of('Params'), PDFDict);
27✔
1067

1068
      let creationDate: Date | undefined;
1069
      let modificationDate: Date | undefined;
1070

1071
      if (isPDFInstance(paramsDict, PDFClasses.PDFDict)) {
27✔
1072
        const creationDateRaw = paramsDict.lookup(PDFName.of('CreationDate'));
27✔
1073
        const modDateRaw = paramsDict.lookup(PDFName.of('ModDate'));
27✔
1074

1075
        if (isPDFInstance(creationDateRaw, PDFClasses.PDFString)) {
27✔
1076
          creationDate = (creationDateRaw as PDFString).decodeDate();
22✔
1077
        }
1078

1079
        if (isPDFInstance(modDateRaw, PDFClasses.PDFString)) {
27✔
1080
          modificationDate = (modDateRaw as PDFString).decodeDate();
22✔
1081
        }
1082
      }
1083

1084
      const descRaw = fileSpec.lookup(PDFName.of('Desc'));
27✔
1085
      let description: string | undefined;
1086

1087
      if (isPDFInstance(descRaw, PDFClasses.PDFHexString)) {
27✔
1088
        description = (descRaw as PDFHexString).decodeText();
27✔
1089
      }
1090

1091
      return [
27✔
1092
        {
1093
          name: fileName.decodeText(),
1094
          data: decodePDFRawStream(stream as PDFRawStream).decode(),
1095
          mimeType: mimeType?.replace(/#([0-9A-Fa-f]{2})/g, (_, hex) =>
81!
1096
            String.fromCharCode(parseInt(hex, 16)),
27✔
1097
          ),
1098
          afRelationship: afRelationship as AFRelationship,
1099
          description,
1100
          creationDate,
1101
          modificationDate,
1102
          embeddedFileDict: efDict as PDFDict,
1103
          specRef,
1104
        },
1105
      ];
1106
    });
1107
  }
1108

1109
  private getUnsavedAttachments(): UnsavedPDFAttachment[] {
1110
    const attachments = this.embeddedFiles.flatMap((file) => {
14✔
1111
      if (file.getAlreadyEmbedded()) return [];
5✔
1112
      const embedder = file.getEmbedder();
2✔
1113

1114
      return {
2✔
1115
        name: embedder.fileName,
1116
        data: embedder.getFileData(),
1117
        description: embedder.options.description,
1118
        mimeType: embedder.options.mimeType,
1119
        afRelationship: embedder.options.afRelationship,
1120
        creationDate: embedder.options.creationDate,
1121
        modificationDate: embedder.options.modificationDate,
1122
        pdfEmbeddedFile: file,
1123
      };
1124
    });
1125

1126
    return attachments;
14✔
1127
  }
1128

1129
  /**
1130
   * Get all attachments that are embedded in this document.
1131
   *
1132
   * @returns Array of attachments with name and data
1133
   */
1134
  getAttachments(): PDFAttachment[] {
1135
    const savedAttachments = this.getSavedAttachments();
14✔
1136
    const unsavedAttachments = this.getUnsavedAttachments();
14✔
1137

1138
    return [...savedAttachments, ...unsavedAttachments];
14✔
1139
  }
1140

1141
  /**
1142
   * Removes an attachment from PDF, based on name.
1143
   * @param {string} name Name of the attachmet to remove.
1144
   */
1145
  detach(name: string) {
1146
    const attachedFiles = this.getAttachments();
4✔
1147
    attachedFiles.forEach((file) => {
4✔
1148
      if (file.name !== name) return;
8✔
1149
      // the file wasn't embedded into context yet
1150
      if ('pdfEmbeddedFile' in file) {
3!
1151
        const i = this.embeddedFiles.findIndex(
×
1152
          (f) => file.pdfEmbeddedFile === f,
×
1153
        );
1154
        if (i !== undefined) this.embeddedFiles.splice(i, 1);
×
1155
      } else {
1156
        // remove references from catalog
1157
        const namesArr = this.catalog
3!
1158
          .Names()
1159
          ?.lookup(PDFName.of('EmbeddedFiles'), PDFDict)
1160
          .lookup(PDFName.of('Names'), PDFArray);
1161
        const iNames = namesArr?.indexOf(file.specRef);
3!
1162
        if (iNames !== undefined && iNames > 0) {
3✔
1163
          // attachment spec ref
1164
          namesArr?.remove(iNames);
3!
1165
          // attachment name
1166
          namesArr?.remove(iNames - 1);
3!
1167
        }
1168
        // AF-Tag for PDF-A3 compliance
1169
        const AF = this.catalog.AttachedFiles();
3✔
1170
        const afIndex = AF?.indexOf(file.specRef);
3✔
1171
        if (afIndex !== undefined) AF?.remove(afIndex);
3!
1172

1173
        // remove references from context
1174
        const streamRef = this.context
3!
1175
          .lookupMaybe(file.specRef, PDFDict)
1176
          ?.lookupMaybe(PDFName.of('EF'), PDFDict)
1177
          ?.get(PDFName.of('F')) as PDFRef | undefined;
1178
        if (streamRef) this.context.delete(streamRef);
3✔
1179
        this.context.delete(file.specRef);
3✔
1180
      }
1181
    });
1182
  }
1183

1184
  /**
1185
   * Embed a font into this document. The input data can be provided in multiple
1186
   * formats:
1187
   *
1188
   * | Type            | Contents                                                |
1189
   * | --------------- | ------------------------------------------------------- |
1190
   * | `StandardFonts` | One of the standard 14 fonts                            |
1191
   * | `string`        | A base64 encoded string (or data URI) containing a font |
1192
   * | `Uint8Array`    | The raw bytes of a font                                 |
1193
   * | `ArrayBuffer`   | The raw bytes of a font                                 |
1194
   *
1195
   * For example:
1196
   * ```js
1197
   * // font=StandardFonts
1198
   * import { StandardFonts } from 'pdf-lib'
1199
   * const font1 = await pdfDoc.embedFont(StandardFonts.Helvetica)
1200
   *
1201
   * // font=string
1202
   * const font2 = await pdfDoc.embedFont('AAEAAAAVAQAABABQRFNJRx/upe...')
1203
   * const font3 = await pdfDoc.embedFont('data:font/opentype;base64,AAEAAA...')
1204
   *
1205
   * // font=Uint8Array
1206
   * import fs from 'fs'
1207
   * const font4 = await pdfDoc.embedFont(fs.readFileSync('Ubuntu-R.ttf'))
1208
   *
1209
   * // font=ArrayBuffer
1210
   * const url = 'https://pdf-lib.js.org/assets/ubuntu/Ubuntu-R.ttf'
1211
   * const ubuntuBytes = await fetch(url).then(res => res.arrayBuffer())
1212
   * const font5 = await pdfDoc.embedFont(ubuntuBytes)
1213
   * ```
1214
   * See also: [[registerFontkit]]
1215
   * @param font The input data for a font.
1216
   * @param options The options to be used when embedding the font.
1217
   * @returns Resolves with the embedded font.
1218
   */
1219
  async embedFont(
1220
    font: StandardFonts | string | Uint8Array | ArrayBuffer,
1221
    options: EmbedFontOptions = {},
35✔
1222
  ): Promise<PDFFont> {
1223
    const { subset = false, customName, features } = options;
35✔
1224

1225
    assertIs(font, 'font', ['string', Uint8Array, ArrayBuffer]);
35✔
1226
    assertIs(subset, 'subset', ['boolean']);
35✔
1227

1228
    let embedder: CustomFontEmbedder | StandardFontEmbedder;
1229
    if (isStandardFont(font)) {
35✔
1230
      embedder = StandardFontEmbedder.for(font, customName);
32✔
1231
    } else if (canBeConvertedToUint8Array(font)) {
3!
1232
      const bytes = toUint8Array(font);
3✔
1233
      const fontkit = this.assertFontkit();
3✔
1234
      embedder = subset
3!
1235
        ? await CustomFontSubsetEmbedder.for(
1236
            fontkit,
1237
            bytes,
1238
            customName,
1239
            features,
1240
          )
1241
        : await CustomFontEmbedder.for(fontkit, bytes, customName, features);
1242
    } else {
1243
      throw new TypeError(
×
1244
        '`font` must be one of `StandardFonts | string | Uint8Array | ArrayBuffer`',
1245
      );
1246
    }
1247

1248
    const ref = this.context.nextRef();
35✔
1249
    const pdfFont = PDFFont.of(ref, this, embedder);
35✔
1250
    this.fonts.push(pdfFont);
35✔
1251

1252
    return pdfFont;
35✔
1253
  }
1254

1255
  /**
1256
   * Embed a standard font into this document.
1257
   * For example:
1258
   * ```js
1259
   * import { StandardFonts } from 'pdf-lib'
1260
   * const helveticaFont = pdfDoc.embedFont(StandardFonts.Helvetica)
1261
   * ```
1262
   * @param font The standard font to be embedded.
1263
   * @param customName The name to be used when embedding the font.
1264
   * @returns The embedded font.
1265
   */
1266
  embedStandardFont(font: StandardFonts, customName?: string): PDFFont {
1267
    assertIs(font, 'font', ['string']);
30✔
1268
    if (!isStandardFont(font)) {
30✔
1269
      throw new TypeError('`font` must be one of type `StandardFonts`');
1✔
1270
    }
1271

1272
    const embedder = StandardFontEmbedder.for(font, customName);
29✔
1273

1274
    const ref = this.context.nextRef();
29✔
1275
    const pdfFont = PDFFont.of(ref, this, embedder);
29✔
1276
    this.fonts.push(pdfFont);
29✔
1277

1278
    return pdfFont;
29✔
1279
  }
1280

1281
  /**
1282
   * Embed a JPEG image into this document. The input data can be provided in
1283
   * multiple formats:
1284
   *
1285
   * | Type          | Contents                                                      |
1286
   * | ------------- | ------------------------------------------------------------- |
1287
   * | `string`      | A base64 encoded string (or data URI) containing a JPEG image |
1288
   * | `Uint8Array`  | The raw bytes of a JPEG image                                 |
1289
   * | `ArrayBuffer` | The raw bytes of a JPEG image                                 |
1290
   *
1291
   * For example:
1292
   * ```js
1293
   * // jpg=string
1294
   * const image1 = await pdfDoc.embedJpg('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD...')
1295
   * const image2 = await pdfDoc.embedJpg('data:image/jpeg;base64,/9j/4AAQ...')
1296
   *
1297
   * // jpg=Uint8Array
1298
   * import fs from 'fs'
1299
   * const uint8Array = fs.readFileSync('cat_riding_unicorn.jpg')
1300
   * const image3 = await pdfDoc.embedJpg(uint8Array)
1301
   *
1302
   * // jpg=ArrayBuffer
1303
   * const url = 'https://pdf-lib.js.org/assets/cat_riding_unicorn.jpg'
1304
   * const arrayBuffer = await fetch(url).then(res => res.arrayBuffer())
1305
   * const image4 = await pdfDoc.embedJpg(arrayBuffer)
1306
   * ```
1307
   *
1308
   * @param jpg The input data for a JPEG image.
1309
   * @returns Resolves with the embedded image.
1310
   */
1311
  async embedJpg(jpg: string | Uint8Array | ArrayBuffer): Promise<PDFImage> {
1312
    assertIs(jpg, 'jpg', ['string', Uint8Array, ArrayBuffer]);
×
1313
    const bytes = toUint8Array(jpg);
×
1314
    const embedder = await JpegEmbedder.for(bytes);
×
1315
    const ref = this.context.nextRef();
×
1316
    const pdfImage = PDFImage.of(ref, this, embedder);
×
1317
    this.images.push(pdfImage);
×
1318
    return pdfImage;
×
1319
  }
1320

1321
  /**
1322
   * Embed a PNG image into this document. The input data can be provided in
1323
   * multiple formats:
1324
   *
1325
   * | Type          | Contents                                                     |
1326
   * | ------------- | ------------------------------------------------------------ |
1327
   * | `string`      | A base64 encoded string (or data URI) containing a PNG image |
1328
   * | `Uint8Array`  | The raw bytes of a PNG image                                 |
1329
   * | `ArrayBuffer` | The raw bytes of a PNG image                                 |
1330
   *
1331
   * For example:
1332
   * ```js
1333
   * // png=string
1334
   * const image1 = await pdfDoc.embedPng('iVBORw0KGgoAAAANSUhEUgAAAlgAAAF3...')
1335
   * const image2 = await pdfDoc.embedPng('data:image/png;base64,iVBORw0KGg...')
1336
   *
1337
   * // png=Uint8Array
1338
   * import fs from 'fs'
1339
   * const uint8Array = fs.readFileSync('small_mario.png')
1340
   * const image3 = await pdfDoc.embedPng(uint8Array)
1341
   *
1342
   * // png=ArrayBuffer
1343
   * const url = 'https://pdf-lib.js.org/assets/small_mario.png'
1344
   * const arrayBuffer = await fetch(url).then(res => res.arrayBuffer())
1345
   * const image4 = await pdfDoc.embedPng(arrayBuffer)
1346
   * ```
1347
   *
1348
   * @param png The input data for a PNG image.
1349
   * @returns Resolves with the embedded image.
1350
   */
1351
  async embedPng(png: string | Uint8Array | ArrayBuffer): Promise<PDFImage> {
1352
    assertIs(png, 'png', ['string', Uint8Array, ArrayBuffer]);
5✔
1353
    const bytes = toUint8Array(png);
5✔
1354
    const embedder = await PngEmbedder.for(bytes);
5✔
1355
    const ref = this.context.nextRef();
5✔
1356
    const pdfImage = PDFImage.of(ref, this, embedder);
5✔
1357
    this.images.push(pdfImage);
5✔
1358
    return pdfImage;
5✔
1359
  }
1360

1361
  async embedSvg(svg: string): Promise<PDFSvg> {
1362
    if (!svg) return new PDFSvg(svg);
×
1363
    const parsedSvg = parseHtml(svg);
×
1364
    const findImages = (element: HTMLElement): HTMLElement[] => {
×
1365
      if (element.tagName === 'image') return [element];
×
1366
      else {
1367
        return element.childNodes
×
1368
          .map((child) =>
1369
            child.nodeType === NodeType.ELEMENT_NODE ? findImages(child) : [],
×
1370
          )
1371
          .flat();
1372
      }
1373
    };
1374
    const images = findImages(parsedSvg);
×
1375
    const imagesDict = {} as Record<string, PDFImage>;
×
1376

1377
    await Promise.all(
×
1378
      images.map(async (image) => {
×
1379
        const href = image.attributes.href ?? image.attributes['xlink:href'];
×
1380
        if (!href || imagesDict[href]) return;
×
1381
        const isPng = href.match(/\.png(\?|$)|^data:image\/png;base64/gim);
×
1382
        const pdfImage = isPng
×
1383
          ? await this.embedPng(href)
1384
          : await this.embedJpg(href);
1385
        imagesDict[href] = pdfImage;
×
1386
      }),
1387
    );
1388

1389
    return new PDFSvg(svg, imagesDict);
×
1390
  }
1391
  /**
1392
   * Embed one or more PDF pages into this document.
1393
   *
1394
   * For example:
1395
   * ```js
1396
   * const pdfDoc = await PDFDocument.create()
1397
   *
1398
   * const sourcePdfUrl = 'https://pdf-lib.js.org/assets/with_large_page_count.pdf'
1399
   * const sourcePdf = await fetch(sourcePdfUrl).then((res) => res.arrayBuffer())
1400
   *
1401
   * // Embed page 74 of `sourcePdf` into `pdfDoc`
1402
   * const [embeddedPage] = await pdfDoc.embedPdf(sourcePdf, [73])
1403
   * ```
1404
   *
1405
   * See [[PDFDocument.load]] for examples of the allowed input data formats.
1406
   *
1407
   * @param pdf The input data containing a PDF document.
1408
   * @param indices The indices of the pages that should be embedded.
1409
   * @returns Resolves with an array of the embedded pages.
1410
   */
1411
  async embedPdf(
1412
    pdf: string | Uint8Array | ArrayBuffer | PDFDocument,
1413
    indices: number[] = [0],
×
1414
  ): Promise<PDFEmbeddedPage[]> {
1415
    assertIs(pdf, 'pdf', [
×
1416
      'string',
1417
      Uint8Array,
1418
      ArrayBuffer,
1419
      [PDFDocument, 'PDFDocument'],
1420
    ]);
1421
    assertIs(indices, 'indices', [Array]);
×
1422

1423
    const srcDoc = isPDFInstance(pdf, PDFClasses.PDFDocument)
×
1424
      ? pdf
1425
      : await PDFDocument.load(pdf as string | Uint8Array | ArrayBuffer);
1426

1427
    const srcPages = pluckIndices((srcDoc as PDFDocument).getPages(), indices);
×
1428

1429
    return this.embedPages(srcPages);
×
1430
  }
1431

1432
  /**
1433
   * Embed a single PDF page into this document.
1434
   *
1435
   * For example:
1436
   * ```js
1437
   * const pdfDoc = await PDFDocument.create()
1438
   *
1439
   * const sourcePdfUrl = 'https://pdf-lib.js.org/assets/with_large_page_count.pdf'
1440
   * const sourceBuffer = await fetch(sourcePdfUrl).then((res) => res.arrayBuffer())
1441
   * const sourcePdfDoc = await PDFDocument.load(sourceBuffer)
1442
   * const sourcePdfPage = sourcePdfDoc.getPages()[73]
1443
   *
1444
   * const embeddedPage = await pdfDoc.embedPage(
1445
   *   sourcePdfPage,
1446
   *
1447
   *   // Clip a section of the source page so that we only embed part of it
1448
   *   { left: 100, right: 450, bottom: 330, top: 570 },
1449
   *
1450
   *   // Translate all drawings of the embedded page by (10, 200) units
1451
   *   [1, 0, 0, 1, 10, 200],
1452
   * )
1453
   * ```
1454
   *
1455
   * @param page The page to be embedded.
1456
   * @param boundingBox
1457
   * Optionally, an area of the source page that should be embedded
1458
   * (defaults to entire page).
1459
   * @param transformationMatrix
1460
   * Optionally, a transformation matrix that is always applied to the embedded
1461
   * page anywhere it is drawn.
1462
   * @returns Resolves with the embedded pdf page.
1463
   */
1464
  async embedPage(
1465
    page: PDFPage,
1466
    boundingBox?: PageBoundingBox,
1467
    transformationMatrix?: TransformationMatrix,
1468
  ): Promise<PDFEmbeddedPage> {
1469
    assertIs(page, 'page', [[PDFPage, 'PDFPage']]);
×
1470
    const [embeddedPage] = await this.embedPages(
×
1471
      [page],
1472
      [boundingBox],
1473
      [transformationMatrix],
1474
    );
1475
    return embeddedPage;
×
1476
  }
1477

1478
  /**
1479
   * Embed one or more PDF pages into this document.
1480
   *
1481
   * For example:
1482
   * ```js
1483
   * const pdfDoc = await PDFDocument.create()
1484
   *
1485
   * const sourcePdfUrl = 'https://pdf-lib.js.org/assets/with_large_page_count.pdf'
1486
   * const sourceBuffer = await fetch(sourcePdfUrl).then((res) => res.arrayBuffer())
1487
   * const sourcePdfDoc = await PDFDocument.load(sourceBuffer)
1488
   *
1489
   * const page1 = sourcePdfDoc.getPages()[0]
1490
   * const page2 = sourcePdfDoc.getPages()[52]
1491
   * const page3 = sourcePdfDoc.getPages()[73]
1492
   *
1493
   * const embeddedPages = await pdfDoc.embedPages([page1, page2, page3])
1494
   * ```
1495
   *
1496
   * @param page
1497
   * The pages to be embedded (they must all share the same context).
1498
   * @param boundingBoxes
1499
   * Optionally, an array of clipping boundaries - one for each page
1500
   * (defaults to entirety of each page).
1501
   * @param transformationMatrices
1502
   * Optionally, an array of transformation matrices - one for each page
1503
   * (each page's transformation will apply anywhere it is drawn).
1504
   * @returns Resolves with an array of the embedded pdf pages.
1505
   */
1506
  async embedPages(
1507
    pages: PDFPage[],
1508
    boundingBoxes: (PageBoundingBox | undefined)[] = [],
×
1509
    transformationMatrices: (TransformationMatrix | undefined)[] = [],
×
1510
  ) {
1511
    if (pages.length === 0) return [];
×
1512

1513
    // Assert all pages have the same context
1514
    for (let idx = 0, len = pages.length - 1; idx < len; idx++) {
×
1515
      const currPage = pages[idx];
×
1516
      const nextPage = pages[idx + 1];
×
1517
      if (currPage.node.context !== nextPage.node.context) {
×
1518
        throw new PageEmbeddingMismatchedContextError();
×
1519
      }
1520
    }
1521

1522
    const context = pages[0].node.context;
×
1523
    const maybeCopyPage =
1524
      context === this.context
×
1525
        ? (p: PDFPageLeaf) => p
×
1526
        : PDFObjectCopier.for(context, this.context).copy;
1527

1528
    const embeddedPages = new Array<PDFEmbeddedPage>(pages.length);
×
1529
    for (let idx = 0, len = pages.length; idx < len; idx++) {
×
1530
      const page = maybeCopyPage(pages[idx].node);
×
1531
      const box = boundingBoxes[idx];
×
1532
      const matrix = transformationMatrices[idx];
×
1533

1534
      const embedder = await PDFPageEmbedder.for(page, box, matrix);
×
1535

1536
      const ref = this.context.nextRef();
×
1537
      embeddedPages[idx] = PDFEmbeddedPage.of(ref, this, embedder);
×
1538
    }
1539

1540
    this.embeddedPages.push(...embeddedPages);
×
1541

1542
    return embeddedPages;
×
1543
  }
1544

1545
  encrypt(options: SecurityOptions) {
1546
    this.context.security = PDFSecurity.create(this.context, options).encrypt();
×
1547
  }
1548

1549
  /**
1550
   * > **NOTE:** You shouldn't need to call this method directly. The [[save]]
1551
   * > and [[saveAsBase64]] methods will automatically ensure that all embedded
1552
   * > assets are flushed before serializing the document.
1553
   *
1554
   * Flush all embedded fonts, PDF pages, and images to this document's
1555
   * [[context]].
1556
   *
1557
   * @returns Resolves when the flush is complete.
1558
   */
1559
  async flush(): Promise<void> {
1560
    await this.embedAll(this.fonts);
102✔
1561
    await this.embedAll(this.images);
102✔
1562
    await this.embedAll(this.embeddedPages);
102✔
1563
    await this.embedAll(this.embeddedFiles);
102✔
1564
    await this.embedAll(this.javaScripts);
102✔
1565
  }
1566

1567
  /**
1568
   * Serialize this document to an array of bytes making up a PDF file.
1569
   * For example:
1570
   * ```js
1571
   * const pdfBytes = await pdfDoc.save()
1572
   * ```
1573
   *
1574
   * There are a number of things you can do with the serialized document,
1575
   * depending on the JavaScript environment you're running in:
1576
   * * Write it to a file in Node or React Native
1577
   * * Download it as a Blob in the browser
1578
   * * Render it in an `iframe`
1579
   *
1580
   * @param options The options to be used when saving the document.
1581
   * @returns Resolves with the bytes of the serialized document.
1582
   */
1583
  async save(options: SaveOptions = {}): Promise<Uint8Array> {
30✔
1584
    // check PDF version
1585
    const vparts = this.context.header.getVersionString().split('.');
49✔
1586
    const uOS =
1587
      options.rewrite || Number(vparts[0]) > 1 || Number(vparts[1]) >= 5;
49✔
1588
    const {
1589
      useObjectStreams = uOS,
39✔
1590
      objectsPerTick = 50,
49✔
1591
      rewrite = false,
44✔
1592
    } = options;
49✔
1593

1594
    assertIs(useObjectStreams, 'useObjectStreams', ['boolean']);
49✔
1595
    assertIs(objectsPerTick, 'objectsPerTick', ['number']);
49✔
1596
    assertIs(rewrite, 'rewrite', ['boolean']);
49✔
1597
    const incrementalUpdate =
1598
      !rewrite &&
49✔
1599
      this.context.pdfFileDetails.originalBytes &&
1600
      this.context.snapshot;
1601
    if (incrementalUpdate) {
49✔
1602
      options.addDefaultPage = false;
18✔
1603
      options.updateFieldAppearances = false;
18✔
1604
    }
1605

1606
    await this.prepareForSave(options);
49✔
1607

1608
    const Writer = useObjectStreams ? PDFStreamWriter : PDFWriter;
49✔
1609
    if (incrementalUpdate) {
49✔
1610
      const increment = await Writer.forContextWithSnapshot(
18✔
1611
        this.context,
1612
        objectsPerTick,
1613
        this.context.snapshot!,
1614
      ).serializeToBuffer();
1615
      const result = new Uint8Array(
18✔
1616
        this.context.pdfFileDetails.originalBytes!.byteLength +
1617
          increment.byteLength,
1618
      );
1619
      result.set(this.context.pdfFileDetails.originalBytes!);
18✔
1620
      result.set(
18✔
1621
        increment,
1622
        this.context.pdfFileDetails.originalBytes!.byteLength,
1623
      );
1624
      return result;
18✔
1625
    }
1626
    return Writer.forContext(this.context, objectsPerTick).serializeToBuffer();
31✔
1627
  }
1628

1629
  /**
1630
   * Serialize only the changes to this document to an array of bytes making up a PDF file.
1631
   * For example:
1632
   * ```js
1633
   * const snapshot = pdfDoc.takeSnapshot();
1634
   * ...
1635
   * const pdfBytes = await pdfDoc.saveIncremental(snapshot);
1636
   * ```
1637
   *
1638
   * Similar to [[save]] function.
1639
   * The changes are saved in an incremental way, the result buffer
1640
   * will contain only the differences
1641
   *
1642
   * @param snapshot The snapshot to be used when saving the document.
1643
   * @param options The options to be used when saving the document.
1644
   * @returns Resolves with the bytes of the serialized document.
1645
   */
1646
  async saveIncremental(
1647
    snapshot: DocumentSnapshot,
1648
    options: IncrementalSaveOptions = {},
6✔
1649
  ): Promise<Uint8Array> {
1650
    // check PDF version
1651
    const vparts = this.context.header.getVersionString().split('.');
50✔
1652
    const uOS = Number(vparts[0]) > 1 || Number(vparts[1]) >= 5;
50✔
1653
    const { objectsPerTick = 50 } = options;
50✔
1654

1655
    assertIs(objectsPerTick, 'objectsPerTick', ['number']);
50✔
1656

1657
    const saveOptions: SaveOptions = {
50✔
1658
      useObjectStreams: uOS,
1659
      ...options,
1660
      addDefaultPage: false,
1661
      updateFieldAppearances: false,
1662
    };
1663
    await this.prepareForSave(saveOptions);
50✔
1664

1665
    const Writer = saveOptions.useObjectStreams ? PDFStreamWriter : PDFWriter;
50✔
1666
    return Writer.forContextWithSnapshot(
50✔
1667
      this.context,
1668
      objectsPerTick,
1669
      snapshot,
1670
    ).serializeToBuffer();
1671
  }
1672

1673
  /**
1674
   * Serialize this document to a base64 encoded string or data URI making up a
1675
   * PDF file. For example:
1676
   * ```js
1677
   * const base64String = await pdfDoc.saveAsBase64()
1678
   * base64String // => 'JVBERi0xLjcKJYGBgYEKC...'
1679
   *
1680
   * const base64DataUri = await pdfDoc.saveAsBase64({ dataUri: true })
1681
   * base64DataUri // => 'data:application/pdf;base64,JVBERi0xLjcKJYGBgYEKC...'
1682
   * ```
1683
   *
1684
   * @param options The options to be used when saving the document.
1685
   * @returns Resolves with a base64 encoded string or data URI of the
1686
   *          serialized document.
1687
   */
1688
  async saveAsBase64(options: Base64SaveOptions = {}): Promise<string> {
1✔
1689
    const { dataUri = false, ...otherOptions } = options;
1✔
1690
    assertIs(dataUri, 'dataUri', ['boolean']);
1✔
1691
    const bytes = await this.save(otherOptions);
1✔
1692
    const base64 = encodeToBase64(bytes);
1✔
1693
    return dataUri ? `data:application/pdf;base64,${base64}` : base64;
1!
1694
  }
1695

1696
  findPageForAnnotationRef(ref: PDFRef): PDFPage | undefined {
1697
    const pages = this.getPages();
×
1698
    for (let idx = 0, len = pages.length; idx < len; idx++) {
×
1699
      const page = pages[idx];
×
1700
      const annotations = page.node.Annots();
×
1701

1702
      if (annotations?.indexOf(ref) !== undefined) {
×
1703
        return page;
×
1704
      }
1705
    }
1706

1707
    return undefined;
×
1708
  }
1709

1710
  takeSnapshot(): DocumentSnapshot {
1711
    const indirectObjects: Set<number> = new Set<number>();
93✔
1712

1713
    const snapshot = new IncrementalDocumentSnapshot(
93✔
1714
      this.context.largestObjectNumber,
1715
      indirectObjects,
1716
      this.context.pdfFileDetails.pdfSize,
1717
      this.context.pdfFileDetails.prevStartXRef,
1718
      this.context,
1719
    );
1720
    if (!this.context.snapshot && this.context.pdfFileDetails.originalBytes) {
93✔
1721
      this.context.snapshot = snapshot;
38✔
1722
      this.catalog.registerChange();
38✔
1723
    }
1724
    return snapshot;
93✔
1725
  }
1726

1727
  /**
1728
   * Returns the update version of the object as 'actual', and all the previous versions, of the objects
1729
   * that has changed in the indicated update (or the last one).
1730
   * If document wasn't load to preserve objects versions, an empty array is returned.
1731
   * @param {number} lastUpdateMinusX If not the last update, how many updates before the last.
1732
   * @returns  {PDFObjectVersions[]} Objects modified in the update, and previous versions
1733
   */
1734
  getChangedObjects(lastUpdateMinusX: number = 0): PDFObjectVersions[] {
6✔
1735
    if (!this.context.preserveObjectsVersions) return [];
8✔
1736
    if (lastUpdateMinusX < 0) lastUpdateMinusX = 0;
6!
1737
    const upind = this.context.xrefs.length - lastUpdateMinusX - 1;
6✔
1738
    const entries = this.context.listXrefEntries(upind);
6✔
1739
    if (!entries.length) return [];
6!
1740
    const changed = new Map<string, PDFObjectVersions>();
6✔
1741
    for (const entry of entries) {
6✔
1742
      const ref = entry.ref;
370✔
1743
      changed.set(ref.toString(), {
370✔
1744
        ref,
1745
        actual: entry.deleted ? undefined : this.context.lookup(ref),
370✔
1746
        previous: this.context.getObjectVersions(ref),
1747
      });
1748
    }
1749
    // if not the las update, then check objects later modified and adjust PDFObjectVersions accordingly
1750
    if (!lastUpdateMinusX) return Array.from(changed.values());
6✔
1751
    while (lastUpdateMinusX) {
2✔
1752
      lastUpdateMinusX -= 1;
3✔
1753
      const upind = this.context.xrefs.length - lastUpdateMinusX - 1;
3✔
1754
      const nentries = this.context.listXrefEntries(upind);
3✔
1755
      for (const nentry of nentries) {
3✔
1756
        const oce = changed.get(nentry.ref.toString());
343✔
1757
        if (oce && oce.actual) {
343✔
1758
          oce.actual = oce.previous[0];
11✔
1759
          oce.previous = oce.previous.slice(1);
11✔
1760
        }
1761
      }
1762
    }
1763
    // if PDF has errors, it may happen to end with objects that has no current, nor previous versions
1764
    return Array.from(changed.values()).filter(
2✔
1765
      (ov) => ov.actual || ov.previous.length,
338✔
1766
    );
1767
  }
1768

1769
  /**
1770
   * Saves the current changes to the document as an incremental update, returns the full document,
1771
   * like save method, and modifies the internal state to be able to continue editing the document
1772
   * for another incremental update.
1773
   * This allows you to save multiple incremental updates without reloading the PDF.
1774
   *
1775
   * For example:
1776
   * ```js
1777
   * const pdfDoc = await PDFDocument.load(pdfBytes, { forIncrementalUpdate: true })
1778
   *
1779
   * const page = pdfDoc.getPage(0)
1780
   * page.drawText('First update')
1781
   * const firstsave = await pdfDoc.saveAndContinue()
1782
   *
1783
   * page.drawText('Second update', { y: 100 })
1784
   * const secondsave = await pdfDoc.saveAndContinue()
1785
   * ```
1786
   *
1787
   * @param options The options to be used when saving changes.
1788
   * @returns Resolves with the complete PDF bytes including all updates.
1789
   */
1790
  async saveAndContinue(
1791
    options: IncrementalSaveOptions = {},
40✔
1792
  ): Promise<Uint8Array> {
1793
    if (!this.context.pdfFileDetails.originalBytes || !this.context.snapshot) {
40✔
1794
      throw new Error(
1✔
1795
        'saveAndContinue() requires the document to be loaded with forIncrementalUpdate: true',
1796
      );
1797
    }
1798
    const originalBytes = this.context.pdfFileDetails.originalBytes;
39✔
1799
    const incrementalBytes = await this.saveIncremental(
39✔
1800
      this.context.snapshot,
1801
      options,
1802
    );
1803

1804
    const newPdfBytes = new Uint8Array(
39✔
1805
      originalBytes.byteLength + incrementalBytes.byteLength,
1806
    );
1807
    newPdfBytes.set(originalBytes);
39✔
1808
    newPdfBytes.set(incrementalBytes, originalBytes.byteLength);
39✔
1809

1810
    this.context.pdfFileDetails.originalBytes = newPdfBytes;
39✔
1811
    this.context.pdfFileDetails.pdfSize = newPdfBytes.byteLength;
39✔
1812

1813
    const incrementalStr = new TextDecoder('latin1').decode(incrementalBytes);
39✔
1814
    const startxrefMatch = incrementalStr.match(/startxref\s+(\d+)/);
39✔
1815
    if (startxrefMatch) {
39!
1816
      this.context.pdfFileDetails.prevStartXRef = parseInt(
39✔
1817
        startxrefMatch[1],
1818
        10,
1819
      );
1820
    } else {
UNCOV
1821
      this.context.pdfFileDetails.prevStartXRef = originalBytes.byteLength;
×
1822
    }
1823

1824
    this.context.snapshot = this.takeSnapshot();
39✔
1825

1826
    return newPdfBytes;
39✔
1827
  }
1828

1829
  private async prepareForSave(options: SaveOptions): Promise<void> {
1830
    const { addDefaultPage = true, updateFieldAppearances = true } = options;
99✔
1831

1832
    assertIs(addDefaultPage, 'addDefaultPage', ['boolean']);
99✔
1833
    assertIs(updateFieldAppearances, 'updateFieldAppearances', ['boolean']);
99✔
1834

1835
    if (addDefaultPage && this.getPageCount() === 0) this.addPage();
99✔
1836

1837
    if (updateFieldAppearances) {
99✔
1838
      const form = this.formCache.getValue();
30✔
1839
      if (form) form.updateFieldAppearances();
30✔
1840
    }
1841

1842
    await this.flush();
99✔
1843
  }
1844

1845
  private async embedAll(embeddables: Embeddable[]): Promise<void> {
1846
    for (let idx = 0, len = embeddables.length; idx < len; idx++) {
510✔
1847
      await embeddables[idx].embed();
99✔
1848
    }
1849
  }
1850

1851
  private updateInfoDict(): void {
1852
    const pdfLib = 'pdf-lib (https://github.com/Hopding/pdf-lib)';
195✔
1853
    const now = new Date();
195✔
1854

1855
    const info = this.getInfoDict();
195✔
1856

1857
    this.setProducer(pdfLib);
195✔
1858
    this.setModificationDate(now);
195✔
1859

1860
    if (!info.get(PDFName.of('Creator'))) this.setCreator(pdfLib);
195✔
1861
    if (!info.get(PDFName.of('CreationDate'))) this.setCreationDate(now);
195✔
1862
  }
1863

1864
  private getInfoDict(): PDFDict {
1865
    const existingInfo = this.context.lookup(this.context.trailerInfo.Info);
797✔
1866
    if (isPDFInstance(existingInfo, PDFClasses.PDFDict)) {
797✔
1867
      return existingInfo as PDFDict;
743✔
1868
    }
1869

1870
    const newInfo = this.context.obj({});
54✔
1871
    this.context.trailerInfo.Info = this.context.register(newInfo);
54✔
1872

1873
    return newInfo;
54✔
1874
  }
1875

1876
  private assertFontkit(): Fontkit {
1877
    if (!this.fontkit) throw new FontkitNotRegisteredError();
3!
1878
    return this.fontkit;
3✔
1879
  }
1880

1881
  private computePages = (): PDFPage[] => {
204✔
1882
    const pages: PDFPage[] = [];
127✔
1883
    this.catalog.Pages().traverse((node, ref) => {
127✔
1884
      if (isPDFInstance(node, PDFClasses.PDFPageLeaf)) {
595✔
1885
        let page = this.pageMap.get(node as PDFPageLeaf);
513✔
1886
        if (!page) {
513✔
1887
          page = PDFPage.of(node as PDFPageLeaf, ref, this);
506✔
1888
          this.pageMap.set(node as PDFPageLeaf, page);
506✔
1889
        }
1890
        pages.push(page);
513✔
1891
      }
1892
    });
1893
    return pages;
127✔
1894
  };
1895

1896
  private getOrCreateForm = (): PDFForm => {
204✔
1897
    const acroForm = this.catalog.getOrCreateAcroForm();
51✔
1898
    return PDFForm.of(acroForm, this);
51✔
1899
  };
1900
}
1901

1902
/* tslint:disable-next-line only-arrow-functions */
1903
function assertIsLiteralOrHexString(
1904
  pdfObject: PDFObject,
1905
): asserts pdfObject is PDFHexString | PDFString {
1906
  if (
55!
1907
    !isPDFInstance(pdfObject, PDFClasses.PDFHexString) &&
74✔
1908
    !isPDFInstance(pdfObject, PDFClasses.PDFString)
1909
  ) {
UNCOV
1910
    throw new UnexpectedObjectTypeError([PDFHexString, PDFString], pdfObject);
×
1911
  }
1912
}
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