@cantoo/pdf-lib
Version:
Create and modify PDF files with JavaScript
211 lines • 11 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const PDFCrossRefSection_1 = tslib_1.__importDefault(require("../document/PDFCrossRefSection"));
const PDFHeader_1 = tslib_1.__importDefault(require("../document/PDFHeader"));
const PDFTrailer_1 = tslib_1.__importDefault(require("../document/PDFTrailer"));
const PDFTrailerDict_1 = tslib_1.__importDefault(require("../document/PDFTrailerDict"));
const PDFRef_1 = tslib_1.__importDefault(require("../objects/PDFRef"));
const PDFStream_1 = tslib_1.__importDefault(require("../objects/PDFStream"));
const PDFObjectStream_1 = tslib_1.__importDefault(require("../structures/PDFObjectStream"));
const CharCodes_1 = tslib_1.__importDefault(require("../syntax/CharCodes"));
const utils_1 = require("../../utils");
const snapshot_1 = require("../../api/snapshot");
const PDFNumber_1 = tslib_1.__importDefault(require("../objects/PDFNumber"));
const PDFName_1 = tslib_1.__importDefault(require("../objects/PDFName"));
const PDFRawStream_1 = tslib_1.__importDefault(require("../objects/PDFRawStream"));
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_1.default &&
object.dict.lookup(PDFName_1.default.of('Type')) === PDFName_1.default.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 tslib_1.__awaiter(this, void 0, void 0, function* () {
const incremental = !(this.snapshot instanceof snapshot_1.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_1.default.Newline;
}
buffer[offset++] = CharCodes_1.default.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 += (0, utils_1.copyStringIntoBuffer)(objectNumber, buffer, offset);
buffer[offset++] = CharCodes_1.default.Space;
const generationNumber = String(ref.generationNumber);
offset += (0, utils_1.copyStringIntoBuffer)(generationNumber, buffer, offset);
buffer[offset++] = CharCodes_1.default.Space;
buffer[offset++] = CharCodes_1.default.o;
buffer[offset++] = CharCodes_1.default.b;
buffer[offset++] = CharCodes_1.default.j;
buffer[offset++] = CharCodes_1.default.Newline;
offset += object.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes_1.default.Newline;
buffer[offset++] = CharCodes_1.default.e;
buffer[offset++] = CharCodes_1.default.n;
buffer[offset++] = CharCodes_1.default.d;
buffer[offset++] = CharCodes_1.default.o;
buffer[offset++] = CharCodes_1.default.b;
buffer[offset++] = CharCodes_1.default.j;
buffer[offset++] = CharCodes_1.default.Newline;
buffer[offset++] = CharCodes_1.default.Newline;
const n = object instanceof PDFObjectStream_1.default ? object.getObjectsCount() : 1;
if (this.shouldWaitForTick(n))
yield (0, utils_1.waitForTick)();
}
if (xref) {
offset += xref.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes_1.default.Newline;
}
if (trailerDict) {
offset += trailerDict.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes_1.default.Newline;
buffer[offset++] = CharCodes_1.default.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_1.default.of(prevStartXRef) : undefined,
});
}
computeBufferSize(incremental) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
this._largestSkippedObjectNum = 0;
this._lastXRefObjectNumber = 0;
const header = PDFHeader_1.default.forVersion(1, 7);
let size = this.snapshot.pdfSize;
if (!incremental) {
size += header.sizeInBytes() + 1;
}
size += 1;
const xref = PDFCrossRefSection_1.default.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 (0, utils_1.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_1.default.of(dref.objectNumber, dref.generationNumber + 1), nextdref ? nextdref.objectNumber : 0);
}
const xrefOffset = size;
size += xref.sizeInBytes() + 1; // '\n'
const trailerDict = PDFTrailerDict_1.default.of(this.createTrailerDict(this.snapshot.prevStartXRef));
size += trailerDict.sizeInBytes() + 2; // '\n\n'
const trailer = PDFTrailer_1.default.forLastCrossRefSectionOffset(xrefOffset);
size += trailer.sizeInBytes();
size -= this.snapshot.pdfSize;
return { size, header, indirectObjects, xref, trailerDict, trailer };
});
}
encrypt(ref, object, security) {
if (object instanceof PDFStream_1.default) {
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, snapshot_1.defaultDocumentSnapshot);
PDFWriter.forContextWithSnapshot = (context, objectsPerTick, snapshot) => new PDFWriter(context, objectsPerTick, snapshot);
exports.default = PDFWriter;
//# sourceMappingURL=PDFWriter.js.map