UNPKG

html2pdf-fix-jspdf

Version:

Client-side HTML-to-PDF rendering using pure JS

573 lines (505 loc) 16.5 kB
import jsPDF from "jspdf-fix-html2pdf"; import html2canvas from "html2canvas"; import { objType, createElement, cloneNode, toPx } from "./utils.js"; import es6promise from "es6-promise"; var Promise = es6promise.Promise; /* ----- CONSTRUCTOR ----- */ var Worker = function Worker(opt) { // Create the root parent for the proto chain, and the starting Worker. var root = Object.assign( Worker.convert(Promise.resolve()), JSON.parse(JSON.stringify(Worker.template)) ); var self = Worker.convert(Promise.resolve(), root); // Set progress, optional settings, and return. self = self.setProgress(1, Worker, 1, [Worker]); self = self.set(opt); return self; }; // Boilerplate for subclassing Promise. Worker.prototype = Object.create(Promise.prototype); Worker.prototype.constructor = Worker; // Converts/casts promises into Workers. Worker.convert = function convert(promise, inherit) { // Uses prototypal inheritance to receive changes made to ancestors' properties. promise.__proto__ = inherit || Worker.prototype; return promise; }; Worker.template = { prop: { src: null, container: null, overlay: null, canvas: null, img: null, pdf: null, pageSize: null }, progress: { val: 0, state: null, n: 0, stack: [] }, opt: { filename: "file.pdf", margin: [0, 0, 0, 0], image: { type: "jpeg", quality: 0.95 }, enableLinks: true, html2canvas: {}, jsPDF: {} } }; /* ----- FROM / TO ----- */ Worker.prototype.from = function from(src, type) { function getType(src) { switch (objType(src)) { case "string": return "string"; case "element": return src.nodeName.toLowerCase === "canvas" ? "canvas" : "element"; default: return "unknown"; } } return this.then(function from_main() { type = type || getType(src); switch (type) { case "string": return this.set({ src: createElement("div", { innerHTML: src }) }); case "element": return this.set({ src: src }); case "canvas": return this.set({ canvas: src }); case "img": return this.set({ img: src }); default: return this.error("Unknown source type."); } }); }; Worker.prototype.to = function to(target) { // Route the 'to' request to the appropriate method. switch (target) { case "container": return this.toContainer(); case "canvas": return this.toCanvas(); case "img": return this.toImg(); case "pdf": return this.toPdf(); default: return this.error("Invalid target."); } }; Worker.prototype.toContainer = function toContainer() { // Set up function prerequisites. var prereqs = [ function checkSrc() { return this.prop.src || this.error("Cannot duplicate - no source HTML."); }, function checkPageSize() { return this.prop.pageSize || this.setPageSize(); } ]; return this.thenList(prereqs).then(function toContainer_main() { // Define the CSS styles for the container and its overlay parent. var overlayCSS = { position: "fixed", overflow: "hidden", zIndex: 1000, left: 0, right: 0, bottom: 0, top: 0, backgroundColor: "rgba(0,0,0,0.8)" }; var containerCSS = { position: "absolute", width: this.prop.pageSize.inner.width + this.prop.pageSize.unit, left: 0, right: 0, top: 0, height: "auto", margin: "auto", backgroundColor: "white" }; // Set the overlay to hidden (could be changed in the future to provide a print preview). overlayCSS.opacity = 0; // Create and attach the elements. var source = cloneNode( this.prop.src, this.opt.html2canvas.javascriptEnabled ); this.prop.overlay = createElement("div", { className: "html2pdf__overlay", style: overlayCSS }); this.prop.container = createElement("div", { className: "html2pdf__container", style: containerCSS }); this.prop.container.appendChild(source); this.prop.overlay.appendChild(this.prop.container); document.body.appendChild(this.prop.overlay); }); }; Worker.prototype.toCanvas = function toCanvas() { // Set up function prerequisites. var prereqs = [ function checkContainer() { return document.body.contains(this.prop.container) || this.toContainer(); } ]; // Fulfill prereqs then create the canvas. return this.thenList(prereqs) .then(function toCanvas_main() { // Handle old-fashioned 'onrendered' argument. var options = Object.assign({}, this.opt.html2canvas); delete options.onrendered; return html2canvas(this.prop.container, options); }) .then(function toCanvas_post(canvas) { // Handle old-fashioned 'onrendered' argument. var onRendered = this.opt.html2canvas.onrendered || function() {}; onRendered(canvas); this.prop.canvas = canvas; document.body.removeChild(this.prop.overlay); }); }; Worker.prototype.toImg = function toImg() { // Set up function prerequisites. var prereqs = [ function checkCanvas() { return this.prop.canvas || this.toCanvas(); } ]; // Fulfill prereqs then create the image. return this.thenList(prereqs).then(function toImg_main() { var imgData = this.prop.canvas.toDataURL( "image/" + this.opt.image.type, this.opt.image.quality ); this.prop.img = document.createElement("img"); this.prop.img.src = imgData; }); }; Worker.prototype.toPdf = function toPdf() { // Set up function prerequisites. var prereqs = [ function checkCanvas() { return this.prop.canvas || this.toCanvas(); } ]; // Fulfill prereqs then create the image. return this.thenList(prereqs).then(function toPdf_main() { // Create local copies of frequently used properties. var canvas = this.prop.canvas; var opt = this.opt; // Calculate the number of pages. var pxFullHeight = canvas.height; var pxPageHeight = Math.floor( canvas.width * this.prop.pageSize.inner.ratio ); var nPages = Math.ceil(pxFullHeight / pxPageHeight); // Define pageHeight separately so it can be trimmed on the final page. var pageHeight = this.prop.pageSize.inner.height; // Create a one-page canvas to split up the full image. var pageCanvas = document.createElement("canvas"); var pageCtx = pageCanvas.getContext("2d"); pageCanvas.width = canvas.width; pageCanvas.height = pxPageHeight; // Initialize the PDF. this.prop.pdf = this.prop.pdf || new jsPDF(opt.jsPDF); for (var page = 0; page < nPages; page++) { // Trim the final page to reduce file size. if (page === nPages - 1 && pxFullHeight % pxPageHeight !== 0) { pageCanvas.height = pxFullHeight % pxPageHeight; pageHeight = (pageCanvas.height * this.prop.pageSize.inner.width) / pageCanvas.width; } // Display the page. var w = pageCanvas.width; var h = pageCanvas.height; pageCtx.fillStyle = "white"; pageCtx.fillRect(0, 0, w, h); pageCtx.drawImage(canvas, 0, page * pxPageHeight, w, h, 0, 0, w, h); // Add the page to the PDF. if (page) this.prop.pdf.addPage(); var imgData = pageCanvas.toDataURL( "image/" + opt.image.type, opt.image.quality ); this.prop.pdf.addImage( imgData, opt.image.type, opt.margin[1], opt.margin[0], this.prop.pageSize.inner.width, pageHeight ); } }); }; /* ----- OUTPUT / SAVE ----- */ Worker.prototype.output = function output(type, options, src) { // Redirect requests to the correct function (outputPdf / outputImg). src = src || "pdf"; if (src.toLowerCase() === "img" || src.toLowerCase() === "image") { return this.outputImg(type, options); } else { return this.outputPdf(type, options); } }; Worker.prototype.outputPdf = function outputPdf(type, options) { // Set up function prerequisites. var prereqs = [ function checkPdf() { return this.prop.pdf || this.toPdf(); } ]; // Fulfill prereqs then perform the appropriate output. return this.thenList(prereqs).then(function outputPdf_main() { /* Currently implemented output types: * https://rawgit.com/MrRio/jsPDF/master/docs/jspdf.js.html#line992 * save(options), arraybuffer, blob, bloburi/bloburl, * datauristring/dataurlstring, dataurlnewwindow, datauri/dataurl */ return this.prop.pdf.output(type, options); }); }; Worker.prototype.outputImg = function outputImg(type, options) { // Set up function prerequisites. var prereqs = [ function checkImg() { return this.prop.img || this.toImg(); } ]; // Fulfill prereqs then perform the appropriate output. return this.thenList(prereqs).then(function outputImg_main() { switch (type) { case undefined: case "img": return this.prop.img; case "datauristring": case "dataurlstring": return this.prop.img.src; case "datauri": case "dataurl": return (document.location.href = this.prop.img.src); default: throw 'Image output type "' + type + '" is not supported.'; } }); }; Worker.prototype.save = function save(filename) { // Set up function prerequisites. var prereqs = [ function checkPdf() { return this.prop.pdf || this.toPdf(); } ]; // Fulfill prereqs, update the filename (if provided), and save the PDF. return this.thenList(prereqs) .set(filename ? { filename: filename } : null) .then(function save_main() { this.prop.pdf.save(this.opt.filename); }); }; /* ----- SET / GET ----- */ Worker.prototype.set = function set(opt) { // TODO: Implement ordered pairs? // Silently ignore invalid or empty input. if (objType(opt) !== "object") { return this; } // Build an array of setter functions to queue. var fns = Object.keys(opt || {}).map(function(key) { if (key in Worker.template.prop) { // Set pre-defined properties. return function set_prop() { this.prop[key] = opt[key]; }; } else { switch (key) { case "margin": return this.setMargin.bind(this, opt.margin); case "jsPDF": return function set_jsPDF() { this.opt.jsPDF = opt.jsPDF; return this.setPageSize(); }; case "pageSize": return this.setPageSize.bind(this, opt.pageSize); default: // Set any other properties in opt. return function set_opt() { this.opt[key] = opt[key]; }; } } }, this); // Set properties within the promise chain. return this.then(function set_main() { return this.thenList(fns); }); }; Worker.prototype.get = function get(key, cbk) { return this.then(function get_main() { // Fetch the requested property, either as a predefined prop or in opt. var val = key in Worker.template.prop ? this.prop[key] : this.opt[key]; return cbk ? cbk(val) : val; }); }; Worker.prototype.setMargin = function setMargin(margin) { return this.then(function setMargin_main() { // Parse the margin property: [top, left, bottom, right]. switch (objType(margin)) { case "number": margin = [margin, margin, margin, margin]; case "array": if (margin.length === 2) { margin = [margin[0], margin[1], margin[0], margin[1]]; } if (margin.length === 4) { break; } default: return this.error("Invalid margin array."); } // Set the margin property, then update pageSize. this.opt.margin = margin; }).then(this.setPageSize); }; Worker.prototype.setPageSize = function setPageSize(pageSize) { return this.then(function setPageSize_main() { // Retrieve page-size based on jsPDF settings, if not explicitly provided. pageSize = pageSize || jsPDF.getPageSize(this.opt.jsPDF); // Add 'inner' field if not present. if (!pageSize.hasOwnProperty("inner")) { pageSize.inner = { width: pageSize.width - this.opt.margin[1] - this.opt.margin[3], height: pageSize.height - this.opt.margin[0] - this.opt.margin[2] }; pageSize.inner.px = { width: toPx(pageSize.inner.width, pageSize.k), height: toPx(pageSize.inner.height, pageSize.k) }; pageSize.inner.ratio = pageSize.inner.height / pageSize.inner.width; } // Attach pageSize to this. this.prop.pageSize = pageSize; }); }; Worker.prototype.setProgress = function setProgress(val, state, n, stack) { // Immediately update all progress values. if (val != null) this.progress.val = val; if (state != null) this.progress.state = state; if (n != null) this.progress.n = n; if (stack != null) this.progress.stack = stack; this.progress.ratio = this.progress.val / this.progress.state; // Return this for command chaining. return this; }; Worker.prototype.updateProgress = function updateProgress( val, state, n, stack ) { // Immediately update all progress values, using setProgress. return this.setProgress( val ? this.progress.val + val : null, state ? state : null, n ? this.progress.n + n : null, stack ? this.progress.stack.concat(stack) : null ); }; /* ----- PROMISE MAPPING ----- */ Worker.prototype.then = function then(onFulfilled, onRejected) { // Wrap `this` for encapsulation. var self = this; return this.thenCore(onFulfilled, onRejected, function then_main( onFulfilled, onRejected ) { // Update progress while queuing, calling, and resolving `then`. self.updateProgress(null, null, 1, [onFulfilled]); return Promise.prototype.then .call(this, function then_pre(val) { self.updateProgress(null, onFulfilled); return val; }) .then(onFulfilled, onRejected) .then(function then_post(val) { self.updateProgress(1); return val; }); }); }; Worker.prototype.thenCore = function thenCore( onFulfilled, onRejected, thenBase ) { // Handle optional thenBase parameter. thenBase = thenBase || Promise.prototype.then; // Wrap `this` for encapsulation and bind it to the promise handlers. var self = this; if (onFulfilled) { onFulfilled = onFulfilled.bind(self); } if (onRejected) { onRejected = onRejected.bind(self); } // Cast self into a Promise to avoid polyfills recursively defining `then`. var isNative = Promise.toString().indexOf("[native code]") !== -1 && Promise.name === "Promise"; var selfPromise = isNative ? self : Worker.convert(Object.assign({}, self), Promise.prototype); // Return the promise, after casting it into a Worker and preserving props. var returnVal = thenBase.call(selfPromise, onFulfilled, onRejected); return Worker.convert(returnVal, self.__proto__); }; Worker.prototype.thenExternal = function thenExternal(onFulfilled, onRejected) { // Call `then` and return a standard promise (exits the Worker chain). return Promise.prototype.then.call(this, onFulfilled, onRejected); }; Worker.prototype.thenList = function thenList(fns) { // Queue a series of promise 'factories' into the promise chain. var self = this; fns.forEach(function thenList_forEach(fn) { self = self.thenCore(fn); }); return self; }; Worker.prototype["catch"] = function(onRejected) { // Bind `this` to the promise handler, call `catch`, and return a Worker. if (onRejected) { onRejected = onRejected.bind(this); } var returnVal = Promise.prototype["catch"].call(this, onRejected); return Worker.convert(returnVal, this); }; Worker.prototype.catchExternal = function catchExternal(onRejected) { // Call `catch` and return a standard promise (exits the Worker chain). return Promise.prototype["catch"].call(this, onRejected); }; Worker.prototype.error = function error(msg) { // Throw the error in the Promise chain. return this.then(function error_main() { throw new Error(msg); }); }; /* ----- ALIASES ----- */ Worker.prototype.using = Worker.prototype.set; Worker.prototype.saveAs = Worker.prototype.save; Worker.prototype.export = Worker.prototype.output; Worker.prototype.run = Worker.prototype.then; /* ----- FINISHING ----- */ // Expose the Worker class. export default Worker;