There are many scenarios where you may need to generate PDF files from Salesforce. Recently I needed to allow users to create a formatted quote-type document from a Lightning Web Component. I tried two different approaches - one was significantly more successful than the other, and I will focus on this more successful approach for this article.
The less successful approach that I initially implemented, was to develop a Visualforce page, and then use the html2pdf javascript library to create a PDF from the Visualforce page. This approach yielded some strange rendering problems when the PDF documents got large, and was fussy and troublesome to change.
The more successful approach was to draw the PDF directly from javascript (without first creating an HTML page) using the jspdf javascript library and the jspdf autotable plugin. In this article I will outline how to use these two libraries to create PDFs from Salesforce LWC, to hopfeully save you some research and troubleshooting time.
Installing jspdf and jspdf autotable
-
Download the latest version of
jspdf
from here -
Save the file locally and then upload as a static resource named
jspdf
-
Ensure to set the cache setting to
public
-
Download the latest version of
jspdf-autotables
from here -
Save the file locally and then upload as a static resource named
jspdfautotable
-
Ensure the cache setting is set to
public
Basic jspdf concepts
jspdf
allows you to place elements on the PDF page using a variety of methods (e.g. text
, addImage
). You place elements using coordinates, and it’s all manual. Meaning, you have to keep track of where you have placed elements before and ensure that as you place new elements you are not overlapping with existing elements already on the page.
You also need to handle page breaking yourself, by keeping track of when you have run out of room on your page, and then using the addPage()
method to add a new page to your document.
The whole process will involve a lot of trial and error - making code changes and then rendering the PDF and adjusting.
jspdf
has a lot of other options that I have not explored - thankfully the documentation is good.
Setting up inside your LWC
Add your imports:
import JSPDF from "@salesforce/resourceUrl/jspdf";
import JSPDF_AUTOTABLE from "@salesforce/resourceUrl/jspdfautotable";
Load the scripts in the connectedCallback()
function - I load jspdf-autotables
after jspdf
:
loadScript(this, JSPDF)
.then(() => {
loadScript(this, JSPDF_AUTOTABLE);
})
.catch((error) => {
console.log("error");
throw error;
});
Inside your function where you will generate the PDF, start with this statement to set up jspdf
:
const { jsPDF } = window.jspdf;
Next, set up your new document. Here you define the page size and orientation, as well as what units you want to use to place elements on the page.
const doc = new jsPDF({
orientation: "p",
unit: "in",
format: "letter"
});
Once you have completed your PDF document, saving your document will also open it in a new browser tab:
doc.save("demo.pdf");
Adding PDF Elements
Text
Adding text is straightforward, using the text
method:
doc.text("Pricing Summary", 1, 2);
This statement adds the text “Pricing Summary” one inch from the left margin and two inches from the top margin.
Formatting Text
Refer to the jspdf
documentation for more, but here are a couple of basics - setting font, style, and size. Settings remain in effect for future text entries until changed:
doc.setFont("helvetica", "normal");
doc.setFontSize(20);
Lines
When adding lines, you need to set line weight, as well as the origin x/y coordinates and the terminus x/y coordinates.
doc.setLineWidth(0.1)
doc.line(1, 3.5, 7.5, 3.5)
Tables
The autotables plugin does a great job of inserting tables - it manages page breaks within tables and has lots of configuration options. The documentation is very thorough.
Here is example code adding a table:
doc.autoTable({
theme: "striped",
headStyles: {
cellPadding: { top: .05, right: .05, bottom: .05, left: .05 },
fontSize: 9,
fillColor: '#cc3300'
},
bodyStyles: {
cellPadding: { top: .05, right: .05, bottom: .05, left: .05 },
fontSize: 9
},
startY: 3.1,
margin: 1,
head: [
[
"Name",
"Cloud",
"Region",
"Active",
"Total/Month",
"Total/Year"
]
],
body: this.subs.map((sub) => [
this.truncateName(sub.Name),
sub.Cloud_Provider__r.Name,
sub.Cloud_Provider_Region__r.Name,
sub.Active__c == true ? "yes" : "no",
currFormatter.format(sub.Monthly_Total__c),
currFormatter.format(sub.Annual_Total__c)
])
});
Some call-outs:
- theme - there are several themes to choose from, and additional configurations can be layered on top to adjust the theme.
- headStyles/bodyStyles - used to format the headers and body. Font size and cell padding are important to include to get the table to look how you would like it. The documentation spells out all of the options for this.
- margin - need to set the margin so that autotables plugin can insert page breaks correctly.
- startY - sets where on the page the table should be placed
- head - header entries, array within an array
- body - table data, array of arrays
Images
Accessing Images as Data URLs
I used apex controller methods to return the images, stored as static resources in Salesforce, as data urls:
@AuraEnabled(cacheable=true)
public static string getLogo(){
String result = '';
StaticResource sr = [
SELECT Id, Body
FROM StaticResource
WHERE Name = 'LogoPng'
LIMIT 1
];
if (sr != null) {
result =
'data:image/png' +
';base64,' +
EncodingUtil.Base64Encode(sr.Body);
}
return result;
}
Inserting Images
Inserting the images is straightforward - you need to specify the image, type, and location.
doc.addImage(this.logo.data, "PNG", 1, 0.5);
You can also add width and height parameters - see “scaling images” below.
Scaling Images
When you insert images, you can define the width and height. But you need to know the aspect ratio of the image in order to resize it and preserve the proportions. Here is a function you can use to get the image dimensions, passing in the data url:
getImageDimensions(file) {
return new Promise(function (resolved, rejected) {
var i = new Image();
i.onload = function () {
resolved({ w: i.width, h: i.height });
};
i.src = file;
});
}
If you are going to use this, the calling function should be configured as async
, and this function called with await
.
Here is an example:
const dimensions = await this.getImageDimensions(this.logo.data);
const aspectRatio = dimensions.w / dimensions.h;
doc.addImage(this.logo.data, "PNG", 6.5, 0.5, 1.25, 1.25 / aspectRatio);
Here we are getting the image dimensions, calculating the aspect ratio, and then using that to set the height of the image.
Extras
Page Numbering
Here is a function that adds page numbers to the bottom of the PDF:
addFooters(doc) {
const pageCount = doc.internal.getNumberOfPages()
doc.setFont('helvetica', 'italic')
doc.setFontSize(8)
for (var i = 1; i <= pageCount; i++) {
doc.setPage(i)
doc.text('Page ' + String(i) + ' of ' + String(pageCount), doc.internal.pageSize.width / 2, 10.5, {
align: 'center'
})
}
}
To use it, just run it before saving the document:
this.addFooters(doc);
doc.save("demo.pdf");
Number and Date Formatting
The javascript objects Intl.NumberFormat
and Intl.DateFormat
are useful for formatting numbers, currency, and date data for including in your PDFs.
Here are some examples:
const currFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
const memFormatter = new Intl.NumberFormat("en-US", {
style: "unit",
unit: "gigabyte",
unitDisplay: "short",
minimumFractionDigits: 1,
maximumFractionDigits: 1
});
const fomrattedAnnualTotal = currFormatter.format(AnnualTotal);
const formattedMemory = memFormatter.format(Memory__c);
const currentDate = new Date();
const dateOptions = {
year: "numeric",
month: "long",
day: "numeric"
};
const formattedDate = new Intl.DateTimeFormat("en-US", dateOptions).format(
currentDate
);
String Truncation
Sometimes when the string data in my tables got too long, they started looking not great - so here is a function you can use to truncate long strings and add an ellipsis:
truncateName(input) {
if (input.length > 25) {
return input.substring(0, 25) + "...";
}
return input;
}
Then, instead of inserting your string column directly, pass it through truncateName
:
this.truncateName(Name)
Wrapping Up
I am pleased to have come across these great, full-featured libraries for creating PDFs from Salesforce. There are a myriad of situations where PDF generation is a requirement, so having an easy, flexible solution is fantastic.
Hopefully you will find this article helpful, and I have not left too many gaps in explaining how to get going with jspdf
. Good luck!