UNPKG

@aeolun/muhammara

Version:

Create, read and modify PDF files and streams. A drop in replacement for hummusjs PDF library

326 lines (294 loc) 9.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 uses src if its not a buffer. * @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; } } function loadPrototypes() { const ignores = ["xObjectForm.js"]; fs.readdirSync(path.join(__dirname, "recipe")) .filter((file) => { return file[0] != "." && !ignores.includes(file); }) .forEach((file) => { const module = require(path.join(__dirname, "recipe", file)); for (let key in module) { if (Recipe.prototype[key]) { throw `Found conflict prototypes=${key} in ${file}.`; } Recipe.prototype[key] = module[key]; } }); } loadPrototypes(); module.exports = Recipe;