pdf-lib
Version:
Create and modify PDF files with JavaScript
157 lines (124 loc) • 4.9 kB
text/typescript
import PDFCrossRefSection from 'src/core/document/PDFCrossRefSection';
import PDFHeader from 'src/core/document/PDFHeader';
import PDFTrailer from 'src/core/document/PDFTrailer';
import PDFTrailerDict from 'src/core/document/PDFTrailerDict';
import PDFDict from 'src/core/objects/PDFDict';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import PDFObjectStream from 'src/core/structures/PDFObjectStream';
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer, waitForTick } from 'src/utils';
export interface SerializationInfo {
size: number;
header: PDFHeader;
indirectObjects: [PDFRef, PDFObject][];
xref?: PDFCrossRefSection;
trailerDict?: PDFTrailerDict;
trailer: PDFTrailer;
}
class PDFWriter {
static forContext = (context: PDFContext, objectsPerTick: number) =>
new PDFWriter(context, objectsPerTick);
protected readonly context: PDFContext;
protected readonly objectsPerTick: number;
private parsedObjects = 0;
protected constructor(context: PDFContext, objectsPerTick: number) {
this.context = context;
this.objectsPerTick = objectsPerTick;
}
async serializeToBuffer(): Promise<Uint8Array> {
const {
size,
header,
indirectObjects,
xref,
trailerDict,
trailer,
} = await this.computeBufferSize();
let offset = 0;
const buffer = new Uint8Array(size);
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];
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)) await 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;
}
protected computeIndirectObjectSize([ref, object]: [
PDFRef,
PDFObject,
]): number {
const refSize = ref.sizeInBytes() + 3; // 'R' -> 'obj\n'
const objectSize = object.sizeInBytes() + 9; // '\nendobj\n\n'
return refSize + objectSize;
}
protected createTrailerDict(): PDFDict {
return this.context.obj({
Size: this.context.largestObjectNumber + 1,
Root: this.context.trailerInfo.Root,
Encrypt: this.context.trailerInfo.Encrypt,
Info: this.context.trailerInfo.Info,
ID: this.context.trailerInfo.ID,
});
}
protected async computeBufferSize(): Promise<SerializationInfo> {
const header = PDFHeader.forVersion(1, 7);
let size = header.sizeInBytes() + 2;
const xref = PDFCrossRefSection.create();
const indirectObjects = this.context.enumerateIndirectObjects();
for (let idx = 0, len = indirectObjects.length; idx < len; idx++) {
const indirectObject = indirectObjects[idx];
const [ref] = indirectObject;
xref.addEntry(ref, size);
size += this.computeIndirectObjectSize(indirectObject);
if (this.shouldWaitForTick(1)) await waitForTick();
}
const xrefOffset = size;
size += xref.sizeInBytes() + 1; // '\n'
const trailerDict = PDFTrailerDict.of(this.createTrailerDict());
size += trailerDict.sizeInBytes() + 2; // '\n\n'
const trailer = PDFTrailer.forLastCrossRefSectionOffset(xrefOffset);
size += trailer.sizeInBytes();
return { size, header, indirectObjects, xref, trailerDict, trailer };
}
protected shouldWaitForTick = (n: number) => {
this.parsedObjects += n;
return this.parsedObjects % this.objectsPerTick === 0;
};
}
export default PDFWriter;