import Embeddable from 'src/api/Embeddable' ; import { EncryptedPDFError , FontkitNotRegisteredError , ForeignPageError , RemovePageFromEmptyDocumentError , } from 'src/api/errors' ; import PDFEmbeddedPage from 'src/api/PDFEmbeddedPage' ; import PDFFont from 'src/api/PDFFont' ; import PDFImage from 'src/api/PDFImage' ; import PDFPage from 'src/api/PDFPage' ; import PDFForm from 'src/api/form/PDFForm' ; import { PageSizes } from 'src/api/sizes' ; import { StandardFonts } from 'src/api/StandardFonts' ; import { CustomFontEmbedder , CustomFontSubsetEmbedder , JpegEmbedder , PageBoundingBox , PageEmbeddingMismatchedContextError , PDFCatalog , PDFContext , PDFDict , PDFHexString , PDFName , PDFObjectCopier , PDFPageEmbedder , PDFPageLeaf , PDFPageTree , PDFParser , PDFStreamWriter , PDFString , PDFWriter , PngEmbedder , StandardFontEmbedder , UnexpectedObjectTypeError , } from 'src/core' ; import { ParseSpeeds , AttachmentOptions , SaveOptions , Base64SaveOptions , LoadOptions , CreateOptions , EmbedFontOptions , SetTitleOptions , } from 'src/api/PDFDocumentOptions' ; import PDFObject from 'src/core/objects/PDFObject' ; import PDFRef from 'src/core/objects/PDFRef' ; import { Fontkit } from 'src/types/fontkit' ; import { TransformationMatrix } from 'src/types/matrix' ; import { assertIs , assertIsOneOfOrUndefined , assertOrUndefined , assertRange , Cache , canBeConvertedToUint8Array , encodeToBase64 , isStandardFont , pluckIndices , range , toUint8Array , } from 'src/utils' ; import FileEmbedder , { AFRelationship } from 'src/core/embedders/FileEmbedder' ; import PDFEmbeddedFile from 'src/api/PDFEmbeddedFile' ; import PDFJavaScript from 'src/api/PDFJavaScript' ; import JavaScriptEmbedder from 'src/core/embedders/JavaScriptEmbedder' ; /**
 * Represents a PDF document.
 */ export default class PDFDocument { param pdf param options returns static async load ( pdf , options = { } ) { const { ignoreEncryption = false , parseSpeed = ParseSpeeds . Slow , throwOnInvalidObject = false , updateMetadata = true , capNumbers = false , } = options ; assertIs ( pdf , 'pdf' , [ 'string' , Uint8Array , ArrayBuffer ] ) ; assertIs ( ignoreEncryption , 'ignoreEncryption' , [ 'boolean' ] ) ; assertIs ( parseSpeed , 'parseSpeed' , [ 'number' ] ) ; assertIs ( throwOnInvalidObject , 'throwOnInvalidObject' , [ 'boolean' ] ) ; const bytes = toUint8Array ( pdf ) ; const context = await PDFParser . forBytesWithOptions ( bytes , parseSpeed , throwOnInvalidObject , capNumbers ) . parseDocument ( ) ; return new PDFDocument ( context , ignoreEncryption , updateMetadata ) ; } returns static async create ( options = { } ) { 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 ; private pageCount : number | undefined ; private readonly pageCache : Cache < PDFPage [ ] > ; private readonly pageMap : Map < PDFPageLeaf , PDFPage > ; private readonly formCache : Cache < PDFForm > ; private readonly fonts : PDFFont [ ] ; private readonly images : PDFImage [ ] ; private readonly embeddedPages : PDFEmbeddedPage [ ] ; private readonly embeddedFiles : PDFEmbeddedFile [ ] ; private readonly javaScripts : PDFJavaScript [ ] ; private constructor ( context , ignoreEncryption , updateMetadata ) { assertIs ( context , 'context' , [ [ PDFContext , 'PDFContext' ] ] ) ; assertIs ( ignoreEncryption , 'ignoreEncryption' , [ 'boolean' ] ) ; this . context = context ; this . catalog = context . lookup ( context . trailerInfo . Root ) as PDFCatalog ; 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 = [ ] ; if ( ! ignoreEncryption && this . isEncrypted ) throw new EncryptedPDFError ( ) ; if ( updateMetadata ) this . updateInfoDict ( ) ; } param fontkit registerFontkit ( fontkit ) { this . fontkit = fontkit ; } returns getForm ( ) { const form = this . formCache . access ( ) ; if ( form . hasXFA ( ) ) { console . warn ( 'Removing XFA form data as pdf-lib does not support reading or writing XFA' ) ; form . deleteXFA ( ) ; } return form ; } returns getTitle ( ) { const title = this . getInfoDict ( ) . lookup ( PDFName . Title ) ; if ( ! title ) return undefined ; assertIsLiteralOrHexString ( title ) ; return title . decodeText ( ) ; } returns getAuthor ( ) { const author = this . getInfoDict ( ) . lookup ( PDFName . Author ) ; if ( ! author ) return undefined ; assertIsLiteralOrHexString ( author ) ; return author . decodeText ( ) ; } returns getSubject ( ) { const subject = this . getInfoDict ( ) . lookup ( PDFName . Subject ) ; if ( ! subject ) return undefined ; assertIsLiteralOrHexString ( subject ) ; return subject . decodeText ( ) ; } returns getKeywords ( ) { const keywords = this . getInfoDict ( ) . lookup ( PDFName . Keywords ) ; if ( ! keywords ) return undefined ; assertIsLiteralOrHexString ( keywords ) ; return keywords . decodeText ( ) ; } returns getCreator ( ) { const creator = this . getInfoDict ( ) . lookup ( PDFName . Creator ) ; if ( ! creator ) return undefined ; assertIsLiteralOrHexString ( creator ) ; return creator . decodeText ( ) ; } returns getProducer ( ) { const producer = this . getInfoDict ( ) . lookup ( PDFName . Producer ) ; if ( ! producer ) return undefined ; assertIsLiteralOrHexString ( producer ) ; return producer . decodeText ( ) ; } returns getCreationDate ( ) { const creationDate = this . getInfoDict ( ) . lookup ( PDFName . CreationDate ) ; if ( ! creationDate ) return undefined ; assertIsLiteralOrHexString ( creationDate ) ; return creationDate . decodeDate ( ) ; } returns getModificationDate ( ) { const modificationDate = this . getInfoDict ( ) . lookup ( PDFName . ModDate ) ; if ( ! modificationDate ) return undefined ; assertIsLiteralOrHexString ( modificationDate ) ; return modificationDate . decodeDate ( ) ; } param title param options setTitle ( title , options ? ) { assertIs ( title , 'title' , [ 'string' ] ) ; const key = PDFName . of ( 'Title' ) ; this . getInfoDict ( ) . set ( key , PDFHexString . fromText ( title ) ) ; if ( options ?. showInWindowTitleBar ) { const prefs = this . catalog . getOrCreateViewerPreferences ( ) ; prefs . setDisplayDocTitle ( true ) ; } } param author setAuthor ( author ) { assertIs ( author , 'author' , [ 'string' ] ) ; const key = PDFName . of ( 'Author' ) ; this . getInfoDict ( ) . set ( key , PDFHexString . fromText ( author ) ) ; } param subject setSubject ( subject ) { assertIs ( subject , 'author' , [ 'string' ] ) ; const key = PDFName . of ( 'Subject' ) ; this . getInfoDict ( ) . set ( key , PDFHexString . fromText ( subject ) ) ; } param keywords setKeywords ( keywords ) { assertIs ( keywords , 'keywords' , [ Array ] ) ; const key = PDFName . of ( 'Keywords' ) ; this . getInfoDict ( ) . set ( key , PDFHexString . fromText ( keywords . join ( ' ' ) ) ) ; } param creator setCreator ( creator ) { assertIs ( creator , 'creator' , [ 'string' ] ) ; const key = PDFName . of ( 'Creator' ) ; this . getInfoDict ( ) . set ( key , PDFHexString . fromText ( creator ) ) ; } param producer setProducer ( producer ) { assertIs ( producer , 'creator' , [ 'string' ] ) ; const key = PDFName . of ( 'Producer' ) ; this . getInfoDict ( ) . set ( key , PDFHexString . fromText ( producer ) ) ; } param language setLanguage ( language ) { assertIs ( language , 'language' , [ 'string' ] ) ; const key = PDFName . of ( 'Lang' ) ; this . catalog . set ( key , PDFString . of ( language ) ) ; } param creationDate setCreationDate ( creationDate ) { assertIs ( creationDate , 'creationDate' , [ [ Date , 'Date' ] ] ) ; const key = PDFName . of ( 'CreationDate' ) ; this . getInfoDict ( ) . set ( key , PDFString . fromDate ( creationDate ) ) ; } param modificationDate setModificationDate ( modificationDate ) { assertIs ( modificationDate , 'modificationDate' , [ [ Date , 'Date' ] ] ) ; const key = PDFName . of ( 'ModDate' ) ; this . getInfoDict ( ) . set ( key , PDFString . fromDate ( modificationDate ) ) ; } returns getPageCount ( ) { if ( this . pageCount === undefined ) this . pageCount = this . getPages ( ) . length ; return this . pageCount ; } returns getPages ( ) { return this . pageCache . access ( ) ; } returns getPage ( index ) { const pages = this . getPages ( ) ; assertRange ( index , 'index' , 0 , pages . length - 1 ) ; return pages [ index ] ; } returns getPageIndices ( ) { return range ( 0 , this . getPageCount ( ) ) ; } param index removePage ( index ) { const pageCount = this . getPageCount ( ) ; if ( this . pageCount === 0 ) throw new RemovePageFromEmptyDocumentError ( ) ; assertRange ( index , 'index' , 0 , pageCount - 1 ) ; this . catalog . removeLeafNode ( index ) ; this . pageCount = pageCount - 1 ; } param page returns addPage ( page ? ) { assertIs ( page , 'page' , [ 'undefined' , [ PDFPage , 'PDFPage' ] , Array ] ) ; return this . insertPage ( this . getPageCount ( ) , page ) ; } param index param page returns insertPage ( index , page ? ) { 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 ; } param srcDoc param indices returns async copyPages ( srcDoc , indices ) { assertIs ( srcDoc , 'srcDoc' , [ [ PDFDocument , 'PDFDocument' ] ] ) ; assertIs ( indices , 'indices' , [ Array ] ) ; await srcDoc . flush ( ) ; const copier = PDFObjectCopier . for ( srcDoc . context , this . context ) ; const srcPages = srcDoc . getPages ( ) ; const copiedPages = new Array ( indices . length ) ; for ( let idx = 0 , len = indices . length ; idx < len ; idx ++ ) { const srcPage = srcPages [ indices [ idx ] ] ; const copiedPage = copier . copy ( srcPage . node ) ; const ref = this . context . register ( copiedPage ) ; copiedPages [ idx ] = PDFPage . of ( copiedPage , ref , this ) ; } return copiedPages ; } returns async copy ( ) { 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 . 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 ; } param name param script addJavaScript ( name , script ) { 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 ) ; } param attachment param name returns async attach ( attachment , name , options = { } ) { assertIs ( attachment , 'attachment' , [ 'string' , Uint8Array , ArrayBuffer ] ) ; assertIs ( name , 'name' , [ 'string' ] ) ; assertOrUndefined ( options . mimeType , 'mimeType' , [ 'string' ] ) ; assertOrUndefined ( options . description , 'description' , [ 'string' ] ) ; assertOrUndefined ( options . creationDate , 'options.creationDate' , [ Date ] ) ; assertOrUndefined ( options . modificationDate , 'options.modificationDate' , [ Date , ] ) ; assertIsOneOfOrUndefined ( options . afRelationship , 'options.afRelationship' , AFRelationship ) ; const bytes = toUint8Array ( attachment ) ; const embedder = FileEmbedder . for ( bytes , name , options ) ; const ref = this . context . nextRef ( ) ; const embeddedFile = PDFEmbeddedFile . of ( ref , this , embedder ) ; this . embeddedFiles . push ( embeddedFile ) ; } param font param options returns async embedFont ( font , options = { } ) { const { subset = false , customName , features } = options ; assertIs ( font , 'font' , [ 'string' , Uint8Array , ArrayBuffer ] ) ; assertIs ( subset , 'subset' , [ 'boolean' ] ) ; let embedder ; if ( isStandardFont ( font ) ) { embedder = StandardFontEmbedder . for ( font , customName ) ; } else if ( canBeConvertedToUint8Array ( font ) ) { const bytes = toUint8Array ( font ) ; const fontkit = this . assertFontkit ( ) ; embedder = subset ? await CustomFontSubsetEmbedder . for ( fontkit , bytes , customName , features ) : await CustomFontEmbedder . for ( fontkit , bytes , customName , features ) ; } else { throw new TypeError ( '`font` must be one of `StandardFonts | string | Uint8Array | ArrayBuffer`' ) ; } const ref = this . context . nextRef ( ) ; const pdfFont = PDFFont . of ( ref , this , embedder ) ; this . fonts . push ( pdfFont ) ; return pdfFont ; } param font param customName returns embedStandardFont ( font , customName ? ) { assertIs ( font , 'font' , [ 'string' ] ) ; if ( ! isStandardFont ( font ) ) { throw new TypeError ( '`font` must be one of type `StandardFonts`' ) ; } const embedder = StandardFontEmbedder . for ( font , customName ) ; const ref = this . context . nextRef ( ) ; const pdfFont = PDFFont . of ( ref , this , embedder ) ; this . fonts . push ( pdfFont ) ; return pdfFont ; } param jpg returns async embedJpg ( jpg ) { assertIs ( jpg , 'jpg' , [ 'string' , Uint8Array , ArrayBuffer ] ) ; const bytes = toUint8Array ( jpg ) ; const embedder = await JpegEmbedder . for ( bytes ) ; const ref = this . context . nextRef ( ) ; const pdfImage = PDFImage . of ( ref , this , embedder ) ; this . images . push ( pdfImage ) ; return pdfImage ; } param png returns async embedPng ( png ) { assertIs ( png , 'png' , [ 'string' , Uint8Array , ArrayBuffer ] ) ; const bytes = toUint8Array ( png ) ; const embedder = await PngEmbedder . for ( bytes ) ; const ref = this . context . nextRef ( ) ; const pdfImage = PDFImage . of ( ref , this , embedder ) ; this . images . push ( pdfImage ) ; return pdfImage ; } param pdf param indices returns async embedPdf ( pdf , indices = [ 0 ] ) { assertIs ( pdf , 'pdf' , [ 'string' , Uint8Array , ArrayBuffer , [ PDFDocument , 'PDFDocument' ] , ] ) ; assertIs ( indices , 'indices' , [ Array ] ) ; const srcDoc = pdf instanceof PDFDocument ? pdf : await PDFDocument . load ( pdf ) ; const srcPages = pluckIndices ( srcDoc . getPages ( ) , indices ) ; return this . embedPages ( srcPages ) ; } param page param boundingBox param transformationMatrix returns async embedPage ( page , boundingBox ? , transformationMatrix ? ) { assertIs ( page , 'page' , [ [ PDFPage , 'PDFPage' ] ] ) ; const [ embeddedPage ] = await this . embedPages ( [ page ] , [ boundingBox ] , [ transformationMatrix ] ) ; return embeddedPage ; } param page param boundingBoxes param transformationMatrices returns async embedPages ( pages , boundingBoxes = [ ] , transformationMatrices = [ ] ) { if ( pages . length === 0 ) return [ ] ; for ( let idx = 0 , len = pages . length - 1 ; idx < len ; idx ++ ) { const currPage = pages [ idx ] ; const nextPage = pages [ idx + 1 ] ; if ( currPage . node . context !== nextPage . node . context ) { throw new PageEmbeddingMismatchedContextError ( ) ; } } const context = pages [ 0 ] . node . context ; const maybeCopyPage = context === this . context ? ( p ) => p : PDFObjectCopier . for ( context , this . context ) . copy ; const embeddedPages = new Array < PDFEmbeddedPage > ( pages . length ) ; for ( let idx = 0 , len = pages . length ; idx < len ; idx ++ ) { const page = maybeCopyPage ( pages [ idx ] . node ) ; const box = boundingBoxes [ idx ] ; const matrix = transformationMatrices [ idx ] ; const embedder = await PDFPageEmbedder . for ( page , box , matrix ) ; const ref = this . context . nextRef ( ) ; embeddedPages [ idx ] = PDFEmbeddedPage . of ( ref , this , embedder ) ; } this . embeddedPages . push ( ... embeddedPages ) ; return embeddedPages ; } returns async flush ( ) { await this . embedAll ( this . fonts ) ; await this . embedAll ( this . images ) ; await this . embedAll ( this . embeddedPages ) ; await this . embedAll ( this . embeddedFiles ) ; await this . embedAll ( this . javaScripts ) ; } param options returns async save ( options = { } ) { const { useObjectStreams = true , addDefaultPage = true , objectsPerTick = 50 , updateFieldAppearances = true , } = options ; assertIs ( useObjectStreams , 'useObjectStreams' , [ 'boolean' ] ) ; assertIs ( addDefaultPage , 'addDefaultPage' , [ 'boolean' ] ) ; assertIs ( objectsPerTick , 'objectsPerTick' , [ 'number' ] ) ; assertIs ( updateFieldAppearances , 'updateFieldAppearances' , [ 'boolean' ] ) ; if ( addDefaultPage && this . getPageCount ( ) === 0 ) this . addPage ( ) ; if ( updateFieldAppearances ) { const form = this . formCache . getValue ( ) ; if ( form ) form . updateFieldAppearances ( ) ; } await this . flush ( ) ; const Writer = useObjectStreams ? PDFStreamWriter : PDFWriter ; return Writer . forContext ( this . context , objectsPerTick ) . serializeToBuffer ( ) ; } param options returns async saveAsBase64 ( options = { } ) { const { dataUri = false , ... otherOptions } = options ; assertIs ( dataUri , 'dataUri' , [ 'boolean' ] ) ; const bytes = await this . save ( otherOptions ) ; const base64 = encodeToBase64 ( bytes ) ; return dataUri ? `data:application/pdf;base64,${ base64 }` : base64 ; } findPageForAnnotationRef ( ref ) { const pages = this . getPages ( ) ; for ( let idx = 0 , len = pages . length ; idx < len ; idx ++ ) { const page = pages [ idx ] ; const annotations = page . node . Annots ( ) ; if ( annotations ?. indexOf ( ref ) !== undefined ) { return page ; } } return undefined ; } private async embedAll ( embeddables ) { for ( let idx = 0 , len = embeddables . length ; idx < len ; idx ++ ) { await embeddables [ idx ] . embed ( ) ; } } private updateInfoDict ( ) { const pdfLib = `pdf-lib (https://github.com/Hopding/pdf-lib)` ; const now = new Date ( ) ; const info = this . getInfoDict ( ) ; this . setProducer ( pdfLib ) ; this . setModificationDate ( now ) ; if ( ! info . get ( PDFName . of ( 'Creator' ) ) ) this . setCreator ( pdfLib ) ; if ( ! info . get ( PDFName . of ( 'CreationDate' ) ) ) this . setCreationDate ( now ) ; } private getInfoDict ( ) { const existingInfo = this . context . lookup ( this . context . trailerInfo . Info ) ; if ( existingInfo instanceof PDFDict ) return existingInfo ; const newInfo = this . context . obj ( { } ) ; this . context . trailerInfo . Info = this . context . register ( newInfo ) ; return newInfo ; } private assertFontkit ( ) { if ( ! this . fontkit ) throw new FontkitNotRegisteredError ( ) ; return this . fontkit ; } private computePages = ( ) : PDFPage [ ] => { const pages = [ ] ; this . catalog . Pages ( ) . traverse ( ( node , ref ) => { if ( node instanceof PDFPageLeaf ) { let page = this . pageMap . get ( node ) ; if ( ! page ) { page = PDFPage . of ( node , ref , this ) ; this . pageMap . set ( node , page ) ; } pages . push ( page ) ; } } ) ; return pages ; } ; private getOrCreateForm = ( ) : PDFForm => { const acroForm = this . catalog . getOrCreateAcroForm ( ) ; return PDFForm . of ( acroForm , this ) ; } ; } function assertIsLiteralOrHexString ( pdfObject ) { if ( ! ( pdfObject instanceof PDFHexString ) && ! ( pdfObject instanceof PDFString ) ) { throw new UnexpectedObjectTypeError ( [ PDFHexString , PDFString ] , pdfObject ) ; } }