Convert web page to PDF with JavaScript

Weihui Guo
4 min readJun 8, 2019

We all know how to print or save a web page as a PDF from a browser. You may even add a button using window.print() to make it more convenient. But to send the page as a PDF attachment via email or convert multiple photos online as a PDF file cannot be done using any browser’s built-in feature. How do I catch the result of window.print() and save it as a variable or object then? You asked.

Turns out it is much easier than you think, with the help of some Java-script libraries. Here I try to put together what I have learned through the course of using JsPDF in my projects and answers on Stack Overflow.

I will start with the simplest code for converting Html to PDF using JsPDF.

Note that you may use window.open(pdf.output(‘bloburl’)) to debug, and then save the PDF file or send it as an attachment once all worked out.

I see many questions on Stack Overflow still asking about fromHtml() or addHtml(). I want to emphasize here that both these plugins are deprecated and no longer supported by the JsPDF team. Going forward, you should use html() instead.

Also, beware that html2canvas 1.0.0-alpha.11 or higher version is needed to use html(). However, the current version html2canvas 1.0.0-rc.3 returns a blank page, a similar issue as using an older version like 0.4.1. You may need to try different versions of the library if it doesn’t function as you expected. Currently, the latest version that works for me is html2canvas v1.0.0-rc.5.

To send the PDF via email, output it as datauristring.

function emailHtml() {
let pdf = new jsPDF('p', 'pt', 'a4');
pdf.setProperties({
title: "jsPDF sample"
});
pdf.html(document.body, {
callback: function () {
let obj = {};
obj.pdfContent = pdf.output('datauristring');
var jsonData = JSON.stringify(obj);
$.ajax({
url: '/api/jspdf/html2pdf',
type: 'POST',
contentType: 'application/json',
data: jsonData
});
}
});
}

There is one problem. Users likely zoom the web page before saving it, which leads to a zoomed page with missing content. To avoid that, you may adjust the html2canvas.scale according to the scale factor.

function download() {
let srcwidth = document.getElementById('pdf').scrollWidth;
let pdf = new jsPDF('p', 'pt', 'a4');
pdf.html(document.getElementById('idName'), {
html2canvas: {
scale: 600 / srcwidth
//600 is the width of a4 page. 'a4': [595.28, 841.89]
},
callback: function () {
window.open(pdf.output('bloburl'));
}
});
}

Update: I just modified jsPDF and moved the logic into html(). So zooming should not be an issue anymore.

Another usage of JsPDF is to save multiple photos/images with various sizes and different orientations to a single PDF file. You do not want to ask your client to save the photos one by one and then merge them into one file, right?

function exportPdf(urls) {
let pdf = new jsPDF('l', 'pt', 'a4');
pdf.text(20, 20, 'Some text here');
for (let i = 0; i < urls.length; i++) {
let img = new Image();
img.src = urls[i];
img.onload = function () {
const imgWidth = this.width;
const imgHeight = this.height;
const imgRatio = imgWidth / imgHeight;
if (i >= 0) {
if (imgRatio > 0) {
pdf.addPage([imgWidth, imgHeight]);
}
else {
pdf.addPage([imgWidth, imgHeight], 'p');
}
}
pdf.setPage(i + 2);
pdf.addImage(img, 'JPEG', 0, 0, imgWidth, imgHeight, null, 'NONE');
if (i == urls.length - 1) {
pdf.save('Photos.pdf');
}
}
}
}

The drawback is an unavoidable empty first page. You can delete it using .deletePage(1) or add some text to it if you will.

A better, but more sophisticated, way is to use a fixed page format, calculate the image size and aspect ratio, and then set the parameters (and rotate the image if needed) accordingly so the image can fit into the paper, i.e., a4 in this case. It can be either pdf.addImage(img, 'JPEG', adjustedX, adjustedY, adjustedWidth, adjustedHeight, null, 'NONE'); or pdf.addImage(img, 'JPEG', adjustedX, adjustedY, adjustedWidth, adjustedHeight, null, -90);

function exportPdf(urls) {
let pdf = new jsPDF('l', 'mm', 'a4');
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const pageRatio = pageWidth / pageHeight;
for (let i = 0; i < urls.length; i++) {
let img = new Image();
img.src = urls[i];
img.onload = function () {
const imgWidth = this.width;
const imgHeight = this.height;
const imgRatio = imgWidth / imgHeight;
if (i > 0) { pdf.addPage(); }
pdf.setPage(i + 1);
if (imgRatio >= 1) {
const wc = imgWidth / pageWidth;
if (imgRatio >= pageRatio) {
pdf.addImage(img, 'JPEG', 0, (pageHeight - imgHeight / wc) / 2, pageWidth, imgHeight / wc, null, 'NONE');
}
else {
const pi = pageRatio / imgRatio;
pdf.addImage(img, 'JPEG', (pageWidth - pageWidth / pi) / 2, 0, pageWidth / pi, (imgHeight / pi) / wc, null, 'NONE');
}
}
else {
const wc = imgWidth / pageHeight;
if (1 / imgRatio > pageRatio) {
const ip = (1 / imgRatio) / pageRatio;
const margin = (pageHeight - ((imgHeight / ip) / wc)) / 4;
pdf.addImage(img, 'JPEG', (pageWidth - (imgHeight / ip) / wc) / 2, -(((imgHeight / ip) / wc) + margin), pageHeight / ip, (imgHeight / ip) / wc, null, 'NONE', -90);
}
else {
pdf.addImage(img, 'JPEG', (pageWidth - imgHeight / wc) / 2, -(imgHeight / wc), pageHeight, imgHeight / wc, null, 'NONE', -90);
}
}
if (i == urls.length - 1) {
pdf.save('Photo.pdf');
}
}
}
}

Lastly, some tips for image processing. 1) It’ll be easier to set the background color of the pdf file to debug, especially for image elements. 2) If you have issues with image orientation, check the image EXIF first.

I collected some of my thoughts and experiences on this topic here as a reference. And I would like to share with you and hope it would be helpful to you as well.

--

--