import { parse as parseHtml, HTMLElement, NodeType, } from 'node-html-better-parser'; import Embeddable from './Embeddable'; import { EncryptedPDFError, FontkitNotRegisteredError, ForeignPageError, RemovePageFromEmptyDocumentError, } from './errors'; import PDFEmbeddedPage from './PDFEmbeddedPage'; import PDFFont from './PDFFont'; import PDFImage from './PDFImage'; import PDFPage from './PDFPage'; import PDFForm from './form/PDFForm'; import { PageSizes } from './sizes'; import { StandardFonts } from './StandardFonts'; import { CustomFontEmbedder, CustomFontSubsetEmbedder, JpegEmbedder, PageBoundingBox, PageEmbeddingMismatchedContextError, PDFArray, PDFCatalog, PDFContext, PDFDict, PDFHeader, decodePDFRawStream, PDFStream, PDFRawStream, PDFHexString, PDFName, PDFObjectCopier, PDFPageEmbedder, PDFPageLeaf, PDFPageTree, PDFParser, PDFStreamWriter, PDFString, PDFWriter, PngEmbedder, StandardFontEmbedder, UnexpectedObjectTypeError, } from '../core'; import { ParseSpeeds, AttachmentOptions, SaveOptions, Base64SaveOptions, LoadOptions, CreateOptions, EmbedFontOptions, SetTitleOptions, IncrementalSaveOptions, } from './PDFDocumentOptions'; import PDFObject from '../core/objects/PDFObject'; import PDFRef from '../core/objects/PDFRef'; import { Fontkit } from '../types/fontkit'; import { TransformationMatrix } from '../types/matrix'; import { assertIs, assertIsOneOfOrUndefined, assertOrUndefined, assertRange, BinaryData, Cache, canBeConvertedToUint8Array, encodeToBase64, isStandardFont, pluckIndices, range, toUint8Array, utf8Encode, } from '../utils'; import FileEmbedder, { AFRelationship } from '../core/embedders/FileEmbedder'; import PDFEmbeddedFile from './PDFEmbeddedFile'; import PDFJavaScript from './PDFJavaScript'; import JavaScriptEmbedder from '../core/embedders/JavaScriptEmbedder'; import PDFJavaScriptAction from './PDFJavaScriptAction'; import { CipherTransformFactory } from '../core/crypto'; import PDFSvg from './PDFSvg'; import PDFSecurity, { SecurityOptions, generateRandomFileId, } from '../core/security/PDFSecurity'; import { IncrementalDocumentSnapshot } from './snapshot'; import type { DocumentSnapshot } from './snapshot'; import { ConvertToPDFAOptions, buildPDFAMetadata, extractForeignXmpDescriptions, mergeXmpExtensionFragments, getDefaultSRGBProfile, parseConformance, ParsedConformance, } from './pdfa'; import { readCatalogMetadataXml, readCatalogPDFAConformance, } from './pdfa/catalogMetadata'; import type { PDFOptionalContentGroup, OptionalContentVisibilityUpdate, } from '../core/interactive/OptionalContent'; export type BasePDFAttachment = { name: string; data: Uint8Array; mimeType: string | undefined; afRelationship: AFRelationship | undefined; description: string | undefined; creationDate: Date | undefined; modificationDate: Date | undefined; }; export type SavedPDFAttachment = BasePDFAttachment & { embeddedFileDict: PDFDict; specRef: PDFRef; }; export type UnsavedPDFAttachment = BasePDFAttachment & { pdfEmbeddedFile: PDFEmbeddedFile; }; export type PDFAttachment = UnsavedPDFAttachment | SavedPDFAttachment; /** * Represents a PDF document. */ export default class PDFDocument { /** * Load an existing [[PDFDocument]]. The input data can be provided in * multiple formats: * * | Type | Contents | * | ------------------ | ------------------------------------------------------ | * | `string` | A base64 encoded string (or data URI) containing a PDF | * | `Uint8Array` | The raw bytes of a PDF | * | `ArrayBuffer` | The raw bytes of a PDF | * | `ArrayBufferView` | The raw bytes of a PDF (includes Node.js `Buffer`) | * * For example: * ```js * import { PDFDocument } from 'pdf-lib' * * // pdf=string * const base64 = * 'JVBERi0xLjcKJYGBgYEKCjUgMCBvYmoKPDwKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbm' + * 'd0aCAxMDQKPj4Kc3RyZWFtCniccwrhMlAAwaJ0Ln2P1Jyy1JLM5ERdc0MjCwUjE4WQNC4Q' + * '6cNlCFZkqGCqYGSqEJLLZWNuYGZiZmbkYuZsZmlmZGRgZmluDCQNzc3NTM2NzdzMXMxMjQ' + * 'ztFEKyuEK0uFxDuAAOERdVCmVuZHN0cmVhbQplbmRvYmoKCjYgMCBvYmoKPDwKL0ZpbHRl' + * 'ciAvRmxhdGVEZWNvZGUKL1R5cGUgL09ialN0bQovTiA0Ci9GaXJzdCAyMAovTGVuZ3RoID' + * 'IxNQo+PgpzdHJlYW0KeJxVj9GqwjAMhu/zFHkBzTo3nCCCiiKIHPEICuJF3cKoSCu2E8/b' + * '20wPIr1p8v9/8kVhgilmGfawX2CGaVrgcAi0/bsy0lrX7IGWpvJ4iJYEN3gEmrrGBlQwGs' + * 'HHO9VBX1wNrxAqMX87RBD5xpJuddqwd82tjAHxzV1U5LPgy52DKXWnr1Lheg+j/c/pzGVr' + * 'iqV0VlwZPXGPCJjElw/ybkwUmeoWgxesDXGhHJC/D/iikp1Av80ptKU0FdBEe25pPihAM1' + * 'u6ytgaaWfs2Hrz35CJT1+EWmAKZW5kc3RyZWFtCmVuZG9iagoKNyAwIG9iago8PAovU2l6' + * 'ZSA4Ci9Sb290IDIgMCBSCi9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9UeXBlIC9YUmVmCi9MZW' + * '5ndGggMzgKL1cgWyAxIDIgMiBdCi9JbmRleCBbIDAgOCBdCj4+CnN0cmVhbQp4nBXEwREA' + * 'EBAEsCwz3vrvRmOOyyOoGhZdutHN2MT55fIAVocD+AplbmRzdHJlYW0KZW5kb2JqCgpzdG' + * 'FydHhyZWYKNTEwCiUlRU9G' * * const dataUri = 'data:application/pdf;base64,' + base64 * * const pdfDoc1 = await PDFDocument.load(base64) * const pdfDoc2 = await PDFDocument.load(dataUri) * * // pdf=Uint8Array / Node Buffer * import fs from 'fs' * const bytes = fs.readFileSync('with_update_sections.pdf') * const pdfDoc3 = await PDFDocument.load(bytes) * * // pdf=ArrayBuffer * const url = 'https://pdf-lib.js.org/assets/with_update_sections.pdf' * const arrayBuffer = await fetch(url).then(res => res.arrayBuffer()) * const pdfDoc4 = await PDFDocument.load(arrayBuffer) * * ``` * * @param pdf The input data containing a PDF document. * @param options The options to be used when loading the document. * @returns Resolves with a document loaded from the input. */ static async load(pdf: BinaryData, options: LoadOptions = {}) { const { ignoreEncryption = false, parseSpeed = ParseSpeeds.Slow, throwOnInvalidObject = false, warnOnInvalidObjects = false, updateMetadata = true, capNumbers = false, password, forIncrementalUpdate = false, preserveXFA = false, } = options; assertIs(pdf, 'pdf', ['string', ArrayBuffer, 'ArrayBufferView']); assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']); assertIs(parseSpeed, 'parseSpeed', ['number']); assertIs(throwOnInvalidObject, 'throwOnInvalidObject', ['boolean']); assertIs(warnOnInvalidObjects, 'warnOnInvalidObjects', ['boolean']); assertIs(password, 'password', ['string', 'undefined']); assertIs(forIncrementalUpdate, 'forIncrementalUpdate', ['boolean']); const bytes = toUint8Array(pdf); const context = await PDFParser.forBytesWithOptions( bytes, parseSpeed, throwOnInvalidObject, undefined, capNumbers, undefined, forIncrementalUpdate, ).parseDocument(); if ( !!context.lookup(context.trailerInfo.Encrypt) && password !== undefined ) { // Decrypt const fileIds = context.lookup(context.trailerInfo.ID, PDFArray); const encryptDict = context.lookup(context.trailerInfo.Encrypt, PDFDict); const decryptedContext = await PDFParser.forBytesWithOptions( bytes, parseSpeed, throwOnInvalidObject, warnOnInvalidObjects, capNumbers, new CipherTransformFactory( encryptDict, (fileIds.get(0) as PDFHexString).asBytes(), password, ), forIncrementalUpdate, ).parseDocument(); const pdfDoc = new PDFDocument( decryptedContext, true, updateMetadata, preserveXFA, ); if (forIncrementalUpdate) pdfDoc.takeSnapshot(); return pdfDoc; } else { const pdfDoc = new PDFDocument( context, ignoreEncryption, updateMetadata, preserveXFA, ); if (forIncrementalUpdate) pdfDoc.takeSnapshot(); return pdfDoc; } } /** * Create a new [[PDFDocument]]. * @returns Resolves with the newly created document. */ static async create(options: CreateOptions = {}) { const { updateMetadata = true } = options; const context = PDFContext.create(); const pageTree = PDFPageTree.withContext(context); const pageTreeRef = context.register(pageTree); const catalog = PDFCatalog.withContextAndPages(context, pageTreeRef); context.trailerInfo.Root = context.register(catalog); return new PDFDocument(context, false, updateMetadata); } /** The low-level context of this document. */ readonly context: PDFContext; /** The catalog of this document. */ readonly catalog: PDFCatalog; /** Whether or not this document is encrypted. */ readonly isEncrypted: boolean; /** The default word breaks used in PDFPage.drawText */ defaultWordBreaks: string[] = [' ']; private fontkit?: Fontkit; /** * When true, [[prepareForSave]] regenerates owned Info/`pdfaid` XMP. * Set by [[convertToPDFA]]. Part/level itself lives only in catalog XMP. */ private managePDFAMetadata = false; private pageCount: number | undefined; private readonly pageCache: Cache; private readonly pageMap: Map; private readonly formCache: Cache; private readonly fonts: PDFFont[]; private readonly images: PDFImage[]; private readonly embeddedPages: PDFEmbeddedPage[]; private readonly embeddedFiles: PDFEmbeddedFile[]; private readonly javaScripts: PDFJavaScript[]; private readonly preserveXFA: boolean; private constructor( context: PDFContext, ignoreEncryption: boolean, updateMetadata: boolean, preserveXFA = false, ) { assertIs(context, 'context', [[PDFContext, 'PDFContext']]); assertIs(ignoreEncryption, 'ignoreEncryption', ['boolean']); assertIs(preserveXFA, 'preserveXFA', ['boolean']); this.context = context; this.catalog = context.lookup(context.trailerInfo.Root) as PDFCatalog; if (!!context.lookup(context.trailerInfo.Encrypt) && context.isDecrypted) { // context.delete(context.trailerInfo.Encrypt); delete context.trailerInfo.Encrypt; } this.isEncrypted = !!context.lookup(context.trailerInfo.Encrypt); this.pageCache = Cache.populatedBy(this.computePages); this.pageMap = new Map(); this.formCache = Cache.populatedBy(this.getOrCreateForm); this.fonts = []; this.images = []; this.embeddedPages = []; this.embeddedFiles = []; this.javaScripts = []; this.preserveXFA = preserveXFA; if (!ignoreEncryption && this.isEncrypted) throw new EncryptedPDFError(); if (updateMetadata) this.updateInfoDict(); } /** * Register a fontkit instance. This must be done before custom fonts can * be embedded. See [here](https://github.com/Hopding/pdf-lib/tree/master#fontkit-installation) * for instructions on how to install and register a fontkit instance. * * > You do **not** need to call this method to embed standard fonts. * * For example: * ```js * import { PDFDocument } from 'pdf-lib' * import fontkit from '@cantoo/fontkit' * * const pdfDoc = await PDFDocument.create() * pdfDoc.registerFontkit(fontkit) * ``` * * @param fontkit The fontkit instance to be registered. */ registerFontkit(fontkit: Fontkit): void { this.fontkit = fontkit; } /** * Get the [[PDFForm]] containing all interactive fields for this document. * For example: * ```js * const form = pdfDoc.getForm() * const fields = form.getFields() * fields.forEach(field => { * const type = field.constructor.name * const name = field.getName() * console.log(`${type}: ${name}`) * }) * ``` * * **XFA caveat:** if the document contains XFA form data and it was **not** * loaded with `preserveXFA: true`, calling this method strips the XFA data * (pdf-lib cannot render or edit XFA and removes it to keep the AcroForm * consistent). Because the XFA read/write helpers ([[getXFAJavaScripts]], * [[setXFAJavaScript]]) operate on that same data, call them **before** * `getForm()` β€” or load with `preserveXFA: true` * otherwise the XFA will already be gone. * * @returns The form for this document. */ getForm(): PDFForm { const form = this.formCache.access(); if (form.hasXFA() && !this.preserveXFA) { console.warn( 'Removing XFA form data as pdf-lib does not support reading or writing XFA. Set preserveXFA: true in load options to keep XFA data.', ); form.deleteXFA(); } return form; } /** * List this document's optional content groups (PDF "layers"), if any. * Visibility reflects the default configuration (`/OCProperties` `/D`). * Returns an empty array when the document has no `/OCProperties`. * * For example: * ```js * const layers = pdfDoc.getOptionalContentGroups() * layers.forEach((layer) => console.log(layer.name, layer.visible)) * ``` */ getOptionalContentGroups(): PDFOptionalContentGroup[] { return this.catalog.getOCProperties()?.getGroups() ?? []; } /** * Set the default visibility of an optional content group (layer) so PDF * readers open the file with that layer on or off. Matches by layer name * (all groups with that name) or by indirect `PDFRef`. * * For example: * ```js * pdfDoc.setOptionalContentGroupVisibility('Watermark', false) * pdfDoc.setOptionalContentGroupVisibility([ * { name: 'Notes', visible: false }, * { ref: layers[0].ref, visible: true }, * ]) * ``` * * This updates `/OCProperties` `/D` (`/ON`, `/OFF`, `/BaseState`) only. It * does not remove layer content from page streams. */ setOptionalContentGroupVisibility( nameOrRef: string | PDFRef, visible: boolean, ): void; setOptionalContentGroupVisibility( updates: OptionalContentVisibilityUpdate[], ): void; setOptionalContentGroupVisibility( nameOrRefOrUpdates: string | PDFRef | OptionalContentVisibilityUpdate[], visible?: boolean, ): void { const ocProperties = this.catalog.getOCProperties(); if (!ocProperties) { throw new Error('This document has no optional content properties'); } const updates: OptionalContentVisibilityUpdate[] = Array.isArray( nameOrRefOrUpdates, ) ? nameOrRefOrUpdates : [ nameOrRefOrUpdates instanceof PDFRef ? { ref: nameOrRefOrUpdates, visible: visible as boolean } : { name: nameOrRefOrUpdates, visible: visible as boolean }, ]; ocProperties.setVisibility(updates); } /** * Get this document's title metadata. The title appears in the * "Document Properties" section of most PDF readers. For example: * ```js * const title = pdfDoc.getTitle() * ``` * @returns A string containing the title of this document, if it has one. */ getTitle(): string | undefined { const title = this.getInfoDict().lookup(PDFName.Title); if (!title) return undefined; assertIsLiteralOrHexString(title); return title.decodeText(); } /** * Get this document's author metadata. The author appears in the * "Document Properties" section of most PDF readers. For example: * ```js * const author = pdfDoc.getAuthor() * ``` * @returns A string containing the author of this document, if it has one. */ getAuthor(): string | undefined { const author = this.getInfoDict().lookup(PDFName.Author); if (!author) return undefined; assertIsLiteralOrHexString(author); return author.decodeText(); } /** * Get this document's subject metadata. The subject appears in the * "Document Properties" section of most PDF readers. For example: * ```js * const subject = pdfDoc.getSubject() * ``` * @returns A string containing the subject of this document, if it has one. */ getSubject(): string | undefined { const subject = this.getInfoDict().lookup(PDFName.Subject); if (!subject) return undefined; assertIsLiteralOrHexString(subject); return subject.decodeText(); } /** * Get this document's keywords metadata. The keywords appear in the * "Document Properties" section of most PDF readers. For example: * ```js * const keywords = pdfDoc.getKeywords() * ``` * @returns A string containing the keywords of this document, if it has any. */ getKeywords(): string | undefined { const keywords = this.getInfoDict().lookup(PDFName.Keywords); if (!keywords) return undefined; assertIsLiteralOrHexString(keywords); return keywords.decodeText(); } /** * Get this document's creator metadata. The creator appears in the * "Document Properties" section of most PDF readers. For example: * ```js * const creator = pdfDoc.getCreator() * ``` * @returns A string containing the creator of this document, if it has one. */ getCreator(): string | undefined { const creator = this.getInfoDict().lookup(PDFName.Creator); if (!creator) return undefined; assertIsLiteralOrHexString(creator); return creator.decodeText(); } /** * Get this document's producer metadata. The producer appears in the * "Document Properties" section of most PDF readers. For example: * ```js * const producer = pdfDoc.getProducer() * ``` * @returns A string containing the producer of this document, if it has one. */ getProducer(): string | undefined { const producer = this.getInfoDict().lookup(PDFName.Producer); if (!producer) return undefined; assertIsLiteralOrHexString(producer); return producer.decodeText(); } /** * Get this document's language metadata. The language appears in the * "Document Properties" section of most PDF readers. For example: * ```js * const language = pdfDoc.getLanguage() * ``` * @returns A string containing the RFC 3066 _Language-Tag_ of this document, * if it has one. */ getLanguage(): string | undefined { const language = this.catalog.get(PDFName.of('Lang')); if (!language) return undefined; assertIsLiteralOrHexString(language); return language.decodeText(); } /** * Get this document's creation date metadata. The creation date appears in * the "Document Properties" section of most PDF readers. For example: * ```js * const creationDate = pdfDoc.getCreationDate() * ``` * @returns A Date containing the creation date of this document, * if it has one. */ getCreationDate(): Date | undefined { const creationDate = this.getInfoDict().lookup(PDFName.CreationDate); if (!creationDate) return undefined; assertIsLiteralOrHexString(creationDate); return creationDate.decodeDate(); } /** * Get this document's modification date metadata. The modification date * appears in the "Document Properties" section of most PDF readers. * For example: * ```js * const modification = pdfDoc.getModificationDate() * ``` * @returns A Date containing the modification date of this document, * if it has one. */ getModificationDate(): Date | undefined { const modificationDate = this.getInfoDict().lookup(PDFName.ModDate); if (!modificationDate) return undefined; assertIsLiteralOrHexString(modificationDate); return modificationDate.decodeDate(); } /** * Set this document's title metadata. The title will appear in the * "Document Properties" section of most PDF readers. For example: * ```js * pdfDoc.setTitle('πŸ₯š The Life of an Egg 🍳') * ``` * * To display the title in the window's title bar, set the * `showInWindowTitleBar` option to `true` (works for _most_ PDF readers). * For example: * ```js * pdfDoc.setTitle('πŸ₯š The Life of an Egg 🍳', { showInWindowTitleBar: true }) * ``` * * @param title The title of this document. * @param options The options to be used when setting the title. */ setTitle(title: string, options?: SetTitleOptions): void { assertIs(title, 'title', ['string']); const key = PDFName.of('Title'); this.getInfoDict().set(key, PDFHexString.fromText(title)); // Indicate that readers should display the title rather than the filename if (options?.showInWindowTitleBar) { const prefs = this.catalog.getOrCreateViewerPreferences(); prefs.setDisplayDocTitle(true); } } /** * Set this document's author metadata. The author will appear in the * "Document Properties" section of most PDF readers. For example: * ```js * pdfDoc.setAuthor('Humpty Dumpty') * ``` * @param author The author of this document. */ setAuthor(author: string): void { assertIs(author, 'author', ['string']); const key = PDFName.of('Author'); this.getInfoDict().set(key, PDFHexString.fromText(author)); } /** * Set this document's subject metadata. The subject will appear in the * "Document Properties" section of most PDF readers. For example: * ```js * pdfDoc.setSubject('πŸ“˜ An Epic Tale of Woe πŸ“–') * ``` * @param subject The subject of this document. */ setSubject(subject: string): void { assertIs(subject, 'author', ['string']); const key = PDFName.of('Subject'); this.getInfoDict().set(key, PDFHexString.fromText(subject)); } /** * Set this document's keyword metadata. These keywords will appear in the * "Document Properties" section of most PDF readers. For example: * ```js * pdfDoc.setKeywords(['eggs', 'wall', 'fall', 'king', 'horses', 'men']) * ``` * @param keywords An array of keywords associated with this document. */ setKeywords(keywords: string[]): void { assertIs(keywords, 'keywords', [Array]); const key = PDFName.of('Keywords'); this.getInfoDict().set(key, PDFHexString.fromText(keywords.join(' '))); } /** * Set this document's creator metadata. The creator will appear in the * "Document Properties" section of most PDF readers. For example: * ```js * pdfDoc.setCreator('PDF App 9000 πŸ€–') * ``` * @param creator The creator of this document. */ setCreator(creator: string): void { assertIs(creator, 'creator', ['string']); const key = PDFName.of('Creator'); this.getInfoDict().set(key, PDFHexString.fromText(creator)); } /** * Set this document's producer metadata. The producer will appear in the * "Document Properties" section of most PDF readers. For example: * ```js * pdfDoc.setProducer('PDF App 9000 πŸ€–') * ``` * @param producer The producer of this document. */ setProducer(producer: string): void { assertIs(producer, 'creator', ['string']); const key = PDFName.of('Producer'); this.getInfoDict().set(key, PDFHexString.fromText(producer)); } /** * Set this document's language metadata. The language will appear in the * "Document Properties" section of some PDF readers. For example: * ```js * pdfDoc.setLanguage('en-us') * ``` * * @param language An RFC 3066 _Language-Tag_ denoting the language of this * document, or an empty string if the language is unknown. */ setLanguage(language: string): void { assertIs(language, 'language', ['string']); const key = PDFName.of('Lang'); this.catalog.set(key, PDFString.of(language)); } /** * Set this document's creation date metadata. The creation date will appear * in the "Document Properties" section of most PDF readers. For example: * ```js * pdfDoc.setCreationDate(new Date()) * ``` * @param creationDate The date this document was created. */ setCreationDate(creationDate: Date): void { assertIs(creationDate, 'creationDate', [[Date, 'Date']]); const key = PDFName.of('CreationDate'); this.getInfoDict().set(key, PDFString.fromDate(creationDate)); } /** * Set this document's modification date metadata. The modification date will * appear in the "Document Properties" section of most PDF readers. For * example: * ```js * pdfDoc.setModificationDate(new Date()) * ``` * @param modificationDate The date this document was last modified. */ setModificationDate(modificationDate: Date): void { assertIs(modificationDate, 'modificationDate', [[Date, 'Date']]); const key = PDFName.of('ModDate'); this.getInfoDict().set(key, PDFString.fromDate(modificationDate)); } /** * Convert this document into a PDF/A compliant document. PDF/A is an * ISO-standardized subset of PDF designed for the long-term archiving of * electronic documents. This method performs the structural changes that a * PDF/A file requires: * * * A unique document identifier (`/ID`) is added to the trailer. * * An `OutputIntent` referencing an embedded ICC color profile is added (the * bundled sRGB profile is used by default). * * An XMP metadata packet identifying the PDF/A conformance level is added * and kept consistent with the document information dictionary. * * The PDF header version is set appropriately for the targeted part. * * For example: * ```js * const pdfDoc = await PDFDocument.load(existingPdfBytes) * pdfDoc.convertToPDFA({ conformance: '3B' }) * const pdfBytes = await pdfDoc.save() * ``` * * > **This method does not, and cannot, guarantee full PDF/A compliance on * > its own.** PDF/A also forbids certain content (encryption, non-embedded * > fonts, transparency for part 1, JavaScript, external references, etc.). * > In particular, any text you draw must use an **embedded** font β€” the * > 14 standard fonts are not embedded and are therefore not PDF/A compliant. * > You are responsible for ensuring the document's content conforms. Validate * > the result with a tool such as [veraPDF](https://verapdf.org/). * * > **Unicode conformance (`'2U'` / `'3U'`) is not verified.** The `U` levels * > additionally require every glyph in the document to have a Unicode * > mapping (a `ToUnicode` CMap or equivalent). This method writes the * > requested conformance level into the metadata but does **not** inspect * > existing content to confirm the mappings are present β€” ensuring that is * > the caller's responsibility. * * > **XMP is refreshed on save.** After conversion, pdf-lib *manages* the * > catalog `/Metadata` stream. On [[save]] / [[saveIncremental]] / * > [[saveAsBase64]] it rebuilds the owned slice (Info-dict mirrors + * > `pdfaid`) so Info and XMP stay equivalent as required by PDF/A, while * > preserving foreign `rdf:Description` blocks (e.g. Factur-X / custom * > schemas). Pass one-shot extras via `options.extensions`; they are written * > into the initial packet and then preserved like any other foreign block. * * > **Ownership contract.** After this method runs, pdf-lib owns the `dc`, * > `xmp`, `pdf`, and `pdfaid` schemas. Add extra XMP with * > `options.extensions` or by merging foreign `rdf:Description` elements * > into the packet β€” those are preserved on sync. Hand-editing owned fields * > in the XMP (e.g. `dc:title`) without going through the Info setters will * > be overwritten. * * @param options The options to be used when converting the document. */ convertToPDFA(options: ConvertToPDFAOptions = {}): void { assertOrUndefined(options.conformance, 'options.conformance', ['string']); assertOrUndefined(options.iccProfile, 'options.iccProfile', [Uint8Array]); assertOrUndefined( options.outputConditionIdentifier, 'options.outputConditionIdentifier', ['string'], ); assertIsOneOfOrUndefined( options.colorComponents, 'options.colorComponents', [1, 3, 4], ); assertOrUndefined(options.extensions, 'options.extensions', [Array]); const { conformance = '3B', iccProfile = getDefaultSRGBProfile(), outputConditionIdentifier = 'sRGB IEC61966-2.1', colorComponents = 3, extensions, } = options; const parsed = parseConformance(conformance); if (this.isEncrypted) { throw new Error('PDF/A documents must not be encrypted.'); } const alreadyConverted = readCatalogPDFAConformance(this.catalog) !== undefined; // PDF/A-1 is based on PDF 1.4; parts 2 and 3 are based on PDF 1.7. this.context.header = PDFHeader.forVersion(1, parsed.part === 1 ? 4 : 7); // A file identifier (`/ID`) is required by PDF/A. Reuse an existing one if // present so incremental updates and encryption stay consistent with the // previously assigned identity. if (!this.context.lookup(this.context.trailerInfo.ID)) { const id = PDFHexString.fromBytes(generateRandomFileId()); this.context.trailerInfo.ID = this.context.obj([id, id]); } // Only (re)install the OutputIntent on first conversion, or when the caller // supplies a custom ICC profile / condition β€” avoids orphaning ICC streams // on repeated convertToPDFA / embedFacturX calls. const shouldUpdateOutputIntent = !alreadyConverted || options.iccProfile !== undefined || options.outputConditionIdentifier !== undefined || options.colorComponents !== undefined; if (shouldUpdateOutputIntent) { // PDF/A permits FlateDecode on ICC streams; the XMP packet stays // unfiltered in writeCatalogMetadataXml. const iccStream = this.context.flateStream(iccProfile, { N: colorComponents, }); const outputIntent = this.context.obj({ Type: 'OutputIntent', S: 'GTS_PDFA1', OutputConditionIdentifier: PDFString.of(outputConditionIdentifier), DestOutputProfile: this.context.register(iccStream), }); this.catalog.set( PDFName.of('OutputIntents'), this.context.obj([this.context.register(outputIntent)]), ); } // Opt into Info↔XMP sync on later saves; write `pdfaid` into catalog XMP // (the single source of truth for part/level). this.managePDFAMetadata = true; this.syncPDFAMetadata(extensions, parsed); } /** * Get the number of pages contained in this document. For example: * ```js * const totalPages = pdfDoc.getPageCount() * ``` * @returns The number of pages in this document. */ getPageCount(): number { if (this.pageCount === undefined) this.pageCount = this.getPages().length; return this.pageCount; } /** * Get an array of all the pages contained in this document. The pages are * stored in the array in the same order that they are rendered in the * document. For example: * ```js * const pages = pdfDoc.getPages() * pages[0] // The first page of the document * pages[2] // The third page of the document * pages[197] // The 198th page of the document * ``` * @returns An array of all the pages contained in this document. */ getPages(): PDFPage[] { return this.pageCache.access(); } /** * Get the page rendered at a particular `index` of the document. For example: * ```js * pdfDoc.getPage(0) // The first page of the document * pdfDoc.getPage(2) // The third page of the document * pdfDoc.getPage(197) // The 198th page of the document * ``` * @returns The [[PDFPage]] rendered at the given `index` of the document. */ getPage(index: number): PDFPage { const pages = this.getPages(); assertRange(index, 'index', 0, pages.length - 1); return pages[index]; } /** * Get an array of indices for all the pages contained in this document. The * array will contain a range of integers from * `0..pdfDoc.getPageCount() - 1`. For example: * ```js * const pdfDoc = await PDFDocument.create() * pdfDoc.addPage() * pdfDoc.addPage() * pdfDoc.addPage() * * const indices = pdfDoc.getPageIndices() * indices // => [0, 1, 2] * ``` * @returns An array of indices for all pages contained in this document. */ getPageIndices(): number[] { return range(0, this.getPageCount()); } /** * Remove the page at a given index from this document. For example: * ```js * pdfDoc.removePage(0) // Remove the first page of the document * pdfDoc.removePage(2) // Remove the third page of the document * pdfDoc.removePage(197) // Remove the 198th page of the document * ``` * Once a page has been removed, it will no longer be rendered at that index * in the document. * @param index The index of the page to be removed. */ removePage(index: number): void { const pageCount = this.getPageCount(); if (this.pageCount === 0) throw new RemovePageFromEmptyDocumentError(); assertRange(index, 'index', 0, pageCount - 1); const page = this.getPage(index); this.catalog.removeLeafNode(index); this.pageCount = pageCount - 1; this.context.delete(page.ref); this.pageCache.invalidate(); } /** * Add a page to the end of this document. This method accepts three * different value types for the `page` parameter: * * | Type | Behavior | * | ------------------ | ----------------------------------------------------------------------------------- | * | `undefined` | Create a new page and add it to the end of this document | * | `[number, number]` | Create a new page with the given dimensions and add it to the end of this document | * | `PDFPage` | Add the existing page to the end of this document | * * For example: * ```js * // page=undefined * const newPage = pdfDoc.addPage() * * // page=[number, number] * import { PageSizes } from 'pdf-lib' * const newPage1 = pdfDoc.addPage(PageSizes.A7) * const newPage2 = pdfDoc.addPage(PageSizes.Letter) * const newPage3 = pdfDoc.addPage([500, 750]) * * // page=PDFPage * const pdfDoc1 = await PDFDocument.create() * const pdfDoc2 = await PDFDocument.load(...) * const [existingPage] = await pdfDoc1.copyPages(pdfDoc2, [0]) * pdfDoc1.addPage(existingPage) * ``` * * @param page Optionally, the desired dimensions or existing page. * @returns The newly created (or existing) page. */ addPage(page?: PDFPage | [number, number]): PDFPage { assertIs(page, 'page', ['undefined', [PDFPage, 'PDFPage'], Array]); return this.insertPage(this.getPageCount(), page); } /** * Insert a page at a given index within this document. This method accepts * three different value types for the `page` parameter: * * | Type | Behavior | * | ------------------ | ------------------------------------------------------------------------------ | * | `undefined` | Create a new page and insert it into this document | * | `[number, number]` | Create a new page with the given dimensions and insert it into this document | * | `PDFPage` | Insert the existing page into this document | * * For example: * ```js * // page=undefined * const newPage = pdfDoc.insertPage(2) * * // page=[number, number] * import { PageSizes } from 'pdf-lib' * const newPage1 = pdfDoc.insertPage(2, PageSizes.A7) * const newPage2 = pdfDoc.insertPage(0, PageSizes.Letter) * const newPage3 = pdfDoc.insertPage(198, [500, 750]) * * // page=PDFPage * const pdfDoc1 = await PDFDocument.create() * const pdfDoc2 = await PDFDocument.load(...) * const [existingPage] = await pdfDoc1.copyPages(pdfDoc2, [0]) * pdfDoc1.insertPage(0, existingPage) * ``` * * @param index The index at which the page should be inserted (zero-based). * @param page Optionally, the desired dimensions or existing page. * @returns The newly created (or existing) page. */ insertPage(index: number, page?: PDFPage | [number, number]): PDFPage { const pageCount = this.getPageCount(); assertRange(index, 'index', 0, pageCount); assertIs(page, 'page', ['undefined', [PDFPage, 'PDFPage'], Array]); if (!page || Array.isArray(page)) { const dims = Array.isArray(page) ? page : PageSizes.A4; page = PDFPage.create(this); page.setSize(...dims); } else if (page.doc !== this) { throw new ForeignPageError(); } const parentRef = this.catalog.insertLeafNode(page.ref, index); page.node.setParent(parentRef); this.pageMap.set(page.node, page); this.pageCache.invalidate(); this.pageCount = pageCount + 1; return page; } /** * Copy pages from a source document into this document. Allows pages to be * copied between different [[PDFDocument]] instances. For example: * ```js * const pdfDoc = await PDFDocument.create() * const srcDoc = await PDFDocument.load(...) * * const copiedPages = await pdfDoc.copyPages(srcDoc, [0, 3, 89]) * const [firstPage, fourthPage, ninetiethPage] = copiedPages; * * pdfDoc.addPage(fourthPage) * pdfDoc.insertPage(0, ninetiethPage) * pdfDoc.addPage(firstPage) * ``` * @param srcDoc The document from which pages should be copied. * @param indices The indices of the pages that should be copied. * @returns Resolves with an array of pages copied into this document. */ async copyPages(srcDoc: PDFDocument, indices: number[]): Promise { assertIs(srcDoc, 'srcDoc', [[PDFDocument, 'PDFDocument']]); assertIs(indices, 'indices', [Array]); await srcDoc.flush(); const copier = PDFObjectCopier.for(srcDoc.context, this.context); const srcPages = srcDoc.getPages(); // Copy each page in a separate thread const copiedPages = indices .map((i) => srcPages[i]) .map(async (page) => copier.copy(page.node)) .map((p) => p.then((copy) => PDFPage.of(copy, this.context.register(copy), this)), ); return Promise.all(copiedPages); } /** * Get a copy of this document. * * For example: * ```js * const srcDoc = await PDFDocument.load(...) * const pdfDoc = await srcDoc.copy() * ``` * * > **NOTE:** This method won't copy all information over to the new * > document (acroforms, outlines, etc...). * * @returns Resolves with a copy this document. */ async copy(): Promise { const pdfCopy = await PDFDocument.create(); const contentPages = await pdfCopy.copyPages(this, this.getPageIndices()); for (let idx = 0, len = contentPages.length; idx < len; idx++) { pdfCopy.addPage(contentPages[idx]); } if (this.getAuthor() !== undefined) { pdfCopy.setAuthor(this.getAuthor()!); } if (this.getCreationDate() !== undefined) { pdfCopy.setCreationDate(this.getCreationDate()!); } if (this.getCreator() !== undefined) { pdfCopy.setCreator(this.getCreator()!); } if (this.getLanguage() !== undefined) { pdfCopy.setLanguage(this.getLanguage()!); } if (this.getModificationDate() !== undefined) { pdfCopy.setModificationDate(this.getModificationDate()!); } if (this.getProducer() !== undefined) { pdfCopy.setProducer(this.getProducer()!); } if (this.getSubject() !== undefined) { pdfCopy.setSubject(this.getSubject()!); } if (this.getTitle() !== undefined) { pdfCopy.setTitle(this.getTitle()!); } pdfCopy.defaultWordBreaks = this.defaultWordBreaks; return pdfCopy; } /** * Add JavaScript to this document. The supplied `script` is executed when the * document is opened. The `script` can be used to perform some operation * when the document is opened (e.g. logging to the console), or it can be * used to define a function that can be referenced later in a JavaScript * action. For example: * ```js * // Show "Hello World!" in the console when the PDF is opened * pdfDoc.addJavaScript( * 'main', * 'console.show(); console.println("Hello World!");' * ); * * // Define a function named "foo" that can be called in JavaScript Actions * pdfDoc.addJavaScript( * 'foo', * 'function foo() { return "foo"; }' * ); * ``` * See the [JavaScript for Acrobat API Reference](https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/js_api_reference.pdf) * for details. * @param name The name of the script. Must be unique per document. * @param script The JavaScript to execute. */ addJavaScript(name: string, script: string) { assertIs(name, 'name', ['string']); assertIs(script, 'script', ['string']); const embedder = JavaScriptEmbedder.for(script, name); const ref = this.context.nextRef(); const javaScript = PDFJavaScript.of(ref, this, embedder); this.javaScripts.push(javaScript); } /** * Get all document-level JavaScript scripts from the document's Names dictionary. * These scripts are executed when the document is opened. * For example: * ```js * const scripts = pdfDoc.getDocumentJavaScripts() * scripts.forEach(({ name, script }) => { * console.log(`Script "${name}":`, script) * }) * ``` * @returns An array of objects containing script names and their JavaScript code. */ getDocumentJavaScripts(): Array<{ name: string; script: string }> { const scripts: Array<{ name: string; script: string }> = []; const namesDict = this.catalog.lookupMaybe(PDFName.of('Names'), PDFDict); const javascriptDict = namesDict?.lookupMaybe( PDFName.of('JavaScript'), PDFDict, ); const jsNames = javascriptDict?.lookupMaybe(PDFName.of('Names'), PDFArray); if (!jsNames) return scripts; // Names array is a flat array of [name1, dict1, name2, dict2, ...] for (let idx = 0; idx < jsNames.size(); idx += 2) { const nameObj = jsNames.get(idx); const actionObj = jsNames.get(idx + 1); if (!nameObj || !actionObj) continue; let name: string; if (nameObj instanceof PDFString) { name = nameObj.asString(); } else if (nameObj instanceof PDFHexString) { name = nameObj.decodeText(); } else { continue; } const actionDict = actionObj instanceof PDFRef ? this.context.lookupMaybe(actionObj, PDFDict) : actionObj instanceof PDFDict ? actionObj : undefined; if (!actionDict) continue; const script = PDFJavaScriptAction.of( actionDict, this, actionObj instanceof PDFRef ? actionObj : undefined, )?.getScript(); if (!script) continue; scripts.push({ name, script }); } return scripts; } /** * Get all JavaScript from XFA form template. * XFA forms can contain JavaScript in