From f439ab53d23b30464d0fa91218f905608793c4a0 Mon Sep 17 00:00:00 2001 From: erdembas Date: Mon, 23 May 2022 20:44:23 +0300 Subject: [PATCH] Unified feature added. --- handlers/generate.js | 105 ++++++++++---------- handlers/generateUnified.js | 89 +++++++++++++++++ index.js | 2 +- package.json | 1 + routes.js | 184 ++++++++++++++++++++++++------------ 5 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 handlers/generateUnified.js diff --git a/handlers/generate.js b/handlers/generate.js index 9b3da59..5fdf8a4 100644 --- a/handlers/generate.js +++ b/handlers/generate.js @@ -4,65 +4,62 @@ const config = require('../config'); const baseURL = config('BASE_URL'); const expiresIn = config('EXPIRES_IN'); -module.exports = async function(req, res) { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(422).json({ - success: true, - errors: errors.errors - }); - }; - - const timestamp = Math.round(Date.now() / 1000); - const randomID = `${timestamp}-${revisedRandId()}`; - const pdfOptions = { - path: `static/exports/${randomID}.pdf`, - format: 'A4', - margin: { - top: 0, - right: 0, - bottom: 0, - left: 0 - } - }; - const availableOptions = ['scale', 'displayHeaderFooter', 'headerTemplate', 'footerTemplate', 'printBackground', 'landscape', - 'pageRanges', 'format', 'width', 'height', 'margin.top', 'right', 'margin.bottom', 'margin.left', 'preferCSSPageSize']; - const integerOptions = ['scale', 'width', 'height']; - for (const option of availableOptions) { - if (req.body[option] && !option.includes('margin')) { - if (integerOptions.indexOf(option) > -1) { - pdfOptions[option] = Number(req.body[option]); - } else { - pdfOptions[option] = req.body[option]; - } - } +module.exports = async function (req, res) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(422).json({ + success: true, + errors: errors.errors, + }); + } - if (req.body[option] && option.includes('margin')) { - pdfOptions.margin[option.replace('margin.', '')] = req.body[option]; - } + const timestamp = Math.round(Date.now() / 1000); + const randomID = `${timestamp}-${revisedRandId()}`; + const pdfOptions = { + path: `static/exports/${randomID}.pdf`, + format: 'A4', + margin: { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + }; + const availableOptions = ['scale', 'displayHeaderFooter', 'headerTemplate', 'footerTemplate', 'printBackground', 'landscape', 'pageRanges', 'format', 'width', 'height', 'margin.top', 'margin.right', 'margin.bottom', 'margin.left', 'preferCSSPageSize']; + const integerOptions = ['scale', 'width', 'height']; + for (const option of availableOptions) { + if (req.body[option] && !option.includes('margin')) { + if (integerOptions.indexOf(option) > -1) { + pdfOptions[option] = Number(req.body[option]); + } else { + pdfOptions[option] = req.body[option]; + } } - if (!global.browser) { - global.browser = await puppeteer.launch({ args: ['--no-sandbox'] }) + if (req.body[option] && option.includes('margin')) { + pdfOptions.margin[option.replace('margin.', '')] = req.body[option]; } - const page = await global.browser.newPage(); - if (req.body.url) { - await page.goto(req.body.url); - } else { - await page.setContent(req.body.html); - } - await page.pdf(pdfOptions); + } - await page.close(); + if (!global.browser) { + global.browser = await puppeteer.launch({ args: ['--no-sandbox'] }); + } + const page = await global.browser.newPage(); + if (req.body.url) { + await page.goto(req.body.url); + } else { + await page.setContent(req.body.html); + } + await page.pdf(pdfOptions); - return res.status(200).json({ - success: true, - url: `${baseURL}/exports/${randomID}.pdf`, - path: `/exports/${randomID}.pdf`, - expires: timestamp + expiresIn - }); -} + await page.close(); + + return res.download(`static/exports/${randomID}.pdf`); +}; function revisedRandId() { - return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10); -} \ No newline at end of file + return Math.random() + .toString(36) + .replace(/[^a-z]+/g, '') + .substr(2, 10); +} diff --git a/handlers/generateUnified.js b/handlers/generateUnified.js new file mode 100644 index 0000000..4cf58e8 --- /dev/null +++ b/handlers/generateUnified.js @@ -0,0 +1,89 @@ +const { validationResult } = require('express-validator'); +const puppeteer = require('puppeteer'); +const config = require('../config'); +const baseURL = config('BASE_URL'); +const expiresIn = config('EXPIRES_IN'); +const PDFMerger = require('pdf-merger-js'); + +module.exports = async function (req, res) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(422).json({ + success: true, + errors: errors.errors, + }); + } + + const timestamp = Math.round(Date.now() / 1000); + + const files = []; + + const mergedFileId = `${timestamp}-merged-${revisedRandId()}`; + + req.body.base64Htmls.forEach((htmlElement) => {}); + + for (const index in req.body.base64Htmls) { + const htmlElement = req.body.base64Htmls[index]; + + const randomID = `${timestamp}-${revisedRandId()}`; + const pdfOptions = { + path: `static/exports/${randomID}.pdf`, + format: 'A4', + margin: { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, + }; + + const availableOptions = ['scale', 'displayHeaderFooter', 'headerTemplate', 'footerTemplate', 'printBackground', 'landscape', 'pageRanges', 'format', 'width', 'height', 'margin.top', 'margin.right', 'margin.bottom', 'margin.left', 'preferCSSPageSize']; + const integerOptions = ['scale', 'width', 'height']; + for (const option of availableOptions) { + if (req.body[option] && !option.includes('margin')) { + if (integerOptions.indexOf(option) > -1) { + pdfOptions[option] = Number(req.body[option]); + } else { + pdfOptions[option] = req.body[option]; + } + } + + if (req.body[option] && option.includes('margin')) { + pdfOptions.margin[option.replace('margin.', '')] = req.body[option]; + } + } + + if (!global.browser) { + global.browser = await puppeteer.launch({ args: ['--no-sandbox'] }); + } + const page = await global.browser.newPage(); + + const dec = Buffer(htmlElement, 'base64').toString('UTF8'); + await page.setContent(dec); + + await page.pdf(pdfOptions); + + await page.close(); + + files.push(randomID); + } + + const merger = new PDFMerger(); + + await (async () => { + files.forEach((id) => { + merger.add('static/exports/' + id + '.pdf'); + }); + + await merger.save('static/exports/' + mergedFileId + '.pdf'); //save under given name and reset the internal document + })(); + + return res.download(`static/exports/${mergedFileId}.pdf`); +}; + +function revisedRandId() { + return Math.random() + .toString(36) + .replace(/[^a-z]+/g, '') + .substr(2, 10); +} diff --git a/index.js b/index.js index e367528..d741ce5 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,7 @@ const speedLimiter = slowDown({ delayMs: config('RATE_LIMIT_DELAY_MS') }); -app.use(bodyParser.json()); +app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ extended: true })); app.use(upload.array()); app.use(express.static('static')) diff --git a/package.json b/package.json index 8ad2645..15f09d9 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "express-slow-down": "^1.3.1", "express-validator": "^6.1.1", "multer": "^1.4.2", + "pdf-merger-js": "^3.4.0", "puppeteer": "^1.19.0" }, "engines": { diff --git a/routes.js b/routes.js index ea2f3b7..1d567d1 100644 --- a/routes.js +++ b/routes.js @@ -1,65 +1,123 @@ const { check } = require('express-validator'); -module.exports.register = function(app) { - app.get('/', (req, res) => res.send(`Welcome! Please see check here if you're looking for the api docs`)) - app.post('/generate', [ - check('url').isURL({require_valid_protocol: true, allow_protocol_relative_urls: false, require_tld: true}).optional(), - check('html').optional(), - check('url_html').custom((value, {req}) => { - if (!req.body.html && !req.body.url) { - throw new Error("Must provide either url or html"); - } - - return true; - }), - check('scale').custom((value, {req}) => { - if (!value) { - return true; - } - if (Number.isNaN(value) || value < 0.1 || value > 2) { - throw new Error("scale must be between 0.1 and 2"); - } - - return true; - }), - check('displayHeaderFooter').isBoolean().optional(), - check('headerTemplate').optional(), - check('footerTemplate').optional(), - check('printBackground').isBoolean().optional(), - check('landscape').isBoolean().optional(), - check('pageRanges').custom((value, {req}) => { - if (!value) { - return true; // optional - } - - const pageRanges = value.split('-'); - if (Number.isNaN(pageRanges[0]) || Number.isNaN(pageRanges[1])) { - throw new Error("Invalid page range. Must be like 2-5"); - } - - return true; - }), - check('format').custom((value, {req}) => { - if (!value) { - return true; - } - - const options = ['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']; - - if (options.indexOf(value) === -1) { - throw new Error(`format must be one of ${options.join(',')}`); - } - - return true; - }), - check('width').isNumeric().optional(), - check('height').isNumeric().optional(), - check('margin.top').isNumeric().optional(), - check('margin.right').isNumeric().optional(), - check('margin.bottom').isNumeric().optional(), - check('margin.left').isNumeric().optional(), - check('preferCSSPageSize').isBoolean().optional() - ], require('./handlers/generate')); - - return app; -} \ No newline at end of file +module.exports.register = function (app) { + app.get('/', (req, res) => res.send(`Welcome! Please see check here if you're looking for the api docs`)); + app.post('/generate', + [ + check('url').isURL({ require_valid_protocol: true, allow_protocol_relative_urls: false, require_tld: true }).optional(), + check('html').optional(), + check('url_html').custom((value, { req }) => { + if (!req.body.html && !req.body.url) { + throw new Error('Must provide either url or html'); + } + + return true; + }), + check('scale').custom((value, { req }) => { + if (!value) { + return true; + } + if (Number.isNaN(value) || value < 0.1 || value > 2) { + throw new Error('scale must be between 0.1 and 2'); + } + + return true; + }), + check('displayHeaderFooter').isBoolean().optional(), + check('headerTemplate').optional(), + check('footerTemplate').optional(), + check('printBackground').isBoolean().optional(), + check('landscape').isBoolean().optional(), + check('pageRanges').custom((value, { req }) => { + if (!value) { + return true; // optional + } + + const pageRanges = value.split('-'); + if (Number.isNaN(pageRanges[0]) || Number.isNaN(pageRanges[1])) { + throw new Error('Invalid page range. Must be like 2-5'); + } + + return true; + }), + check('format').custom((value, { req }) => { + if (!value) { + return true; + } + + const options = ['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']; + + if (options.indexOf(value) === -1) { + throw new Error(`format must be one of ${options.join(',')}`); + } + + return true; + }), + check('width').isNumeric().optional(), + check('height').isNumeric().optional(), + check('margin.top').isNumeric().optional(), + check('margin.right').isNumeric().optional(), + check('margin.bottom').isNumeric().optional(), + check('margin.left').isNumeric().optional(), + check('preferCSSPageSize').isBoolean().optional(), + ], + require('./handlers/generate') + ); + + app.post( + '/generateUnified', + [ + check('base64Htmls').isArray(), + check('scale').custom((value, { req }) => { + if (!value) { + return true; + } + if (Number.isNaN(value) || value < 0.1 || value > 2) { + throw new Error('scale must be between 0.1 and 2'); + } + + return true; + }), + check('displayHeaderFooter').isBoolean().optional(), + check('headerTemplate').optional(), + check('footerTemplate').optional(), + check('printBackground').isBoolean().optional(), + check('landscape').isBoolean().optional(), + check('pageRanges').custom((value, { req }) => { + if (!value) { + return true; // optional + } + + const pageRanges = value.split('-'); + if (Number.isNaN(pageRanges[0]) || Number.isNaN(pageRanges[1])) { + throw new Error('Invalid page range. Must be like 2-5'); + } + + return true; + }), + check('format').custom((value, { req }) => { + if (!value) { + return true; + } + + const options = ['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']; + + if (options.indexOf(value) === -1) { + throw new Error(`format must be one of ${options.join(',')}`); + } + + return true; + }), + check('width').isNumeric().optional(), + check('height').isNumeric().optional(), + check('margin.top').isNumeric().optional(), + check('margin.right').isNumeric().optional(), + check('margin.bottom').isNumeric().optional(), + check('margin.left').isNumeric().optional(), + check('preferCSSPageSize').isBoolean().optional(), + ], + require('./handlers/generateUnified') + ); + + return app; +};