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

adnsistemas / pdf-lib / #15

20 Feb 2026 04:09PM UTC coverage: 73.994% (+0.2%) from 73.811%
#15

push

David N. Abdala
Added delete attachmets features to Readme

2559 of 3972 branches covered (64.43%)

Branch coverage included in aggregate %.

7183 of 9194 relevant lines covered (78.13%)

156851.39 hits per line

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

81.78
/src/api/PDFDocument.ts
1
import {
32✔
2
  parse as parseHtml,
3
  HTMLElement,
4
  NodeType,
5
} from 'node-html-better-parser';
6
import Embeddable from './Embeddable';
7
import {
32✔
8
  EncryptedPDFError,
9
  FontkitNotRegisteredError,
10
  ForeignPageError,
11
  RemovePageFromEmptyDocumentError,
12
} from './errors';
13
import PDFEmbeddedPage from './PDFEmbeddedPage';
32✔
14
import PDFFont from './PDFFont';
32✔
15
import PDFImage from './PDFImage';
32✔
16
import PDFPage from './PDFPage';
32✔
17
import PDFForm from './form/PDFForm';
32✔
18
import { PageSizes } from './sizes';
32✔
19
import { StandardFonts } from './StandardFonts';
20
import {
32✔
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 {
32✔
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 {
32✔
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';
32✔
76
import PDFEmbeddedFile from './PDFEmbeddedFile';
32✔
77
import PDFJavaScript from './PDFJavaScript';
32✔
78
import JavaScriptEmbedder from '../core/embedders/JavaScriptEmbedder';
32✔
79
import { CipherTransformFactory } from '../core/crypto';
32✔
80
import PDFSvg from './PDFSvg';
32✔
81
import PDFSecurity, { SecurityOptions } from '../core/security/PDFSecurity';
32✔
82
import { IncrementalDocumentSnapshot } from './snapshot';
32✔
83
import type { DocumentSnapshot } from './snapshot';
84

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

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

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

104
export type PDFAttachment = UnsavedPDFAttachment | SavedPDFAttachment;
105

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

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

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

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

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

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

248
    return new PDFDocument(context, false, updateMetadata);
57✔
249
  }
250

251
  /** The low-level context of this document. */
252
  readonly context: PDFContext;
253

254
  /** The catalog of this document. */
255
  readonly catalog: PDFCatalog;
256

257
  /** Whether or not this document is encrypted. */
258
  readonly isEncrypted: boolean;
259

260
  /** The default word breaks used in PDFPage.drawText */
261
  defaultWordBreaks: string[] = [' '];
203✔
262

263
  private fontkit?: Fontkit;
264
  private pageCount: number | undefined;
265
  private readonly pageCache: Cache<PDFPage[]>;
266
  private readonly pageMap: Map<PDFPageLeaf, PDFPage>;
267
  private readonly formCache: Cache<PDFForm>;
268
  private readonly fonts: PDFFont[];
269
  private readonly images: PDFImage[];
270
  private readonly embeddedPages: PDFEmbeddedPage[];
271
  private readonly embeddedFiles: PDFEmbeddedFile[];
272
  private readonly javaScripts: PDFJavaScript[];
273

274
  private constructor(
275
    context: PDFContext,
276
    ignoreEncryption: boolean,
277
    updateMetadata: boolean,
278
  ) {
279
    assertIs(context, 'context', [[PDFContext, 'PDFContext']]);
203✔
280
    assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']);
203✔
281

282
    this.context = context;
203✔
283
    this.catalog = context.lookup(context.trailerInfo.Root) as PDFCatalog;
203✔
284

285
    if (!!context.lookup(context.trailerInfo.Encrypt) && context.isDecrypted) {
203✔
286
      // context.delete(context.trailerInfo.Encrypt);
287
      delete context.trailerInfo.Encrypt;
1✔
288
    }
289
    this.isEncrypted = !!context.lookup(context.trailerInfo.Encrypt);
203✔
290

291
    this.pageCache = Cache.populatedBy(this.computePages);
203✔
292
    this.pageMap = new Map();
203✔
293
    this.formCache = Cache.populatedBy(this.getOrCreateForm);
203✔
294
    this.fonts = [];
203✔
295
    this.images = [];
203✔
296
    this.embeddedPages = [];
203✔
297
    this.embeddedFiles = [];
203✔
298
    this.javaScripts = [];
203✔
299

300
    if (!ignoreEncryption && this.isEncrypted) throw new EncryptedPDFError();
203✔
301

302
    if (updateMetadata) this.updateInfoDict();
200✔
303
  }
304

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

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

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

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

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

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

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

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

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

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

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

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

513
    // Indicate that readers should display the title rather than the filename
514
    if (options?.showInWindowTitleBar) {
10✔
515
      const prefs = this.catalog.getOrCreateViewerPreferences();
1✔
516
      prefs.setDisplayDocTitle(true);
1✔
517
    }
518
  }
519

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

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

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

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

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

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

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

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

635
  /**
636
   * Get the number of pages contained in this document. For example:
637
   * ```js
638
   * const totalPages = pdfDoc.getPageCount()
639
   * ```
640
   * @returns The number of pages in this document.
641
   */
642
  getPageCount(): number {
643
    if (this.pageCount === undefined) this.pageCount = this.getPages().length;
187✔
644
    return this.pageCount;
187✔
645
  }
646

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

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

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

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

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

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

798
    const parentRef = this.catalog.insertLeafNode(page.ref, index);
52✔
799
    page.node.setParent(parentRef);
52✔
800

801
    this.pageMap.set(page.node, page);
52✔
802
    this.pageCache.invalidate();
52✔
803

804
    this.pageCount = pageCount + 1;
52✔
805

806
    return page;
52✔
807
  }
808

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

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

861
    for (let idx = 0, len = contentPages.length; idx < len; idx++) {
1✔
862
      pdfCopy.addPage(contentPages[idx]);
2✔
863
    }
864

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

891
    return pdfCopy;
1✔
892
  }
893

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

922
    const embedder = JavaScriptEmbedder.for(script, name);
3✔
923

924
    const ref = this.context.nextRef();
3✔
925
    const javaScript = PDFJavaScript.of(ref, this, embedder);
3✔
926
    this.javaScripts.push(javaScript);
3✔
927
  }
928

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

1003
    const bytes = toUint8Array(attachment);
10✔
1004
    const embedder = FileEmbedder.for(bytes, name, options);
10✔
1005

1006
    const ref = this.context.nextRef();
10✔
1007
    const embeddedFile = PDFEmbeddedFile.of(ref, this, embedder);
10✔
1008
    this.embeddedFiles.push(embeddedFile);
10✔
1009
  }
1010

1011
  private getRawAttachments() {
1012
    if (!this.catalog.has(PDFName.of('Names'))) return [];
14!
1013
    const Names = this.catalog.lookup(PDFName.of('Names'), PDFDict);
14✔
1014

1015
    if (!Names.has(PDFName.of('EmbeddedFiles'))) return [];
14!
1016
    const EmbeddedFiles = Names.lookup(PDFName.of('EmbeddedFiles'), PDFDict);
14✔
1017

1018
    if (!EmbeddedFiles.has(PDFName.of('Names'))) return [];
14!
1019
    const EFNames = EmbeddedFiles.lookup(PDFName.of('Names'), PDFArray);
14✔
1020

1021
    const rawAttachments = [];
14✔
1022
    for (let idx = 0, len = EFNames.size(); idx < len; idx += 2) {
14✔
1023
      const fileName = EFNames.lookup(idx) as PDFHexString | PDFString;
27✔
1024
      const fileSpec = EFNames.lookup(idx + 1, PDFDict);
27✔
1025
      rawAttachments.push({
27✔
1026
        fileName,
1027
        fileSpec,
1028
        specRef: EFNames.get(idx + 1) as PDFRef,
1029
      });
1030
    }
1031

1032
    return rawAttachments;
14✔
1033
  }
1034

1035
  private getSavedAttachments(): SavedPDFAttachment[] {
1036
    const rawAttachments = this.getRawAttachments();
14✔
1037
    return rawAttachments.flatMap(({ fileName, fileSpec, specRef }) => {
14✔
1038
      const efDict = fileSpec.lookup(PDFName.of('EF'));
27✔
1039
      if (!(efDict instanceof PDFDict)) return [];
27!
1040

1041
      const stream = efDict.lookup(PDFName.of('F'));
27✔
1042
      if (!(stream instanceof PDFStream)) return [];
27!
1043

1044
      const afr = fileSpec.lookup(PDFName.of('AFRelationship'));
27✔
1045
      const afRelationship =
1046
        afr instanceof PDFName
27✔
1047
          ? afr.toString().slice(1) // Remove leading slash
1048
          : afr instanceof PDFString
24!
1049
            ? afr.decodeText()
1050
            : undefined;
1051

1052
      const embeddedFileDict = stream.dict;
27✔
1053
      const subtype = embeddedFileDict.lookup(PDFName.of('Subtype'));
27✔
1054

1055
      const mimeType =
1056
        subtype instanceof PDFName
27!
1057
          ? subtype.toString().slice(1)
1058
          : subtype instanceof PDFString
×
1059
            ? subtype.decodeText()
1060
            : undefined;
1061

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

1064
      let creationDate: Date | undefined;
1065
      let modificationDate: Date | undefined;
1066

1067
      if (paramsDict instanceof PDFDict) {
27✔
1068
        const creationDateRaw = paramsDict.lookup(PDFName.of('CreationDate'));
27✔
1069
        const modDateRaw = paramsDict.lookup(PDFName.of('ModDate'));
27✔
1070

1071
        if (creationDateRaw instanceof PDFString) {
27✔
1072
          creationDate = creationDateRaw.decodeDate();
22✔
1073
        }
1074

1075
        if (modDateRaw instanceof PDFString) {
27✔
1076
          modificationDate = modDateRaw.decodeDate();
22✔
1077
        }
1078
      }
1079

1080
      const descRaw = fileSpec.lookup(PDFName.of('Desc'));
27✔
1081
      let description: string | undefined;
1082

1083
      if (descRaw instanceof PDFHexString) {
27✔
1084
        description = descRaw.decodeText();
27✔
1085
      }
1086

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

1105
  private getUnsavedAttachments(): UnsavedPDFAttachment[] {
1106
    const attachments = this.embeddedFiles.flatMap((file) => {
14✔
1107
      if (file.getAlreadyEmbedded()) return [];
5✔
1108
      const embedder = file.getEmbedder();
2✔
1109

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

1122
    return attachments;
14✔
1123
  }
1124

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

1134
    return [...savedAttachments, ...unsavedAttachments];
14✔
1135
  }
1136

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

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

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

1221
    assertIs(font, 'font', ['string', Uint8Array, ArrayBuffer]);
35✔
1222
    assertIs(subset, 'subset', ['boolean']);
35✔
1223

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

1244
    const ref = this.context.nextRef();
35✔
1245
    const pdfFont = PDFFont.of(ref, this, embedder);
35✔
1246
    this.fonts.push(pdfFont);
35✔
1247

1248
    return pdfFont;
35✔
1249
  }
1250

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

1268
    const embedder = StandardFontEmbedder.for(font, customName);
29✔
1269

1270
    const ref = this.context.nextRef();
29✔
1271
    const pdfFont = PDFFont.of(ref, this, embedder);
29✔
1272
    this.fonts.push(pdfFont);
29✔
1273

1274
    return pdfFont;
29✔
1275
  }
1276

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

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

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

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

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

1419
    const srcDoc =
1420
      pdf instanceof PDFDocument ? pdf : await PDFDocument.load(pdf);
×
1421

1422
    const srcPages = pluckIndices(srcDoc.getPages(), indices);
×
1423

1424
    return this.embedPages(srcPages);
×
1425
  }
1426

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

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

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

1517
    const context = pages[0].node.context;
×
1518
    const maybeCopyPage =
1519
      context === this.context
×
1520
        ? (p: PDFPageLeaf) => p
×
1521
        : PDFObjectCopier.for(context, this.context).copy;
1522

1523
    const embeddedPages = new Array<PDFEmbeddedPage>(pages.length);
×
1524
    for (let idx = 0, len = pages.length; idx < len; idx++) {
×
1525
      const page = maybeCopyPage(pages[idx].node);
×
1526
      const box = boundingBoxes[idx];
×
1527
      const matrix = transformationMatrices[idx];
×
1528

1529
      const embedder = await PDFPageEmbedder.for(page, box, matrix);
×
1530

1531
      const ref = this.context.nextRef();
×
1532
      embeddedPages[idx] = PDFEmbeddedPage.of(ref, this, embedder);
×
1533
    }
1534

1535
    this.embeddedPages.push(...embeddedPages);
×
1536

1537
    return embeddedPages;
×
1538
  }
1539

1540
  encrypt(options: SecurityOptions) {
1541
    this.context.security = PDFSecurity.create(this.context, options).encrypt();
×
1542
  }
1543

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

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

1589
    assertIs(useObjectStreams, 'useObjectStreams', ['boolean']);
47✔
1590
    assertIs(objectsPerTick, 'objectsPerTick', ['number']);
47✔
1591
    assertIs(rewrite, 'rewrite', ['boolean']);
47✔
1592
    const incrementalUpdate =
1593
      !rewrite &&
47✔
1594
      this.context.pdfFileDetails.originalBytes &&
1595
      this.context.snapshot;
1596
    if (incrementalUpdate) {
47✔
1597
      options.addDefaultPage = false;
18✔
1598
      options.updateFieldAppearances = false;
18✔
1599
    }
1600

1601
    await this.prepareForSave(options);
47✔
1602

1603
    const Writer = useObjectStreams ? PDFStreamWriter : PDFWriter;
47✔
1604
    if (incrementalUpdate) {
47✔
1605
      const increment = await Writer.forContextWithSnapshot(
18✔
1606
        this.context,
1607
        objectsPerTick,
1608
        this.context.snapshot!,
1609
      ).serializeToBuffer();
1610
      const result = new Uint8Array(
18✔
1611
        this.context.pdfFileDetails.originalBytes!.byteLength +
1612
          increment.byteLength,
1613
      );
1614
      result.set(this.context.pdfFileDetails.originalBytes!);
18✔
1615
      result.set(
18✔
1616
        increment,
1617
        this.context.pdfFileDetails.originalBytes!.byteLength,
1618
      );
1619
      return result;
18✔
1620
    }
1621
    return Writer.forContext(this.context, objectsPerTick).serializeToBuffer();
29✔
1622
  }
1623

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

1650
    assertIs(objectsPerTick, 'objectsPerTick', ['number']);
50✔
1651

1652
    const saveOptions: SaveOptions = {
50✔
1653
      useObjectStreams: uOS,
1654
      ...options,
1655
      addDefaultPage: false,
1656
      updateFieldAppearances: false,
1657
    };
1658
    await this.prepareForSave(saveOptions);
50✔
1659

1660
    const Writer = saveOptions.useObjectStreams ? PDFStreamWriter : PDFWriter;
50✔
1661
    return Writer.forContextWithSnapshot(
50✔
1662
      this.context,
1663
      objectsPerTick,
1664
      snapshot,
1665
    ).serializeToBuffer();
1666
  }
1667

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

1691
  findPageForAnnotationRef(ref: PDFRef): PDFPage | undefined {
1692
    const pages = this.getPages();
×
1693
    for (let idx = 0, len = pages.length; idx < len; idx++) {
×
1694
      const page = pages[idx];
×
1695
      const annotations = page.node.Annots();
×
1696

1697
      if (annotations?.indexOf(ref) !== undefined) {
×
1698
        return page;
×
1699
      }
1700
    }
1701

1702
    return undefined;
×
1703
  }
1704

1705
  takeSnapshot(): DocumentSnapshot {
1706
    const indirectObjects: Set<number> = new Set<number>();
93✔
1707

1708
    const snapshot = new IncrementalDocumentSnapshot(
93✔
1709
      this.context.largestObjectNumber,
1710
      indirectObjects,
1711
      this.context.pdfFileDetails.pdfSize,
1712
      this.context.pdfFileDetails.prevStartXRef,
1713
      this.context,
1714
    );
1715
    if (!this.context.snapshot && this.context.pdfFileDetails.originalBytes) {
93✔
1716
      this.context.snapshot = snapshot;
38✔
1717
      this.catalog.registerChange();
38✔
1718
    }
1719
    return snapshot;
93✔
1720
  }
1721

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

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

1800
    const newPdfBytes = new Uint8Array(
39✔
1801
      originalBytes.byteLength + incrementalBytes.byteLength,
1802
    );
1803
    newPdfBytes.set(originalBytes);
39✔
1804
    newPdfBytes.set(incrementalBytes, originalBytes.byteLength);
39✔
1805

1806
    this.context.pdfFileDetails.originalBytes = newPdfBytes;
39✔
1807
    this.context.pdfFileDetails.pdfSize = newPdfBytes.byteLength;
39✔
1808

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

1820
    this.context.snapshot = this.takeSnapshot();
39✔
1821

1822
    return newPdfBytes;
39✔
1823
  }
1824

1825
  private async prepareForSave(options: SaveOptions): Promise<void> {
1826
    const { addDefaultPage = true, updateFieldAppearances = true } = options;
97✔
1827

1828
    assertIs(addDefaultPage, 'addDefaultPage', ['boolean']);
97✔
1829
    assertIs(updateFieldAppearances, 'updateFieldAppearances', ['boolean']);
97✔
1830

1831
    if (addDefaultPage && this.getPageCount() === 0) this.addPage();
97✔
1832

1833
    if (updateFieldAppearances) {
97✔
1834
      const form = this.formCache.getValue();
28✔
1835
      if (form) form.updateFieldAppearances();
28✔
1836
    }
1837

1838
    await this.flush();
97✔
1839
  }
1840

1841
  private async embedAll(embeddables: Embeddable[]): Promise<void> {
1842
    for (let idx = 0, len = embeddables.length; idx < len; idx++) {
500✔
1843
      await embeddables[idx].embed();
99✔
1844
    }
1845
  }
1846

1847
  private updateInfoDict(): void {
1848
    const pdfLib = 'pdf-lib (https://github.com/Hopding/pdf-lib)';
194✔
1849
    const now = new Date();
194✔
1850

1851
    const info = this.getInfoDict();
194✔
1852

1853
    this.setProducer(pdfLib);
194✔
1854
    this.setModificationDate(now);
194✔
1855

1856
    if (!info.get(PDFName.of('Creator'))) this.setCreator(pdfLib);
194✔
1857
    if (!info.get(PDFName.of('CreationDate'))) this.setCreationDate(now);
194✔
1858
  }
1859

1860
  private getInfoDict(): PDFDict {
1861
    const existingInfo = this.context.lookup(this.context.trailerInfo.Info);
794✔
1862
    if (existingInfo instanceof PDFDict) {
794✔
1863
      return existingInfo;
740✔
1864
    }
1865

1866
    const newInfo = this.context.obj({});
54✔
1867
    this.context.trailerInfo.Info = this.context.register(newInfo);
54✔
1868

1869
    return newInfo;
54✔
1870
  }
1871

1872
  private assertFontkit(): Fontkit {
1873
    if (!this.fontkit) throw new FontkitNotRegisteredError();
3!
1874
    return this.fontkit;
3✔
1875
  }
1876

1877
  private computePages = (): PDFPage[] => {
203✔
1878
    const pages: PDFPage[] = [];
126✔
1879
    this.catalog.Pages().traverse((node, ref) => {
126✔
1880
      if (node instanceof PDFPageLeaf) {
594✔
1881
        let page = this.pageMap.get(node);
512✔
1882
        if (!page) {
512✔
1883
          page = PDFPage.of(node, ref, this);
505✔
1884
          this.pageMap.set(node, page);
505✔
1885
        }
1886
        pages.push(page);
512✔
1887
      }
1888
    });
1889
    return pages;
126✔
1890
  };
1891

1892
  private getOrCreateForm = (): PDFForm => {
203✔
1893
    const acroForm = this.catalog.getOrCreateAcroForm();
51✔
1894
    return PDFForm.of(acroForm, this);
51✔
1895
  };
1896
}
1897

1898
/* tslint:disable-next-line only-arrow-functions */
1899
function assertIsLiteralOrHexString(
1900
  pdfObject: PDFObject,
1901
): asserts pdfObject is PDFHexString | PDFString {
1902
  if (
55!
1903
    !(pdfObject instanceof PDFHexString) &&
74✔
1904
    !(pdfObject instanceof PDFString)
1905
  ) {
1906
    throw new UnexpectedObjectTypeError([PDFHexString, PDFString], pdfObject);
×
1907
  }
1908
}
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