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

adnsistemas / pdf-lib / #8

02 Dec 2025 12:22AM UTC coverage: 70.499% (+0.3%) from 70.248%
#8

push

David N. Abdala
Inclusion of saveAndContinue logic

2443 of 3918 branches covered (62.35%)

Branch coverage included in aggregate %.

6767 of 9146 relevant lines covered (73.99%)

145637.43 hits per line

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

79.19
/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 PDFAttachment = {
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 PDFObjectVersions = {
96
  ref: PDFRef;
97
  actual: PDFObject | undefined;
98
  previous: PDFObject[];
99
};
100

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

173
    assertIs(pdf, 'pdf', ['string', Uint8Array, ArrayBuffer]);
142✔
174
    assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']);
142✔
175
    assertIs(parseSpeed, 'parseSpeed', ['number']);
142✔
176
    assertIs(throwOnInvalidObject, 'throwOnInvalidObject', ['boolean']);
142✔
177
    assertIs(warnOnInvalidObjects, 'warnOnInvalidObjects', ['boolean']);
142✔
178
    assertIs(password, 'password', ['string', 'undefined']);
142✔
179
    assertIs(forIncrementalUpdate, 'forIncrementalUpdate', ['boolean']);
142✔
180
    assertIs(preserveObjectsVersions, 'preserveObjectsVersions', ['boolean']);
142✔
181

182
    const bytes = toUint8Array(pdf);
142✔
183
    const context = await PDFParser.forBytesWithOptions(
142✔
184
      bytes,
185
      parseSpeed,
186
      throwOnInvalidObject,
187
      undefined,
188
      capNumbers,
189
      undefined,
190
      forIncrementalUpdate,
191
      preserveObjectsVersions,
192
    ).parseDocument();
193
    if (
141!
194
      !!context.lookup(context.trailerInfo.Encrypt) &&
146✔
195
      password !== undefined
196
    ) {
197
      // Decrypt
198
      const fileIds = context.lookup(context.trailerInfo.ID, PDFArray);
×
199
      const encryptDict = context.lookup(context.trailerInfo.Encrypt, PDFDict);
×
200
      const decryptedContext = await PDFParser.forBytesWithOptions(
×
201
        bytes,
202
        parseSpeed,
203
        throwOnInvalidObject,
204
        warnOnInvalidObjects,
205
        capNumbers,
206
        new CipherTransformFactory(
207
          encryptDict,
208
          (fileIds.get(0) as PDFHexString).asBytes(),
209
          password,
210
        ),
211
        forIncrementalUpdate,
212
        preserveObjectsVersions,
213
      ).parseDocument();
214
      const pdfDoc = new PDFDocument(decryptedContext, true, updateMetadata);
×
215
      if (forIncrementalUpdate) pdfDoc.takeSnapshot();
×
216
      return pdfDoc;
×
217
    } else {
218
      const pdfDoc = new PDFDocument(context, ignoreEncryption, updateMetadata);
141✔
219
      if (forIncrementalUpdate) pdfDoc.takeSnapshot();
138✔
220
      return pdfDoc;
138✔
221
    }
222
  }
223

224
  /**
225
   * Create a new [[PDFDocument]].
226
   * @returns Resolves with the newly created document.
227
   */
228
  static async create(options: CreateOptions = {}) {
52✔
229
    const { updateMetadata = true } = options;
57✔
230

231
    const context = PDFContext.create();
57✔
232
    const pageTree = PDFPageTree.withContext(context);
57✔
233
    const pageTreeRef = context.register(pageTree);
57✔
234
    const catalog = PDFCatalog.withContextAndPages(context, pageTreeRef);
57✔
235
    context.trailerInfo.Root = context.register(catalog);
57✔
236

237
    return new PDFDocument(context, false, updateMetadata);
57✔
238
  }
239

240
  /** The low-level context of this document. */
241
  readonly context: PDFContext;
242

243
  /** The catalog of this document. */
244
  readonly catalog: PDFCatalog;
245

246
  /** Whether or not this document is encrypted. */
247
  readonly isEncrypted: boolean;
248

249
  /** The default word breaks used in PDFPage.drawText */
250
  defaultWordBreaks: string[] = [' '];
198✔
251

252
  private fontkit?: Fontkit;
253
  private pageCount: number | undefined;
254
  private readonly pageCache: Cache<PDFPage[]>;
255
  private readonly pageMap: Map<PDFPageLeaf, PDFPage>;
256
  private readonly formCache: Cache<PDFForm>;
257
  private readonly fonts: PDFFont[];
258
  private readonly images: PDFImage[];
259
  private readonly embeddedPages: PDFEmbeddedPage[];
260
  private readonly embeddedFiles: PDFEmbeddedFile[];
261
  private readonly javaScripts: PDFJavaScript[];
262

263
  private constructor(
264
    context: PDFContext,
265
    ignoreEncryption: boolean,
266
    updateMetadata: boolean,
267
  ) {
268
    assertIs(context, 'context', [[PDFContext, 'PDFContext']]);
198✔
269
    assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']);
198✔
270

271
    this.context = context;
198✔
272
    this.catalog = context.lookup(context.trailerInfo.Root) as PDFCatalog;
198✔
273

274
    if (!!context.lookup(context.trailerInfo.Encrypt) && context.isDecrypted) {
198!
275
      // context.delete(context.trailerInfo.Encrypt);
276
      delete context.trailerInfo.Encrypt;
×
277
    }
278
    this.isEncrypted = !!context.lookup(context.trailerInfo.Encrypt);
198✔
279

280
    this.pageCache = Cache.populatedBy(this.computePages);
198✔
281
    this.pageMap = new Map();
198✔
282
    this.formCache = Cache.populatedBy(this.getOrCreateForm);
198✔
283
    this.fonts = [];
198✔
284
    this.images = [];
198✔
285
    this.embeddedPages = [];
198✔
286
    this.embeddedFiles = [];
198✔
287
    this.javaScripts = [];
198✔
288

289
    if (!ignoreEncryption && this.isEncrypted) throw new EncryptedPDFError();
198✔
290

291
    if (updateMetadata) this.updateInfoDict();
195✔
292
  }
293

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

316
  /**
317
   * Get the [[PDFForm]] containing all interactive fields for this document.
318
   * For example:
319
   * ```js
320
   * const form = pdfDoc.getForm()
321
   * const fields = form.getFields()
322
   * fields.forEach(field => {
323
   *   const type = field.constructor.name
324
   *   const name = field.getName()
325
   *   console.log(`${type}: ${name}`)
326
   * })
327
   * ```
328
   * @returns The form for this document.
329
   */
330
  getForm(): PDFForm {
331
    const form = this.formCache.access();
151✔
332
    if (form.hasXFA()) {
151✔
333
      console.warn(
1✔
334
        'Removing XFA form data as pdf-lib does not support reading or writing XFA',
335
      );
336
      form.deleteXFA();
1✔
337
    }
338
    return form;
151✔
339
  }
340

341
  /**
342
   * Get this document's title metadata. The title appears in the
343
   * "Document Properties" section of most PDF readers. For example:
344
   * ```js
345
   * const title = pdfDoc.getTitle()
346
   * ```
347
   * @returns A string containing the title of this document, if it has one.
348
   */
349
  getTitle(): string | undefined {
350
    const title = this.getInfoDict().lookup(PDFName.Title);
10✔
351
    if (!title) return undefined;
10✔
352
    assertIsLiteralOrHexString(title);
9✔
353
    return title.decodeText();
9✔
354
  }
355

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

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

386
  /**
387
   * Get this document's keywords metadata. The keywords appear in the
388
   * "Document Properties" section of most PDF readers. For example:
389
   * ```js
390
   * const keywords = pdfDoc.getKeywords()
391
   * ```
392
   * @returns A string containing the keywords of this document, if it has any.
393
   */
394
  getKeywords(): string | undefined {
395
    const keywords = this.getInfoDict().lookup(PDFName.Keywords);
3✔
396
    if (!keywords) return undefined;
3✔
397
    assertIsLiteralOrHexString(keywords);
2✔
398
    return keywords.decodeText();
2✔
399
  }
400

401
  /**
402
   * Get this document's creator metadata. The creator appears in the
403
   * "Document Properties" section of most PDF readers. For example:
404
   * ```js
405
   * const creator = pdfDoc.getCreator()
406
   * ```
407
   * @returns A string containing the creator of this document, if it has one.
408
   */
409
  getCreator(): string | undefined {
410
    const creator = this.getInfoDict().lookup(PDFName.Creator);
6✔
411
    if (!creator) return undefined;
6!
412
    assertIsLiteralOrHexString(creator);
6✔
413
    return creator.decodeText();
6✔
414
  }
415

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

431
  /**
432
   * Get this document's language metadata. The language appears in the
433
   * "Document Properties" section of most PDF readers. For example:
434
   * ```js
435
   * const language = pdfDoc.getLanguage()
436
   * ```
437
   * @returns A string containing the RFC 3066 _Language-Tag_ of this document,
438
   *          if it has one.
439
   */
440
  getLanguage(): string | undefined {
441
    const language = this.catalog.get(PDFName.of('Lang'));
5✔
442
    if (!language) return undefined;
5✔
443
    assertIsLiteralOrHexString(language);
3✔
444
    return language.decodeText();
3✔
445
  }
446

447
  /**
448
   * Get this document's creation date metadata. The creation date appears in
449
   * the "Document Properties" section of most PDF readers. For example:
450
   * ```js
451
   * const creationDate = pdfDoc.getCreationDate()
452
   * ```
453
   * @returns A Date containing the creation date of this document,
454
   *          if it has one.
455
   */
456
  getCreationDate(): Date | undefined {
457
    const creationDate = this.getInfoDict().lookup(PDFName.CreationDate);
6✔
458
    if (!creationDate) return undefined;
6!
459
    assertIsLiteralOrHexString(creationDate);
6✔
460
    return creationDate.decodeDate();
6✔
461
  }
462

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

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

502
    // Indicate that readers should display the title rather than the filename
503
    if (options?.showInWindowTitleBar) {
10✔
504
      const prefs = this.catalog.getOrCreateViewerPreferences();
1✔
505
      prefs.setDisplayDocTitle(true);
1✔
506
    }
507
  }
508

509
  /**
510
   * Set this document's author metadata. The author will appear in the
511
   * "Document Properties" section of most PDF readers. For example:
512
   * ```js
513
   * pdfDoc.setAuthor('Humpty Dumpty')
514
   * ```
515
   * @param author The author of this document.
516
   */
517
  setAuthor(author: string): void {
518
    assertIs(author, 'author', ['string']);
5✔
519
    const key = PDFName.of('Author');
5✔
520
    this.getInfoDict().set(key, PDFHexString.fromText(author));
5✔
521
  }
522

523
  /**
524
   * Set this document's subject metadata. The subject will appear in the
525
   * "Document Properties" section of most PDF readers. For example:
526
   * ```js
527
   * pdfDoc.setSubject('📘 An Epic Tale of Woe 📖')
528
   * ```
529
   * @param subject The subject of this document.
530
   */
531
  setSubject(subject: string): void {
532
    assertIs(subject, 'author', ['string']);
3✔
533
    const key = PDFName.of('Subject');
3✔
534
    this.getInfoDict().set(key, PDFHexString.fromText(subject));
3✔
535
  }
536

537
  /**
538
   * Set this document's keyword metadata. These keywords will appear in the
539
   * "Document Properties" section of most PDF readers. For example:
540
   * ```js
541
   * pdfDoc.setKeywords(['eggs', 'wall', 'fall', 'king', 'horses', 'men'])
542
   * ```
543
   * @param keywords An array of keywords associated with this document.
544
   */
545
  setKeywords(keywords: string[]): void {
546
    assertIs(keywords, 'keywords', [Array]);
2✔
547
    const key = PDFName.of('Keywords');
2✔
548
    this.getInfoDict().set(key, PDFHexString.fromText(keywords.join(' ')));
2✔
549
  }
550

551
  /**
552
   * Set this document's creator metadata. The creator will appear in the
553
   * "Document Properties" section of most PDF readers. For example:
554
   * ```js
555
   * pdfDoc.setCreator('PDF App 9000 🤖')
556
   * ```
557
   * @param creator The creator of this document.
558
   */
559
  setCreator(creator: string): void {
560
    assertIs(creator, 'creator', ['string']);
72✔
561
    const key = PDFName.of('Creator');
72✔
562
    this.getInfoDict().set(key, PDFHexString.fromText(creator));
72✔
563
  }
564

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

579
  /**
580
   * Set this document's language metadata. The language will appear in the
581
   * "Document Properties" section of some PDF readers. For example:
582
   * ```js
583
   * pdfDoc.setLanguage('en-us')
584
   * ```
585
   *
586
   * @param language An RFC 3066 _Language-Tag_ denoting the language of this
587
   *                 document, or an empty string if the language is unknown.
588
   */
589
  setLanguage(language: string): void {
590
    assertIs(language, 'language', ['string']);
3✔
591
    const key = PDFName.of('Lang');
3✔
592
    this.catalog.set(key, PDFString.of(language));
3✔
593
  }
594

595
  /**
596
   * Set this document's creation date metadata. The creation date will appear
597
   * in the "Document Properties" section of most PDF readers. For example:
598
   * ```js
599
   * pdfDoc.setCreationDate(new Date())
600
   * ```
601
   * @param creationDate The date this document was created.
602
   */
603
  setCreationDate(creationDate: Date): void {
604
    assertIs(creationDate, 'creationDate', [[Date, 'Date']]);
58✔
605
    const key = PDFName.of('CreationDate');
58✔
606
    this.getInfoDict().set(key, PDFString.fromDate(creationDate));
58✔
607
  }
608

609
  /**
610
   * Set this document's modification date metadata. The modification date will
611
   * appear in the "Document Properties" section of most PDF readers. For
612
   * example:
613
   * ```js
614
   * pdfDoc.setModificationDate(new Date())
615
   * ```
616
   * @param modificationDate The date this document was last modified.
617
   */
618
  setModificationDate(modificationDate: Date): void {
619
    assertIs(modificationDate, 'modificationDate', [[Date, 'Date']]);
192✔
620
    const key = PDFName.of('ModDate');
192✔
621
    this.getInfoDict().set(key, PDFString.fromDate(modificationDate));
192✔
622
  }
623

624
  /**
625
   * Get the number of pages contained in this document. For example:
626
   * ```js
627
   * const totalPages = pdfDoc.getPageCount()
628
   * ```
629
   * @returns The number of pages in this document.
630
   */
631
  getPageCount(): number {
632
    if (this.pageCount === undefined) this.pageCount = this.getPages().length;
184✔
633
    return this.pageCount;
184✔
634
  }
635

636
  /**
637
   * Get an array of all the pages contained in this document. The pages are
638
   * stored in the array in the same order that they are rendered in the
639
   * document. For example:
640
   * ```js
641
   * const pages = pdfDoc.getPages()
642
   * pages[0]   // The first page of the document
643
   * pages[2]   // The third page of the document
644
   * pages[197] // The 198th page of the document
645
   * ```
646
   * @returns An array of all the pages contained in this document.
647
   */
648
  getPages(): PDFPage[] {
649
    return this.pageCache.access();
159✔
650
  }
651

652
  /**
653
   * Get the page rendered at a particular `index` of the document. For example:
654
   * ```js
655
   * pdfDoc.getPage(0)   // The first page of the document
656
   * pdfDoc.getPage(2)   // The third page of the document
657
   * pdfDoc.getPage(197) // The 198th page of the document
658
   * ```
659
   * @returns The [[PDFPage]] rendered at the given `index` of the document.
660
   */
661
  getPage(index: number): PDFPage {
662
    const pages = this.getPages();
59✔
663
    assertRange(index, 'index', 0, pages.length - 1);
59✔
664
    return pages[index];
59✔
665
  }
666

667
  /**
668
   * Get an array of indices for all the pages contained in this document. The
669
   * array will contain a range of integers from
670
   * `0..pdfDoc.getPageCount() - 1`. For example:
671
   * ```js
672
   * const pdfDoc = await PDFDocument.create()
673
   * pdfDoc.addPage()
674
   * pdfDoc.addPage()
675
   * pdfDoc.addPage()
676
   *
677
   * const indices = pdfDoc.getPageIndices()
678
   * indices // => [0, 1, 2]
679
   * ```
680
   * @returns An array of indices for all pages contained in this document.
681
   */
682
  getPageIndices(): number[] {
683
    return range(0, this.getPageCount());
1✔
684
  }
685

686
  /**
687
   * Remove the page at a given index from this document. For example:
688
   * ```js
689
   * pdfDoc.removePage(0)   // Remove the first page of the document
690
   * pdfDoc.removePage(2)   // Remove the third page of the document
691
   * pdfDoc.removePage(197) // Remove the 198th page of the document
692
   * ```
693
   * Once a page has been removed, it will no longer be rendered at that index
694
   * in the document.
695
   * @param index The index of the page to be removed.
696
   */
697
  removePage(index: number): void {
698
    const pageCount = this.getPageCount();
5✔
699
    if (this.pageCount === 0) throw new RemovePageFromEmptyDocumentError();
5✔
700
    assertRange(index, 'index', 0, pageCount - 1);
4✔
701
    const page = this.getPage(index);
4✔
702
    this.catalog.removeLeafNode(index);
4✔
703
    this.pageCount = pageCount - 1;
4✔
704
    this.context.delete(page.ref);
4✔
705
  }
706

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

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

787
    const parentRef = this.catalog.insertLeafNode(page.ref, index);
52✔
788
    page.node.setParent(parentRef);
52✔
789

790
    this.pageMap.set(page.node, page);
52✔
791
    this.pageCache.invalidate();
52✔
792

793
    this.pageCount = pageCount + 1;
52✔
794

795
    return page;
52✔
796
  }
797

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

832
  /**
833
   * Get a copy of this document.
834
   *
835
   * For example:
836
   * ```js
837
   * const srcDoc = await PDFDocument.load(...)
838
   * const pdfDoc = await srcDoc.copy()
839
   * ```
840
   *
841
   * > **NOTE:**  This method won't copy all information over to the new
842
   * > document (acroforms, outlines, etc...).
843
   *
844
   * @returns Resolves with a copy this document.
845
   */
846
  async copy(): Promise<PDFDocument> {
847
    const pdfCopy = await PDFDocument.create();
1✔
848
    const contentPages = await pdfCopy.copyPages(this, this.getPageIndices());
1✔
849

850
    for (let idx = 0, len = contentPages.length; idx < len; idx++) {
1✔
851
      pdfCopy.addPage(contentPages[idx]);
2✔
852
    }
853

854
    if (this.getAuthor() !== undefined) {
1✔
855
      pdfCopy.setAuthor(this.getAuthor()!);
1✔
856
    }
857
    if (this.getCreationDate() !== undefined) {
1✔
858
      pdfCopy.setCreationDate(this.getCreationDate()!);
1✔
859
    }
860
    if (this.getCreator() !== undefined) {
1✔
861
      pdfCopy.setCreator(this.getCreator()!);
1✔
862
    }
863
    if (this.getLanguage() !== undefined) {
1!
864
      pdfCopy.setLanguage(this.getLanguage()!);
×
865
    }
866
    if (this.getModificationDate() !== undefined) {
1✔
867
      pdfCopy.setModificationDate(this.getModificationDate()!);
1✔
868
    }
869
    if (this.getProducer() !== undefined) {
1✔
870
      pdfCopy.setProducer(this.getProducer()!);
1✔
871
    }
872
    if (this.getSubject() !== undefined) {
1✔
873
      pdfCopy.setSubject(this.getSubject()!);
1✔
874
    }
875
    if (this.getTitle() !== undefined) {
1✔
876
      pdfCopy.setTitle(this.getTitle()!);
1✔
877
    }
878
    pdfCopy.defaultWordBreaks = this.defaultWordBreaks;
1✔
879

880
    return pdfCopy;
1✔
881
  }
882

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

911
    const embedder = JavaScriptEmbedder.for(script, name);
3✔
912

913
    const ref = this.context.nextRef();
3✔
914
    const javaScript = PDFJavaScript.of(ref, this, embedder);
3✔
915
    this.javaScripts.push(javaScript);
3✔
916
  }
917

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

992
    const bytes = toUint8Array(attachment);
9✔
993
    const embedder = FileEmbedder.for(bytes, name, options);
9✔
994

995
    const ref = this.context.nextRef();
9✔
996
    const embeddedFile = PDFEmbeddedFile.of(ref, this, embedder);
9✔
997
    this.embeddedFiles.push(embeddedFile);
9✔
998
  }
999

1000
  private getRawAttachments() {
1001
    if (!this.catalog.has(PDFName.of('Names'))) return [];
3!
1002
    const Names = this.catalog.lookup(PDFName.of('Names'), PDFDict);
3✔
1003

1004
    if (!Names.has(PDFName.of('EmbeddedFiles'))) return [];
3!
1005
    const EmbeddedFiles = Names.lookup(PDFName.of('EmbeddedFiles'), PDFDict);
3✔
1006

1007
    if (!EmbeddedFiles.has(PDFName.of('Names'))) return [];
3!
1008
    const EFNames = EmbeddedFiles.lookup(PDFName.of('Names'), PDFArray);
3✔
1009

1010
    const rawAttachments = [];
3✔
1011
    for (let idx = 0, len = EFNames.size(); idx < len; idx += 2) {
3✔
1012
      const fileName = EFNames.lookup(idx) as PDFHexString | PDFString;
7✔
1013
      const fileSpec = EFNames.lookup(idx + 1, PDFDict);
7✔
1014
      rawAttachments.push({ fileName, fileSpec });
7✔
1015
    }
1016

1017
    return rawAttachments;
3✔
1018
  }
1019

1020
  private getSavedAttachments(): PDFAttachment[] {
1021
    const rawAttachments = this.getRawAttachments();
3✔
1022
    return rawAttachments.flatMap(({ fileName, fileSpec }) => {
3✔
1023
      const efDict = fileSpec.lookup(PDFName.of('EF'));
7✔
1024
      if (!(efDict instanceof PDFDict)) return [];
7!
1025

1026
      const stream = efDict.lookup(PDFName.of('F'));
7✔
1027
      if (!(stream instanceof PDFStream)) return [];
7!
1028

1029
      const afr = fileSpec.lookup(PDFName.of('AFRelationship'));
7✔
1030
      const afRelationship =
1031
        afr instanceof PDFName
7✔
1032
          ? afr.toString().slice(1) // Remove leading slash
1033
          : afr instanceof PDFString
4!
1034
            ? afr.decodeText()
1035
            : undefined;
1036

1037
      const embeddedFileDict = stream.dict;
7✔
1038
      const subtype = embeddedFileDict.lookup(PDFName.of('Subtype'));
7✔
1039

1040
      const mimeType =
1041
        subtype instanceof PDFName
7!
1042
          ? subtype.toString().slice(1)
1043
          : subtype instanceof PDFString
×
1044
            ? subtype.decodeText()
1045
            : undefined;
1046

1047
      const paramsDict = embeddedFileDict.lookup(PDFName.of('Params'), PDFDict);
7✔
1048

1049
      let creationDate: Date | undefined;
1050
      let modificationDate: Date | undefined;
1051

1052
      if (paramsDict instanceof PDFDict) {
7✔
1053
        const creationDateRaw = paramsDict.lookup(PDFName.of('CreationDate'));
7✔
1054
        const modDateRaw = paramsDict.lookup(PDFName.of('ModDate'));
7✔
1055

1056
        if (creationDateRaw instanceof PDFString) {
7✔
1057
          creationDate = creationDateRaw.decodeDate();
4✔
1058
        }
1059

1060
        if (modDateRaw instanceof PDFString) {
7✔
1061
          modificationDate = modDateRaw.decodeDate();
4✔
1062
        }
1063
      }
1064

1065
      const description = (
1066
        fileSpec.lookup(PDFName.of('Desc')) as PDFHexString
7✔
1067
      ).decodeText();
1068

1069
      return [
7✔
1070
        {
1071
          name: fileName.decodeText(),
1072
          data: decodePDFRawStream(stream as PDFRawStream).decode(),
1073
          mimeType: mimeType?.replace(/#([0-9A-Fa-f]{2})/g, (_, hex) =>
21!
1074
            String.fromCharCode(parseInt(hex, 16)),
7✔
1075
          ),
1076
          afRelationship: afRelationship as AFRelationship,
1077
          description,
1078
          creationDate,
1079
          modificationDate,
1080
        },
1081
      ];
1082
    });
1083
  }
1084

1085
  private getUnsavedAttachments(): PDFAttachment[] {
1086
    const attachments = this.embeddedFiles.map((file) => {
3✔
1087
      const embedder = file.getEmbedder();
2✔
1088

1089
      return {
2✔
1090
        name: embedder.fileName,
1091
        data: embedder.getFileData(),
1092
        description: embedder.options.description,
1093
        mimeType: embedder.options.mimeType,
1094
        afRelationship: embedder.options.afRelationship,
1095
        creationDate: embedder.options.creationDate,
1096
        modificationDate: embedder.options.modificationDate,
1097
      };
1098
    });
1099

1100
    return attachments;
3✔
1101
  }
1102

1103
  /**
1104
   * Get all attachments that are embedded in this document.
1105
   *
1106
   * @returns Array of attachments with name and data
1107
   */
1108
  getAttachments(): PDFAttachment[] {
1109
    const savedAttachments = this.getSavedAttachments();
3✔
1110
    const unsavedAttachments = this.getUnsavedAttachments();
3✔
1111

1112
    return [...savedAttachments, ...unsavedAttachments];
3✔
1113
  }
1114

1115
  /**
1116
   * Embed a font into this document. The input data can be provided in multiple
1117
   * formats:
1118
   *
1119
   * | Type            | Contents                                                |
1120
   * | --------------- | ------------------------------------------------------- |
1121
   * | `StandardFonts` | One of the standard 14 fonts                            |
1122
   * | `string`        | A base64 encoded string (or data URI) containing a font |
1123
   * | `Uint8Array`    | The raw bytes of a font                                 |
1124
   * | `ArrayBuffer`   | The raw bytes of a font                                 |
1125
   *
1126
   * For example:
1127
   * ```js
1128
   * // font=StandardFonts
1129
   * import { StandardFonts } from 'pdf-lib'
1130
   * const font1 = await pdfDoc.embedFont(StandardFonts.Helvetica)
1131
   *
1132
   * // font=string
1133
   * const font2 = await pdfDoc.embedFont('AAEAAAAVAQAABABQRFNJRx/upe...')
1134
   * const font3 = await pdfDoc.embedFont('data:font/opentype;base64,AAEAAA...')
1135
   *
1136
   * // font=Uint8Array
1137
   * import fs from 'fs'
1138
   * const font4 = await pdfDoc.embedFont(fs.readFileSync('Ubuntu-R.ttf'))
1139
   *
1140
   * // font=ArrayBuffer
1141
   * const url = 'https://pdf-lib.js.org/assets/ubuntu/Ubuntu-R.ttf'
1142
   * const ubuntuBytes = await fetch(url).then(res => res.arrayBuffer())
1143
   * const font5 = await pdfDoc.embedFont(ubuntuBytes)
1144
   * ```
1145
   * See also: [[registerFontkit]]
1146
   * @param font The input data for a font.
1147
   * @param options The options to be used when embedding the font.
1148
   * @returns Resolves with the embedded font.
1149
   */
1150
  async embedFont(
1151
    font: StandardFonts | string | Uint8Array | ArrayBuffer,
1152
    options: EmbedFontOptions = {},
35✔
1153
  ): Promise<PDFFont> {
1154
    const { subset = false, customName, features } = options;
35✔
1155

1156
    assertIs(font, 'font', ['string', Uint8Array, ArrayBuffer]);
35✔
1157
    assertIs(subset, 'subset', ['boolean']);
35✔
1158

1159
    let embedder: CustomFontEmbedder | StandardFontEmbedder;
1160
    if (isStandardFont(font)) {
35✔
1161
      embedder = StandardFontEmbedder.for(font, customName);
32✔
1162
    } else if (canBeConvertedToUint8Array(font)) {
3!
1163
      const bytes = toUint8Array(font);
3✔
1164
      const fontkit = this.assertFontkit();
3✔
1165
      embedder = subset
3!
1166
        ? await CustomFontSubsetEmbedder.for(
1167
            fontkit,
1168
            bytes,
1169
            customName,
1170
            features,
1171
          )
1172
        : await CustomFontEmbedder.for(fontkit, bytes, customName, features);
1173
    } else {
1174
      throw new TypeError(
×
1175
        '`font` must be one of `StandardFonts | string | Uint8Array | ArrayBuffer`',
1176
      );
1177
    }
1178

1179
    const ref = this.context.nextRef();
35✔
1180
    const pdfFont = PDFFont.of(ref, this, embedder);
35✔
1181
    this.fonts.push(pdfFont);
35✔
1182

1183
    return pdfFont;
35✔
1184
  }
1185

1186
  /**
1187
   * Embed a standard font into this document.
1188
   * For example:
1189
   * ```js
1190
   * import { StandardFonts } from 'pdf-lib'
1191
   * const helveticaFont = pdfDoc.embedFont(StandardFonts.Helvetica)
1192
   * ```
1193
   * @param font The standard font to be embedded.
1194
   * @param customName The name to be used when embedding the font.
1195
   * @returns The embedded font.
1196
   */
1197
  embedStandardFont(font: StandardFonts, customName?: string): PDFFont {
1198
    assertIs(font, 'font', ['string']);
30✔
1199
    if (!isStandardFont(font)) {
30✔
1200
      throw new TypeError('`font` must be one of type `StandardFonts`');
1✔
1201
    }
1202

1203
    const embedder = StandardFontEmbedder.for(font, customName);
29✔
1204

1205
    const ref = this.context.nextRef();
29✔
1206
    const pdfFont = PDFFont.of(ref, this, embedder);
29✔
1207
    this.fonts.push(pdfFont);
29✔
1208

1209
    return pdfFont;
29✔
1210
  }
1211

1212
  /**
1213
   * Embed a JPEG image into this document. The input data can be provided in
1214
   * multiple formats:
1215
   *
1216
   * | Type          | Contents                                                      |
1217
   * | ------------- | ------------------------------------------------------------- |
1218
   * | `string`      | A base64 encoded string (or data URI) containing a JPEG image |
1219
   * | `Uint8Array`  | The raw bytes of a JPEG image                                 |
1220
   * | `ArrayBuffer` | The raw bytes of a JPEG image                                 |
1221
   *
1222
   * For example:
1223
   * ```js
1224
   * // jpg=string
1225
   * const image1 = await pdfDoc.embedJpg('/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD...')
1226
   * const image2 = await pdfDoc.embedJpg('data:image/jpeg;base64,/9j/4AAQ...')
1227
   *
1228
   * // jpg=Uint8Array
1229
   * import fs from 'fs'
1230
   * const uint8Array = fs.readFileSync('cat_riding_unicorn.jpg')
1231
   * const image3 = await pdfDoc.embedJpg(uint8Array)
1232
   *
1233
   * // jpg=ArrayBuffer
1234
   * const url = 'https://pdf-lib.js.org/assets/cat_riding_unicorn.jpg'
1235
   * const arrayBuffer = await fetch(url).then(res => res.arrayBuffer())
1236
   * const image4 = await pdfDoc.embedJpg(arrayBuffer)
1237
   * ```
1238
   *
1239
   * @param jpg The input data for a JPEG image.
1240
   * @returns Resolves with the embedded image.
1241
   */
1242
  async embedJpg(jpg: string | Uint8Array | ArrayBuffer): Promise<PDFImage> {
1243
    assertIs(jpg, 'jpg', ['string', Uint8Array, ArrayBuffer]);
×
1244
    const bytes = toUint8Array(jpg);
×
1245
    const embedder = await JpegEmbedder.for(bytes);
×
1246
    const ref = this.context.nextRef();
×
1247
    const pdfImage = PDFImage.of(ref, this, embedder);
×
1248
    this.images.push(pdfImage);
×
1249
    return pdfImage;
×
1250
  }
1251

1252
  /**
1253
   * Embed a PNG image into this document. The input data can be provided in
1254
   * multiple formats:
1255
   *
1256
   * | Type          | Contents                                                     |
1257
   * | ------------- | ------------------------------------------------------------ |
1258
   * | `string`      | A base64 encoded string (or data URI) containing a PNG image |
1259
   * | `Uint8Array`  | The raw bytes of a PNG image                                 |
1260
   * | `ArrayBuffer` | The raw bytes of a PNG image                                 |
1261
   *
1262
   * For example:
1263
   * ```js
1264
   * // png=string
1265
   * const image1 = await pdfDoc.embedPng('iVBORw0KGgoAAAANSUhEUgAAAlgAAAF3...')
1266
   * const image2 = await pdfDoc.embedPng('data:image/png;base64,iVBORw0KGg...')
1267
   *
1268
   * // png=Uint8Array
1269
   * import fs from 'fs'
1270
   * const uint8Array = fs.readFileSync('small_mario.png')
1271
   * const image3 = await pdfDoc.embedPng(uint8Array)
1272
   *
1273
   * // png=ArrayBuffer
1274
   * const url = 'https://pdf-lib.js.org/assets/small_mario.png'
1275
   * const arrayBuffer = await fetch(url).then(res => res.arrayBuffer())
1276
   * const image4 = await pdfDoc.embedPng(arrayBuffer)
1277
   * ```
1278
   *
1279
   * @param png The input data for a PNG image.
1280
   * @returns Resolves with the embedded image.
1281
   */
1282
  async embedPng(png: string | Uint8Array | ArrayBuffer): Promise<PDFImage> {
1283
    assertIs(png, 'png', ['string', Uint8Array, ArrayBuffer]);
5✔
1284
    const bytes = toUint8Array(png);
5✔
1285
    const embedder = await PngEmbedder.for(bytes);
5✔
1286
    const ref = this.context.nextRef();
5✔
1287
    const pdfImage = PDFImage.of(ref, this, embedder);
5✔
1288
    this.images.push(pdfImage);
5✔
1289
    return pdfImage;
5✔
1290
  }
1291

1292
  async embedSvg(svg: string): Promise<PDFSvg> {
1293
    if (!svg) return new PDFSvg(svg);
×
1294
    const parsedSvg = parseHtml(svg);
×
1295
    const findImages = (element: HTMLElement): HTMLElement[] => {
×
1296
      if (element.tagName === 'image') return [element];
×
1297
      else {
1298
        return element.childNodes
×
1299
          .map((child) =>
1300
            child.nodeType === NodeType.ELEMENT_NODE ? findImages(child) : [],
×
1301
          )
1302
          .flat();
1303
      }
1304
    };
1305
    const images = findImages(parsedSvg);
×
1306
    const imagesDict = {} as Record<string, PDFImage>;
×
1307

1308
    await Promise.all(
×
1309
      images.map(async (image) => {
×
1310
        const href = image.attributes.href ?? image.attributes['xlink:href'];
×
1311
        if (!href || imagesDict[href]) return;
×
1312
        const isPng = href.match(/\.png(\?|$)|^data:image\/png;base64/gim);
×
1313
        const pdfImage = isPng
×
1314
          ? await this.embedPng(href)
1315
          : await this.embedJpg(href);
1316
        imagesDict[href] = pdfImage;
×
1317
      }),
1318
    );
1319

1320
    return new PDFSvg(svg, imagesDict);
×
1321
  }
1322
  /**
1323
   * Embed one or more PDF pages into this document.
1324
   *
1325
   * For example:
1326
   * ```js
1327
   * const pdfDoc = await PDFDocument.create()
1328
   *
1329
   * const sourcePdfUrl = 'https://pdf-lib.js.org/assets/with_large_page_count.pdf'
1330
   * const sourcePdf = await fetch(sourcePdfUrl).then((res) => res.arrayBuffer())
1331
   *
1332
   * // Embed page 74 of `sourcePdf` into `pdfDoc`
1333
   * const [embeddedPage] = await pdfDoc.embedPdf(sourcePdf, [73])
1334
   * ```
1335
   *
1336
   * See [[PDFDocument.load]] for examples of the allowed input data formats.
1337
   *
1338
   * @param pdf The input data containing a PDF document.
1339
   * @param indices The indices of the pages that should be embedded.
1340
   * @returns Resolves with an array of the embedded pages.
1341
   */
1342
  async embedPdf(
1343
    pdf: string | Uint8Array | ArrayBuffer | PDFDocument,
1344
    indices: number[] = [0],
×
1345
  ): Promise<PDFEmbeddedPage[]> {
1346
    assertIs(pdf, 'pdf', [
×
1347
      'string',
1348
      Uint8Array,
1349
      ArrayBuffer,
1350
      [PDFDocument, 'PDFDocument'],
1351
    ]);
1352
    assertIs(indices, 'indices', [Array]);
×
1353

1354
    const srcDoc =
1355
      pdf instanceof PDFDocument ? pdf : await PDFDocument.load(pdf);
×
1356

1357
    const srcPages = pluckIndices(srcDoc.getPages(), indices);
×
1358

1359
    return this.embedPages(srcPages);
×
1360
  }
1361

1362
  /**
1363
   * Embed a single PDF page into this document.
1364
   *
1365
   * For example:
1366
   * ```js
1367
   * const pdfDoc = await PDFDocument.create()
1368
   *
1369
   * const sourcePdfUrl = 'https://pdf-lib.js.org/assets/with_large_page_count.pdf'
1370
   * const sourceBuffer = await fetch(sourcePdfUrl).then((res) => res.arrayBuffer())
1371
   * const sourcePdfDoc = await PDFDocument.load(sourceBuffer)
1372
   * const sourcePdfPage = sourcePdfDoc.getPages()[73]
1373
   *
1374
   * const embeddedPage = await pdfDoc.embedPage(
1375
   *   sourcePdfPage,
1376
   *
1377
   *   // Clip a section of the source page so that we only embed part of it
1378
   *   { left: 100, right: 450, bottom: 330, top: 570 },
1379
   *
1380
   *   // Translate all drawings of the embedded page by (10, 200) units
1381
   *   [1, 0, 0, 1, 10, 200],
1382
   * )
1383
   * ```
1384
   *
1385
   * @param page The page to be embedded.
1386
   * @param boundingBox
1387
   * Optionally, an area of the source page that should be embedded
1388
   * (defaults to entire page).
1389
   * @param transformationMatrix
1390
   * Optionally, a transformation matrix that is always applied to the embedded
1391
   * page anywhere it is drawn.
1392
   * @returns Resolves with the embedded pdf page.
1393
   */
1394
  async embedPage(
1395
    page: PDFPage,
1396
    boundingBox?: PageBoundingBox,
1397
    transformationMatrix?: TransformationMatrix,
1398
  ): Promise<PDFEmbeddedPage> {
1399
    assertIs(page, 'page', [[PDFPage, 'PDFPage']]);
×
1400
    const [embeddedPage] = await this.embedPages(
×
1401
      [page],
1402
      [boundingBox],
1403
      [transformationMatrix],
1404
    );
1405
    return embeddedPage;
×
1406
  }
1407

1408
  /**
1409
   * Embed one or more PDF pages into this document.
1410
   *
1411
   * For example:
1412
   * ```js
1413
   * const pdfDoc = await PDFDocument.create()
1414
   *
1415
   * const sourcePdfUrl = 'https://pdf-lib.js.org/assets/with_large_page_count.pdf'
1416
   * const sourceBuffer = await fetch(sourcePdfUrl).then((res) => res.arrayBuffer())
1417
   * const sourcePdfDoc = await PDFDocument.load(sourceBuffer)
1418
   *
1419
   * const page1 = sourcePdfDoc.getPages()[0]
1420
   * const page2 = sourcePdfDoc.getPages()[52]
1421
   * const page3 = sourcePdfDoc.getPages()[73]
1422
   *
1423
   * const embeddedPages = await pdfDoc.embedPages([page1, page2, page3])
1424
   * ```
1425
   *
1426
   * @param page
1427
   * The pages to be embedded (they must all share the same context).
1428
   * @param boundingBoxes
1429
   * Optionally, an array of clipping boundaries - one for each page
1430
   * (defaults to entirety of each page).
1431
   * @param transformationMatrices
1432
   * Optionally, an array of transformation matrices - one for each page
1433
   * (each page's transformation will apply anywhere it is drawn).
1434
   * @returns Resolves with an array of the embedded pdf pages.
1435
   */
1436
  async embedPages(
1437
    pages: PDFPage[],
1438
    boundingBoxes: (PageBoundingBox | undefined)[] = [],
×
1439
    transformationMatrices: (TransformationMatrix | undefined)[] = [],
×
1440
  ) {
1441
    if (pages.length === 0) return [];
×
1442

1443
    // Assert all pages have the same context
1444
    for (let idx = 0, len = pages.length - 1; idx < len; idx++) {
×
1445
      const currPage = pages[idx];
×
1446
      const nextPage = pages[idx + 1];
×
1447
      if (currPage.node.context !== nextPage.node.context) {
×
1448
        throw new PageEmbeddingMismatchedContextError();
×
1449
      }
1450
    }
1451

1452
    const context = pages[0].node.context;
×
1453
    const maybeCopyPage =
1454
      context === this.context
×
1455
        ? (p: PDFPageLeaf) => p
×
1456
        : PDFObjectCopier.for(context, this.context).copy;
1457

1458
    const embeddedPages = new Array<PDFEmbeddedPage>(pages.length);
×
1459
    for (let idx = 0, len = pages.length; idx < len; idx++) {
×
1460
      const page = maybeCopyPage(pages[idx].node);
×
1461
      const box = boundingBoxes[idx];
×
1462
      const matrix = transformationMatrices[idx];
×
1463

1464
      const embedder = await PDFPageEmbedder.for(page, box, matrix);
×
1465

1466
      const ref = this.context.nextRef();
×
1467
      embeddedPages[idx] = PDFEmbeddedPage.of(ref, this, embedder);
×
1468
    }
1469

1470
    this.embeddedPages.push(...embeddedPages);
×
1471

1472
    return embeddedPages;
×
1473
  }
1474

1475
  encrypt(options: SecurityOptions) {
1476
    this.context.security = PDFSecurity.create(this.context, options).encrypt();
×
1477
  }
1478

1479
  /**
1480
   * > **NOTE:** You shouldn't need to call this method directly. The [[save]]
1481
   * > and [[saveAsBase64]] methods will automatically ensure that all embedded
1482
   * > assets are flushed before serializing the document.
1483
   *
1484
   * Flush all embedded fonts, PDF pages, and images to this document's
1485
   * [[context]].
1486
   *
1487
   * @returns Resolves when the flush is complete.
1488
   */
1489
  async flush(): Promise<void> {
1490
    await this.embedAll(this.fonts);
97✔
1491
    await this.embedAll(this.images);
97✔
1492
    await this.embedAll(this.embeddedPages);
97✔
1493
    await this.embedAll(this.embeddedFiles);
97✔
1494
    await this.embedAll(this.javaScripts);
97✔
1495
  }
1496

1497
  /**
1498
   * Serialize this document to an array of bytes making up a PDF file.
1499
   * For example:
1500
   * ```js
1501
   * const pdfBytes = await pdfDoc.save()
1502
   * ```
1503
   *
1504
   * There are a number of things you can do with the serialized document,
1505
   * depending on the JavaScript environment you're running in:
1506
   * * Write it to a file in Node or React Native
1507
   * * Download it as a Blob in the browser
1508
   * * Render it in an `iframe`
1509
   *
1510
   * @param options The options to be used when saving the document.
1511
   * @returns Resolves with the bytes of the serialized document.
1512
   */
1513
  async save(options: SaveOptions = {}): Promise<Uint8Array> {
28✔
1514
    // check PDF version
1515
    const vparts = this.context.header.getVersionString().split('.');
44✔
1516
    const uOS =
1517
      options.rewrite || Number(vparts[0]) > 1 || Number(vparts[1]) >= 5;
44✔
1518
    const {
1519
      useObjectStreams = uOS,
36✔
1520
      objectsPerTick = 50,
44✔
1521
      rewrite = false,
39✔
1522
    } = options;
44✔
1523

1524
    assertIs(useObjectStreams, 'useObjectStreams', ['boolean']);
44✔
1525
    assertIs(objectsPerTick, 'objectsPerTick', ['number']);
44✔
1526
    assertIs(rewrite, 'rewrite', ['boolean']);
44✔
1527
    const incrementalUpdate =
1528
      !rewrite &&
44✔
1529
      this.context.pdfFileDetails.originalBytes &&
1530
      this.context.snapshot;
1531
    if (incrementalUpdate) {
44✔
1532
      options.addDefaultPage = false;
18✔
1533
      options.updateFieldAppearances = false;
18✔
1534
    }
1535

1536
    await this.prepareForSave(options);
44✔
1537

1538
    const Writer = useObjectStreams ? PDFStreamWriter : PDFWriter;
44✔
1539
    if (incrementalUpdate) {
44✔
1540
      const increment = await Writer.forContextWithSnapshot(
18✔
1541
        this.context,
1542
        objectsPerTick,
1543
        this.context.snapshot!,
1544
      ).serializeToBuffer();
1545
      const result = new Uint8Array(
18✔
1546
        this.context.pdfFileDetails.originalBytes!.byteLength +
1547
          increment.byteLength,
1548
      );
1549
      result.set(this.context.pdfFileDetails.originalBytes!);
18✔
1550
      result.set(
18✔
1551
        increment,
1552
        this.context.pdfFileDetails.originalBytes!.byteLength,
1553
      );
1554
      return result;
18✔
1555
    }
1556
    return Writer.forContext(this.context, objectsPerTick).serializeToBuffer();
26✔
1557
  }
1558

1559
  /**
1560
   * Serialize only the changes to this document to an array of bytes making up a PDF file.
1561
   * For example:
1562
   * ```js
1563
   * const snapshot = pdfDoc.takeSnapshot();
1564
   * ...
1565
   * const pdfBytes = await pdfDoc.saveIncremental(snapshot);
1566
   * ```
1567
   *
1568
   * Similar to [[save]] function.
1569
   * The changes are saved in an incremental way, the result buffer
1570
   * will contain only the differences
1571
   *
1572
   * @param snapshot The snapshot to be used when saving the document.
1573
   * @param options The options to be used when saving the document.
1574
   * @returns Resolves with the bytes of the serialized document.
1575
   */
1576
  async saveIncremental(
1577
    snapshot: DocumentSnapshot,
1578
    options: IncrementalSaveOptions = {},
6✔
1579
  ): Promise<Uint8Array> {
1580
    // check PDF version
1581
    const vparts = this.context.header.getVersionString().split('.');
50✔
1582
    const uOS = Number(vparts[0]) > 1 || Number(vparts[1]) >= 5;
50✔
1583
    const { objectsPerTick = 50 } = options;
50✔
1584

1585
    assertIs(objectsPerTick, 'objectsPerTick', ['number']);
50✔
1586

1587
    const saveOptions: SaveOptions = {
50✔
1588
      useObjectStreams: uOS,
1589
      ...options,
1590
      addDefaultPage: false,
1591
      updateFieldAppearances: false,
1592
    };
1593
    await this.prepareForSave(saveOptions);
50✔
1594

1595
    const Writer = saveOptions.useObjectStreams ? PDFStreamWriter : PDFWriter;
50✔
1596
    return Writer.forContextWithSnapshot(
50✔
1597
      this.context,
1598
      objectsPerTick,
1599
      snapshot,
1600
    ).serializeToBuffer();
1601
  }
1602

1603
  /**
1604
   * Serialize this document to a base64 encoded string or data URI making up a
1605
   * PDF file. For example:
1606
   * ```js
1607
   * const base64String = await pdfDoc.saveAsBase64()
1608
   * base64String // => 'JVBERi0xLjcKJYGBgYEKC...'
1609
   *
1610
   * const base64DataUri = await pdfDoc.saveAsBase64({ dataUri: true })
1611
   * base64DataUri // => 'data:application/pdf;base64,JVBERi0xLjcKJYGBgYEKC...'
1612
   * ```
1613
   *
1614
   * @param options The options to be used when saving the document.
1615
   * @returns Resolves with a base64 encoded string or data URI of the
1616
   *          serialized document.
1617
   */
1618
  async saveAsBase64(options: Base64SaveOptions = {}): Promise<string> {
×
1619
    const { dataUri = false, ...otherOptions } = options;
×
1620
    assertIs(dataUri, 'dataUri', ['boolean']);
×
1621
    const bytes = await this.save(otherOptions);
×
1622
    const base64 = encodeToBase64(bytes);
×
1623
    return dataUri ? `data:application/pdf;base64,${base64}` : base64;
×
1624
  }
1625

1626
  findPageForAnnotationRef(ref: PDFRef): PDFPage | undefined {
1627
    const pages = this.getPages();
×
1628
    for (let idx = 0, len = pages.length; idx < len; idx++) {
×
1629
      const page = pages[idx];
×
1630
      const annotations = page.node.Annots();
×
1631

1632
      if (annotations?.indexOf(ref) !== undefined) {
×
1633
        return page;
×
1634
      }
1635
    }
1636

1637
    return undefined;
×
1638
  }
1639

1640
  takeSnapshot(): DocumentSnapshot {
1641
    const indirectObjects: number[] = [];
93✔
1642

1643
    const snapshot = new IncrementalDocumentSnapshot(
93✔
1644
      this.context.largestObjectNumber,
1645
      indirectObjects,
1646
      this.context.pdfFileDetails.pdfSize,
1647
      this.context.pdfFileDetails.prevStartXRef,
1648
      this.context,
1649
    );
1650
    if (!this.context.snapshot && this.context.pdfFileDetails.originalBytes) {
93✔
1651
      this.context.snapshot = snapshot;
38✔
1652
      this.catalog.registerChange();
38✔
1653
    }
1654
    return snapshot;
93✔
1655
  }
1656

1657
  /**
1658
   * Returns the update version of the object as 'actual', and all the previous versions, of the objects
1659
   * that has changed in the indicated update (or the last one).
1660
   * If document wasn't load to preserve objects versions, an empty array is returned.
1661
   * @param {number} lastUpdateMinusX If not the last update, how many updates before the last.
1662
   * @returns  {PDFObjectVersions[]} Objects modified in the update, and previous versions
1663
   */
1664
  getChangedObjects(lastUpdateMinusX: number = 0): PDFObjectVersions[] {
6✔
1665
    if (!this.context.preserveObjectsVersions) return [];
8✔
1666
    if (lastUpdateMinusX < 0) lastUpdateMinusX = 0;
6!
1667
    const upind = this.context.xrefs.length - lastUpdateMinusX - 1;
6✔
1668
    const entries = this.context.listXrefEntries(upind);
6✔
1669
    if (!entries.length) return [];
6!
1670
    const changed = new Map<PDFRef, PDFObjectVersions>();
6✔
1671
    for (const entry of entries) {
6✔
1672
      const ref = entry.ref;
370✔
1673
      changed.set(ref, {
370✔
1674
        ref,
1675
        actual: entry.deleted ? undefined : this.context.lookup(ref),
370✔
1676
        previous: this.context.getObjectVersions(ref),
1677
      });
1678
    }
1679
    // if not the las update, then check objects later modified and adjust PDFObjectVersions accordingly
1680
    if (!lastUpdateMinusX)
6✔
1681
      return Array.from(changed.entries()).map((value) => value[1]);
32✔
1682
    while (lastUpdateMinusX) {
2✔
1683
      lastUpdateMinusX -= 1;
3✔
1684
      const upind = this.context.xrefs.length - lastUpdateMinusX - 1;
3✔
1685
      const nentries = this.context.listXrefEntries(upind);
3✔
1686
      for (const nentry of nentries) {
3✔
1687
        const oce = changed.get(nentry.ref);
343✔
1688
        if (oce && oce.actual) {
343✔
1689
          oce.actual = oce.previous[0];
11✔
1690
          oce.previous = oce.previous.slice(1);
11✔
1691
        }
1692
      }
1693
    }
1694
    // if PDF has errors, it may happen to end with objects that has no current, nor previous versions
1695
    return Array.from(changed.entries())
2✔
1696
      .map((value) => value[1])
338✔
1697
      .filter((ov) => ov.actual || ov.previous.length);
338✔
1698
  }
1699

1700
  /**
1701
   * Saves the current changes to the document as an incremental update, returns the full document,
1702
   * like save method, and modifies the internal state to be able to continue editing the document
1703
   * for another incremental update.
1704
   * This allows you to save multiple incremental updates without reloading the PDF.
1705
   *
1706
   * For example:
1707
   * ```js
1708
   * const pdfDoc = await PDFDocument.load(pdfBytes, { forIncrementalUpdate: true })
1709
   *
1710
   * const page = pdfDoc.getPage(0)
1711
   * page.drawText('First update')
1712
   * const firstsave = await pdfDoc.saveAndContinue()
1713
   *
1714
   * page.drawText('Second update', { y: 100 })
1715
   * const secondsave = await pdfDoc.saveAndContinue()
1716
   * ```
1717
   *
1718
   * @param options The options to be used when saving changes.
1719
   * @returns Resolves with the complete PDF bytes including all updates.
1720
   */
1721
  async saveAndContinue(
1722
    options: IncrementalSaveOptions = {},
40✔
1723
  ): Promise<Uint8Array> {
1724
    if (!this.context.pdfFileDetails.originalBytes || !this.context.snapshot) {
40✔
1725
      throw new Error(
1✔
1726
        'saveAndContinue() requires the document to be loaded with forIncrementalUpdate: true',
1727
      );
1728
    }
1729
    const originalBytes = this.context.pdfFileDetails.originalBytes;
39✔
1730
    const incrementalBytes = await this.saveIncremental(
39✔
1731
      this.context.snapshot,
1732
      options,
1733
    );
1734

1735
    const newPdfBytes = new Uint8Array(
39✔
1736
      originalBytes.byteLength + incrementalBytes.byteLength,
1737
    );
1738
    newPdfBytes.set(originalBytes);
39✔
1739
    newPdfBytes.set(incrementalBytes, originalBytes.byteLength);
39✔
1740

1741
    this.context.pdfFileDetails.originalBytes = newPdfBytes;
39✔
1742
    this.context.pdfFileDetails.pdfSize = newPdfBytes.byteLength;
39✔
1743

1744
    const incrementalStr = new TextDecoder('latin1').decode(incrementalBytes);
39✔
1745
    const startxrefMatch = incrementalStr.match(/startxref\s+(\d+)/);
39✔
1746
    if (startxrefMatch) {
39!
1747
      this.context.pdfFileDetails.prevStartXRef = parseInt(
39✔
1748
        startxrefMatch[1],
1749
        10,
1750
      );
1751
    } else {
1752
      this.context.pdfFileDetails.prevStartXRef = originalBytes.byteLength;
×
1753
    }
1754

1755
    this.context.snapshot = this.takeSnapshot();
39✔
1756

1757
    return newPdfBytes;
39✔
1758
  }
1759

1760
  private async prepareForSave(options: SaveOptions): Promise<void> {
1761
    const { addDefaultPage = true, updateFieldAppearances = true } = options;
94✔
1762

1763
    assertIs(addDefaultPage, 'addDefaultPage', ['boolean']);
94✔
1764
    assertIs(updateFieldAppearances, 'updateFieldAppearances', ['boolean']);
94✔
1765

1766
    if (addDefaultPage && this.getPageCount() === 0) this.addPage();
94✔
1767

1768
    if (updateFieldAppearances) {
94✔
1769
      const form = this.formCache.getValue();
25✔
1770
      if (form) form.updateFieldAppearances();
25✔
1771
    }
1772

1773
    await this.flush();
94✔
1774
  }
1775

1776
  private async embedAll(embeddables: Embeddable[]): Promise<void> {
1777
    for (let idx = 0, len = embeddables.length; idx < len; idx++) {
485✔
1778
      await embeddables[idx].embed();
98✔
1779
    }
1780
  }
1781

1782
  private updateInfoDict(): void {
1783
    const pdfLib = 'pdf-lib (https://github.com/Hopding/pdf-lib)';
189✔
1784
    const now = new Date();
189✔
1785

1786
    const info = this.getInfoDict();
189✔
1787

1788
    this.setProducer(pdfLib);
189✔
1789
    this.setModificationDate(now);
189✔
1790

1791
    if (!info.get(PDFName.of('Creator'))) this.setCreator(pdfLib);
189✔
1792
    if (!info.get(PDFName.of('CreationDate'))) this.setCreationDate(now);
189✔
1793
  }
1794

1795
  private getInfoDict(): PDFDict {
1796
    const existingInfo = this.context.lookup(this.context.trailerInfo.Info);
777✔
1797
    if (existingInfo instanceof PDFDict) {
777✔
1798
      return existingInfo;
723✔
1799
    }
1800

1801
    const newInfo = this.context.obj({});
54✔
1802
    this.context.trailerInfo.Info = this.context.register(newInfo);
54✔
1803

1804
    return newInfo;
54✔
1805
  }
1806

1807
  private assertFontkit(): Fontkit {
1808
    if (!this.fontkit) throw new FontkitNotRegisteredError();
3!
1809
    return this.fontkit;
3✔
1810
  }
1811

1812
  private computePages = (): PDFPage[] => {
198✔
1813
    const pages: PDFPage[] = [];
124✔
1814
    this.catalog.Pages().traverse((node, ref) => {
124✔
1815
      if (node instanceof PDFPageLeaf) {
592✔
1816
        let page = this.pageMap.get(node);
510✔
1817
        if (!page) {
510✔
1818
          page = PDFPage.of(node, ref, this);
503✔
1819
          this.pageMap.set(node, page);
503✔
1820
        }
1821
        pages.push(page);
510✔
1822
      }
1823
    });
1824
    return pages;
124✔
1825
  };
1826

1827
  private getOrCreateForm = (): PDFForm => {
198✔
1828
    const acroForm = this.catalog.getOrCreateAcroForm();
51✔
1829
    return PDFForm.of(acroForm, this);
51✔
1830
  };
1831
}
1832

1833
/* tslint:disable-next-line only-arrow-functions */
1834
function assertIsLiteralOrHexString(
1835
  pdfObject: PDFObject,
1836
): asserts pdfObject is PDFHexString | PDFString {
1837
  if (
53!
1838
    !(pdfObject instanceof PDFHexString) &&
70✔
1839
    !(pdfObject instanceof PDFString)
1840
  ) {
1841
    throw new UnexpectedObjectTypeError([PDFHexString, PDFString], pdfObject);
×
1842
  }
1843
}
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