UNPKG

hummus-recipe

Version:

A powerful PDF tool for NodeJS based on HummusJS

304 lines (275 loc) 10.5 kB
const muhammara = require('muhammara'); const path = require('path'); const fs = require('fs'); const streams = require('memory-streams'); /** * @name Recipe * @desc Create a pdfDoc * @namespace * @constructor * @param {string} src - The file path or Buffer of the src file. * @param {string} output - The path of the output file. * @param {Object} [options] - The options for pdfDoc * @param {number} [options.version] - The pdf version * @param {string} [options.author] - The author * @param {string} [options.title] - The title * @param {string} [options.subject] - The subject * @param {string} [options.colorspace] - The default colorspace: rgb, cmyk, gray * @param {string[]} [options.keywords] - The array of keywords * @param {string} [options.password] - permission password * @param {string} [options.userPassword] - this 'view' password also enables encryption * @param {string} [options.ownerPassword] - this allows owner to 'edit' file * @param {string} [options.userProtectionFlag] - encryption security level (see permissions) * @param {string|string[]} [options.fontSrcPath] - directory location(s) of additional fonts */ class Recipe { constructor(src, output, options = {}) { this.src = src; // detect the src is Buffer or not this.isBufferSrc = this.src instanceof Buffer; this.isNewPDF = (!this.isBufferSrc && src.toLowerCase() === 'new'); this.encryptOptions = this._getEncryptOptions(options, this.isNewPDF); this.options = Object.assign({}, options, this.encryptOptions); this.current = {}; this.current.defaultFontSize = 14; if (this.isBufferSrc) { this.outStream = new streams.WritableStream(); this.output = output; } else { this.output = output || src; if (this.src) { this.filename = path.basename(this.src); } } this.muhammara = muhammara; this.logFile = 'muhammara-error.log'; this.textMarkupAnnotations = [ 'Highlight', 'Underline', 'StrikeOut', 'Squiggly' ]; this.annotationsToWrite = []; this.annotations = []; this.vectorsToWrite = []; this.xObjects = []; this.needToEncrypt = false; this.needToInsertPages = false; this._setParameters(options); this._loadFonts(path.join(__dirname, '../fonts')); if (options.fontSrcPath) { this._loadFonts(options.fontSrcPath); } this._createWriter(); } _createWriter() { if (this.isNewPDF) { this.writer = muhammara.createWriter(this.output, Object.assign( {}, this.encryptOptions, { version: this._getVersion(this.options.version) }) ); } else { this.read(); try { if (this.isBufferSrc) { this.writer = muhammara.createWriterToModify( new muhammara.PDFRStreamForBuffer(this.src), new muhammara.PDFStreamForResponse(this.outStream), Object.assign( {}, this.encryptOptions, { log: this.logFile }) ); } else { this.writer = muhammara.createWriterToModify(this.src, Object.assign( {}, this.encryptOptions, { modifiedFilePath: this.output, log: this.logFile }) ); } } catch (err) { throw new Error(err); } } this.info(this.options); } _getVersion(version) { const supportedVersions = [ 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 ]; if (!supportedVersions.includes(version)) { version = 1.7; } version = muhammara[`ePDFVersion${version * 10}`]; return version; } get position() { const { ox, oy } = this._reverseCoordinate(this._position.x, this._position.y); return { x: ox, y: oy }; } read(inSrc) { const isForExternal = (inSrc) ? true : false; try { let src = (isForExternal) ? inSrc : this.src; if (this.isBufferSrc) { src = new muhammara.PDFRStreamForBuffer(this.src); } const pdfReader = muhammara.createReader(src, this.encryptOptions); const pages = pdfReader.getPagesCount(); if (pages == 0) { // broken or modify password protected throw 'HummusJS: Unable to read/edit PDF file (pages=0)'; } const metadata = { pages }; for (var i = 0; i < pages; i++) { const info = pdfReader.parsePage(i); const dimensions = info.getMediaBox(); const rotate = info.getRotate(); let layout, width, height, pageSize; let side1 = Math.abs(dimensions[2] - dimensions[0]); let side2 = Math.abs(dimensions[3] - dimensions[1]); if (side1 > side2 && rotate % 180 === 0) { layout = 'landscape'; } else if (side1 < side2 && rotate % 180 !== 0) { layout = 'landscape'; } else { layout = 'portrait'; } if (layout === 'landscape') { width = (side1 > side2) ? side1 : side2; height = (side1 > side2) ? side2 : side1; } else { width = (side1 > side2) ? side2 : side1; height = (side1 > side2) ? side1 : side2; } pageSize = [width, height].sort((a, b) => { return (a > b) ? 1 : -1; }); const page = { pageNumber: i + 1, mediaBox: dimensions, layout, rotate, width, height, size: pageSize, // usually 0 offsetX: dimensions[0], offsetY: dimensions[1] }; metadata[page.pageNumber] = page; } if (!isForExternal) { this.pdfReader = pdfReader; this.metadata = metadata; } return metadata; } catch (err) { throw new Error(err); } } /** * End the pdfDoc * @function * @memberof Recipe * @param {function} callback - The callback function. */ endPDF(callback) { this._writeInfo(); this.writer.end(); // This is a temporary work around for copying context will overwrite the current one // write annotations at the end. if ( (this.annotations && this.annotations.length > 0) || (this.annotationsToWrite && this.annotationsToWrite.length > 0) ) { if (this.isBufferSrc) { const oldStream = this.outStream; this.outStream = new streams.WritableStream(); this.writer = muhammara.createWriterToModify( new muhammara.PDFRStreamForBuffer(oldStream.toBuffer()), new muhammara.PDFStreamForResponse(this.outStream), Object.assign( {}, this.encryptOptions, { log: this.logFile }) ); } else { this.writer = muhammara.createWriterToModify(this.output, Object.assign( {}, this.encryptOptions, { modifiedFilePath: this.output, log: this.logFile }) ); } this._writeAnnotations(); this._writeInfo(); this.writer.end(); } if (this.needToInsertPages) { if (this.isBufferSrc) { // eslint-disable-next-line no-console console.log('Feature: Inserting Pages is not supported in Buffer Mode yet.'); } else { this._insertPages(); } } if (this.needToEncrypt) { if (this.isBufferSrc) { // eslint-disable-next-line no-console console.log('Feature: Encryption is not supported in Buffer Mode yet.'); } else { this._encrypt(); } } if (this.isBufferSrc && this.output) { fs.writeFileSync(this.output, this.outStream.toBuffer()); } if (callback) { if (this.isBufferSrc) { if (this.output) { return callback(this.output); } else { return callback(this.outStream.toBuffer()); } } else { return callback(); } } } /** * Register callback procedure with hummus-recipe. * @function * @memberof Recipe * @param {string} key name assigned to given callback. Note that if an actual function is being * registered, and its given name is what is to be used to access it, the key is unnecessary. * @param {Function} callback procedure that can be accessed through hummus-recipe */ register(key, callback) { // Assume simply registering a function which will have an embedded name if (typeof key !== 'string') { if (!key.name) { throw 'Cannot register unnamed callback function. Provide \'name\' as first argument, then callback function.'; } callback = key; key = key.name; } if (this.__proto__[key]){ throw `Found conflict in Recipe prototypes. ${key} already exists.`; } if (typeof callback !== 'function') { throw `${key} expecting callback to be of type function.`; } this.__proto__[key] = callback; } } module.exports = Recipe;