import fs from 'fs'; import CryptoJS from 'crypto-js'; export default { /** * Embed contents of `src` in PDF * @param {Buffer | ArrayBuffer | string} src input Buffer, ArrayBuffer, base64 encoded string or path to file * @param {object} options * * options.name: filename to be shown in PDF, will use `src` if none set * * options.type: filetype to be shown in PDF * * options.description: description to be shown in PDF * * options.hidden: if true, do not add attachment to EmbeddedFiles dictionary. Useful for file attachment annotations * * options.creationDate: override creation date * * options.modifiedDate: override modified date * * options.relationship: Relationship between the PDF document and its attached file. Can be 'Alternative', 'Data', 'Source', 'Supplement' or 'Unspecified'. * @returns filespec reference */ file(src, options = {}) { options.name = options.name || src; options.relationship = options.relationship || 'Unspecified'; const refBody = { Type: 'EmbeddedFile', Params: {}, }; let data; if (!src) { throw new Error('No src specified'); } if (Buffer.isBuffer(src)) { data = src; } else if (src instanceof ArrayBuffer) { data = Buffer.from(new Uint8Array(src)); } else { const match = /^data:(.*?);base64,(.*)$/.exec(src); if (match) { if (match[1]) { refBody.Subtype = match[1].replace('/', '#2F'); } data = Buffer.from(match[2], 'base64'); } else { data = fs.readFileSync(src); if (!data) { throw new Error(`Could not read contents of file at filepath ${src}`); } // update CreationDate and ModDate const { birthtime, ctime } = fs.statSync(src); refBody.Params.CreationDate = birthtime; refBody.Params.ModDate = ctime; } } // override creation date and modified date if (options.creationDate instanceof Date) { refBody.Params.CreationDate = options.creationDate; } if (options.modifiedDate instanceof Date) { refBody.Params.ModDate = options.modifiedDate; } // add optional subtype if (options.type) { refBody.Subtype = options.type.replace('/', '#2F'); } // add checksum and size information const checksum = CryptoJS.MD5( CryptoJS.lib.WordArray.create(new Uint8Array(data)), ); refBody.Params.CheckSum = new String(checksum); refBody.Params.Size = data.byteLength; // save some space when embedding the same file again // if a file with the same name and metadata exists, reuse its reference let ref; if (!this._fileRegistry) this._fileRegistry = {}; let file = this._fileRegistry[options.name]; if (file && isEqual(refBody, file)) { ref = file.ref; } else { ref = this.ref(refBody); ref.end(data); this._fileRegistry[options.name] = { ...refBody, ref }; } // add filespec for embedded file const fileSpecBody = { Type: 'Filespec', AFRelationship: options.relationship, F: new String(options.name), EF: { F: ref }, UF: new String(options.name), }; if (options.description) { fileSpecBody.Desc = new String(options.description); } const filespec = this.ref(fileSpecBody); filespec.end(); if (!options.hidden) { this.addNamedEmbeddedFile(options.name, filespec); } // Add file to the catalogue to be PDF/A3 compliant if (this._root.data.AF) { this._root.data.AF.push(filespec); } else { this._root.data.AF = [filespec]; } return filespec; }, }; /** check two embedded file metadata objects for equality */ function isEqual(a, b) { return ( a.Subtype === b.Subtype && a.Params.CheckSum.toString() === b.Params.CheckSum.toString() && a.Params.Size === b.Params.Size && a.Params.CreationDate.getTime() === b.Params.CreationDate.getTime() && ((a.Params.ModDate === undefined && b.Params.ModDate === undefined) || a.Params.ModDate.getTime() === b.Params.ModDate.getTime()) ); }