const hummus = require('hummus');
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.hummus = hummus;
this.logFile = 'hummus-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 = hummus.createWriter(this.output,
Object.assign( {}, this.encryptOptions, {
version: this._getVersion(this.options.version)
})
);
} else {
this.read();
try {
if (this.isBufferSrc) {
this.writer = hummus.createWriterToModify(
new hummus.PDFRStreamForBuffer(this.src),
new hummus.PDFStreamForResponse(this.outStream),
Object.assign( {}, this.encryptOptions, {
log: this.logFile
})
);
} else {
this.writer = hummus.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 = hummus[`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 hummus.PDFRStreamForBuffer(this.src);
}
const pdfReader = hummus.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 = hummus.createWriterToModify(
new hummus.PDFRStreamForBuffer(oldStream.toBuffer()),
new hummus.PDFStreamForResponse(this.outStream),
Object.assign( {}, this.encryptOptions, {
log: this.logFile
})
);
} else {
this.writer = hummus.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;