Creating PDFs from Lightning Web Components

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

  1. Download the latest version of jspdf from here

  2. Save the file locally and then upload as a static resource named jspdf

  3. Ensure to set the cache setting to public

  4. Download the latest version of jspdf-autotables from here

  5. Save the file locally and then upload as a static resource named jspdfautotable

  6. 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!