UNPKG

@cantoo/pdf-lib

Version:

Create and modify PDF files with JavaScript

209 lines 10.3 kB
import { __awaiter } from "tslib"; import PDFCrossRefSection from '../document/PDFCrossRefSection.js'; import PDFHeader from '../document/PDFHeader.js'; import PDFTrailer from '../document/PDFTrailer.js'; import PDFTrailerDict from '../document/PDFTrailerDict.js'; import PDFRef from '../objects/PDFRef.js'; import PDFStream from '../objects/PDFStream.js'; import PDFObjectStream from '../structures/PDFObjectStream.js'; import CharCodes from '../syntax/CharCodes.js'; import { copyStringIntoBuffer, waitForTick } from '../../utils/index.js'; import { DefaultDocumentSnapshot, defaultDocumentSnapshot, } from '../../api/snapshot/index.js'; import PDFNumber from '../objects/PDFNumber.js'; import PDFName from '../objects/PDFName.js'; import PDFRawStream from '../objects/PDFRawStream.js'; class PDFWriter { constructor(context, objectsPerTick, snapshot) { this.parsedObjects = 0; /** * If PDF has an XRef Stream, then the last object will be probably be skipped on saving. * If that's the case, this property will have that object number, and the PDF /Size can * be corrected, to be accurate. */ this._largestSkippedObjectNum = 0; /** * Used to check wheter an object should be saved or not, preserves the object number of the * last XRef Stream object, if there is one. */ this._lastXRefObjectNumber = 0; this.shouldWaitForTick = (n) => { this.parsedObjects += n; return this.parsedObjects % this.objectsPerTick === 0; }; this.context = context; this.objectsPerTick = objectsPerTick; this.snapshot = snapshot; } /** * For incremental saves, defers the decision to the snapshot. * For full saves, checks that the object is not the last XRef stream object. * @param {boolean} incremental If making an incremental save, or a full save of the PDF * @param {number} objNum Object number * @param {[PDFRef, PDFObject][]} objects List of objects that form the PDF * @returns {boolean} whether the object should be saved or not */ shouldSave(incremental, objNum, objects) { let should = true; if (incremental) { should = this.snapshot.shouldSave(objNum); } else { // only the last XRef Stream will be regenerated on save if (!this._lastXRefObjectNumber) { // if no XRef Stream, then nothing should be skipped this._lastXRefObjectNumber = this.context.largestObjectNumber + 1; const checkWatermark = this._lastXRefObjectNumber - 10; // max number of objects in the final part of the PDF to check // search the last XRef Stream, if there is one, objects are expected to be in object number order for (let idx = objects.length - 1; idx > 0; idx--) { // if not in last 'rangeToCheck' objects, there is none that should be skipped, most probably a linearized PDF, or without XRef Streams if (objects[idx][0].objectNumber < checkWatermark) break; const object = objects[idx][1]; if (object instanceof PDFRawStream && object.dict.lookup(PDFName.of('Type')) === PDFName.of('XRef')) { this._lastXRefObjectNumber = objects[idx][0].objectNumber; break; } } } should = objNum !== this._lastXRefObjectNumber; } if (!should && this._largestSkippedObjectNum < objNum) { this._largestSkippedObjectNum = objNum; } return should; } serializeToBuffer() { return __awaiter(this, void 0, void 0, function* () { const incremental = !(this.snapshot instanceof DefaultDocumentSnapshot); const { size, header, indirectObjects, xref, trailerDict, trailer } = yield this.computeBufferSize(incremental); let offset = 0; const buffer = new Uint8Array(size); if (!incremental) { offset += header.copyBytesInto(buffer, offset); buffer[offset++] = CharCodes.Newline; } buffer[offset++] = CharCodes.Newline; for (let idx = 0, len = indirectObjects.length; idx < len; idx++) { const [ref, object] = indirectObjects[idx]; if (!this.shouldSave(incremental, ref.objectNumber, indirectObjects)) { continue; } const objectNumber = String(ref.objectNumber); offset += copyStringIntoBuffer(objectNumber, buffer, offset); buffer[offset++] = CharCodes.Space; const generationNumber = String(ref.generationNumber); offset += copyStringIntoBuffer(generationNumber, buffer, offset); buffer[offset++] = CharCodes.Space; buffer[offset++] = CharCodes.o; buffer[offset++] = CharCodes.b; buffer[offset++] = CharCodes.j; buffer[offset++] = CharCodes.Newline; offset += object.copyBytesInto(buffer, offset); buffer[offset++] = CharCodes.Newline; buffer[offset++] = CharCodes.e; buffer[offset++] = CharCodes.n; buffer[offset++] = CharCodes.d; buffer[offset++] = CharCodes.o; buffer[offset++] = CharCodes.b; buffer[offset++] = CharCodes.j; buffer[offset++] = CharCodes.Newline; buffer[offset++] = CharCodes.Newline; const n = object instanceof PDFObjectStream ? object.getObjectsCount() : 1; if (this.shouldWaitForTick(n)) yield waitForTick(); } if (xref) { offset += xref.copyBytesInto(buffer, offset); buffer[offset++] = CharCodes.Newline; } if (trailerDict) { offset += trailerDict.copyBytesInto(buffer, offset); buffer[offset++] = CharCodes.Newline; buffer[offset++] = CharCodes.Newline; } offset += trailer.copyBytesInto(buffer, offset); return buffer; }); } computeIndirectObjectSize([ref, object]) { const refSize = ref.sizeInBytes() + 3; // 'R' -> 'obj\n' const objectSize = object.sizeInBytes() + 9; // '\nendobj\n\n' return refSize + objectSize; } createTrailerDict(prevStartXRef) { /** * if last object (XRef Stream) is not in the output, then size is one less. * An XRef Stream object should always be the largest object number in PDF */ const size = this.context.largestObjectNumber + (this._largestSkippedObjectNum === this.context.largestObjectNumber ? 0 : 1); return this.context.obj({ Size: size, Root: this.context.trailerInfo.Root, Encrypt: this.context.trailerInfo.Encrypt, Info: this.context.trailerInfo.Info, ID: this.context.trailerInfo.ID, Prev: prevStartXRef ? PDFNumber.of(prevStartXRef) : undefined, }); } computeBufferSize(incremental) { return __awaiter(this, void 0, void 0, function* () { this._largestSkippedObjectNum = 0; this._lastXRefObjectNumber = 0; const header = PDFHeader.forVersion(1, 7); let size = this.snapshot.pdfSize; if (!incremental) { size += header.sizeInBytes() + 1; } size += 1; const xref = PDFCrossRefSection.create(); const security = this.context.security; const indirectObjects = this.context.enumerateIndirectObjects(); for (let idx = 0, len = indirectObjects.length; idx < len; idx++) { const indirectObject = indirectObjects[idx]; const [ref, object] = indirectObject; if (!this.shouldSave(incremental, ref.objectNumber, indirectObjects)) { continue; } if (security) this.encrypt(ref, object, security); xref.addEntry(ref, size); size += this.computeIndirectObjectSize(indirectObject); if (this.shouldWaitForTick(1)) yield waitForTick(); } // deleted objects for (let idx = 0; idx < this.snapshot.deletedCount; idx++) { const dref = this.snapshot.deletedRef(idx); if (!dref) break; const nextdref = this.snapshot.deletedRef(idx + 1); // add 1 to generation number for deleted ref xref.addDeletedEntry(PDFRef.of(dref.objectNumber, dref.generationNumber + 1), nextdref ? nextdref.objectNumber : 0); } const xrefOffset = size; size += xref.sizeInBytes() + 1; // '\n' const trailerDict = PDFTrailerDict.of(this.createTrailerDict(this.snapshot.prevStartXRef)); size += trailerDict.sizeInBytes() + 2; // '\n\n' const trailer = PDFTrailer.forLastCrossRefSectionOffset(xrefOffset); size += trailer.sizeInBytes(); size -= this.snapshot.pdfSize; return { size, header, indirectObjects, xref, trailerDict, trailer }; }); } encrypt(ref, object, security) { if (object instanceof PDFStream) { const encryptFn = security.getEncryptFn(ref.objectNumber, ref.generationNumber); const unencryptedContents = object.getContents(); const encryptedContents = encryptFn(unencryptedContents); object.updateContents(encryptedContents); } } } PDFWriter.forContext = (context, objectsPerTick) => new PDFWriter(context, objectsPerTick, defaultDocumentSnapshot); PDFWriter.forContextWithSnapshot = (context, objectsPerTick, snapshot) => new PDFWriter(context, objectsPerTick, snapshot); export default PDFWriter; //# sourceMappingURL=PDFWriter.js.map