pdfkit/lib/document.js
2018-12-17 21:20:19 +09:00

321 lines
7.5 KiB
JavaScript

/*
PDFDocument - represents an entire PDF document
By Devon Govett
*/
import stream from 'stream';
import fs from 'fs';
import PDFObject from './object';
import PDFReference from './reference';
import PDFPage from './page';
import PDFSecurity from './security';
import ColorMixin from './mixins/color';
import VectorMixin from './mixins/vector';
import FontsMixin from './mixins/fonts';
import TextMixin from './mixins/text';
import ImagesMixin from './mixins/images';
import AnnotationsMixin from './mixins/annotations';
import OutlineMixin from './mixins/outline';
class PDFDocument extends stream.Readable {
constructor(options = {}) {
super(options);
this.options = options;
// PDF version
switch (options.pdfVersion) {
case '1.4':
this.version = 1.4;
break;
case '1.5':
this.version = 1.5;
break;
case '1.6':
this.version = 1.6;
break;
case '1.7':
case '1.7ext3':
this.version = 1.7;
break;
default:
this.version = 1.3;
break;
}
// Whether streams should be compressed
this.compress = this.options.compress != null ? this.options.compress : true;
this._pageBuffer = [];
this._pageBufferStart = 0;
// The PDF object store
this._offsets = [];
this._waiting = 0;
this._ended = false;
this._offset = 0;
const Pages = this.ref({
Type: 'Pages',
Count: 0,
Kids: []});
Pages.finalize = function() {
this.offset = this.document._offset;
this.document._write(this.id + " " + this.gen + " obj");
this.document._write('<<');
this.document._write('/Type /Pages');
this.document._write(`/Count ${this.data.Count}`);
this.document._write(`/Kids [${Buffer.concat(this.data.Kids).slice(0,-1).toString()}]`);
this.document._write('>>');
this.document._write('endobj');
return this.document._refEnd(this);
};
this._root = this.ref({
Type: 'Catalog',
Pages
});
// The current page
this.page = null;
// Initialize mixins
this.initColor();
this.initVector();
this.initFonts();
this.initText();
this.initImages();
this.initOutline();
// Initialize the metadata
this.info = {
Producer: 'PDFKit',
Creator: 'PDFKit',
CreationDate: new Date()
};
if (this.options.info) {
for (let key in this.options.info) {
const val = this.options.info[key];
this.info[key] = val;
}
}
// Generate file ID
this._id = PDFSecurity.generateFileID(this.info);
// Initialize security settings
this._security = PDFSecurity.create(this, options);
// Write the header
// PDF version
this._write(`%PDF-${this.version}`);
// 4 binary chars, as recommended by the spec
this._write("%\xFF\xFF\xFF\xFF");
// Add the first page
if (this.options.autoFirstPage !== false) {
this.addPage();
}
}
addPage(options) {
// end the current page if needed
if (options == null) { ({ options } = this); }
if (!this.options.bufferPages) { this.flushPages(); }
// create a page object
this.page = new PDFPage(this, options);
this._pageBuffer.push(this.page);
// add the page to the object store
const pages = this._root.data.Pages.data;
pages.Kids.push(new Buffer(this.page.dictionary + ' '));
pages.Count++;
// reset x and y coordinates
this.x = this.page.margins.left;
this.y = this.page.margins.top;
// flip PDF coordinate system so that the origin is in
// the top left rather than the bottom left
this._ctm = [1, 0, 0, 1, 0, 0];
this.transform(1, 0, 0, -1, 0, this.page.height);
this.emit('pageAdded');
return this;
}
bufferedPageRange() {
return { start: this._pageBufferStart, count: this._pageBuffer.length };
}
switchToPage(n) {
let page;
if (!(page = this._pageBuffer[n - this._pageBufferStart])) {
throw new Error(`switchToPage(${n}) out of bounds, current buffer covers pages ${this._pageBufferStart} to ${(this._pageBufferStart + this._pageBuffer.length) - 1}`);
}
return this.page = page;
}
flushPages() {
// this local variable exists so we're future-proof against
// reentrant calls to flushPages.
const pages = this._pageBuffer;
this._pageBuffer = [];
this._pageBufferStart += pages.length;
for (let page of pages) {
page.end();
}
}
ref(data) {
const ref = new PDFReference(this, this._offsets.length + 1, data);
this._offsets.push(null); // placeholder for this object's offset once it is finalized
this._waiting++;
return ref;
}
_read() {}
// do nothing, but this method is required by node
_write(data) {
if (!Buffer.isBuffer(data)) {
data = new Buffer(data + '\n', 'binary');
}
this.push(data);
return this._offset += data.length;
}
addContent(data) {
this.page.write(data);
return this;
}
_refEnd(ref) {
this._offsets[ref.id - 1] = ref.offset;
if ((--this._waiting === 0) && this._ended) {
this._finalize();
return this._ended = false;
}
}
write(filename, fn) {
// print a deprecation warning with a stacktrace
const err = new Error(`\
PDFDocument#write is deprecated, and will be removed in a future version of PDFKit. \
Please pipe the document into a Node stream.\
`
);
console.warn(err.stack);
this.pipe(fs.createWriteStream(filename));
this.end();
return this.once('end', fn);
}
output(fn) {
// more difficult to support this. It would involve concatenating all the buffers together
throw new Error(`\
PDFDocument#output is deprecated, and has been removed from PDFKit. \
Please pipe the document into a Node stream.\
`
);
}
end() {
this.flushPages();
this._info = this.ref();
for (let key in this.info) {
let val = this.info[key];
if (typeof val === 'string') {
val = new String(val);
}
let entry = this.ref(val);
entry.end();
this._info.data[key] = entry;
}
this._info.end();
for (let name in this._fontFamilies) {
const font = this._fontFamilies[name];
font.finalize();
}
this.endOutline();
this._root.end();
this._root.data.Pages.end();
if (this._security) {
this._security.end();
}
if (this._waiting === 0) {
return this._finalize();
} else {
return this._ended = true;
}
}
_finalize(fn) {
// generate xref
const xRefOffset = this._offset;
this._write("xref");
this._write(`0 ${this._offsets.length + 1}`);
this._write("0000000000 65535 f ");
for (let offset of this._offsets) {
offset = (`0000000000${offset}`).slice(-10);
this._write(offset + ' 00000 n ');
}
// trailer
const trailer = {
Size: this._offsets.length + 1,
Root: this._root,
Info: this._info,
ID: [this._id, this._id]
};
if (this._security) {
trailer.Encrypt = this._security.dictionary;
}
this._write('trailer');
this._write(PDFObject.convert(trailer));
this._write('startxref');
this._write(`${xRefOffset}`);
this._write('%%EOF');
// end the stream
return this.push(null);
}
toString() {
return "[object PDFDocument]";
}
};
const mixin = methods => {
Object.assign(PDFDocument.prototype, methods);
};
mixin(ColorMixin);
mixin(VectorMixin);
mixin(FontsMixin);
mixin(TextMixin);
mixin(ImagesMixin);
mixin(AnnotationsMixin);
mixin(OutlineMixin);
export default PDFDocument;