A Step-by-Step Guide to Merging and Displaying PDFs in Salesforce

A Step-by-Step Guide to Merging and Displaying PDFs in Salesforce

Last Updated on May 30, 2024 by Rakesh Gupta

Big Idea or Enduring Question:

  • How to Merge PDFs and Display Them on the UI in Salesforce? 

Objectives:

After reading this blog, you’ll be able to:

  • Understand the needs of using a JavaScript library to merge PDFs.
  • Load and utilize a JavaScript library like PDF-LIB to merge PDF files on the client side.
  • Create and configure a Lightning Web Component (LWC) to display merged PDFs.
  • Embed an iframe within the LWC to display the merged PDF files directly on the Salesforce UI.
  • Launch a Lightning Web Component from a quick action on a Opportunity.
  • and much more.

In the past, I’ve written a few articles on Lightning Web Component. Why not check them out while you are at it?!

  1. Send Email as a Quick Action Using Lightning Web Component
  2. Embed Screen Flows in Lightning Web Component

Business Use case

Jordan Brown is working as a Salesforce Developer at Gurukul on Cloud (GoC). During a conversation with the Sales Director, Riley Wilson, Jordan discovered that Sales reps spend too much time sifting through quote PDFs attached to quotes for given opportunities.

An opportunity can have more than one quote, each potentially having one or more PDFs attached to it.

Riley asked Jordan to explore and provide a solution that allows Sales reps to view all quote PDFs in one place. He emphasized that the files from quotes should be displayed in the following order based on the quote status:

  • Accepted
  • Denied
  • Rejected
  • Approved
  • In Review
  • Needs Review
  • Draft

Additionally, if a quote has more than one file, the files should be displayed in the order they were created, with the most recently created file first.

What is a PDF Merge?

PDF Merge is the process of combining multiple PDF documents into a single PDF file. This is commonly done to consolidate related documents, simplify file management, or prepare documents for easier sharing and printing. The merging process ensures that all pages from the selected PDFs are combined in a specific order, creating a seamless, single document.

This is useful for all sorts of reasons, like:

  1. Keeping things together: Imagine a business proposal with a cover letter, project details, and terms of service. Merging them keeps everything in one place.
  2. Simplifying legal documents: Got a contract with amendments and exhibits scattered around? Merging them creates a neat, complete reference.
  3. Managing finances: Monthly, quarterly, and annual reports become much easier to handle when they’re all in one file.
  4. Quotes: Combine quotes for different products or services into a comprehensive package.

In the context of Salesforce, merging PDFs can be extremely beneficial for streamlining workflows and improving the efficiency of sales and service processes. For example, sales teams can merge quote PDFs for a specific opportunity to present a consolidated document to clients, saving time and reducing the risk of missing or misplacing individual files.

What Solutions Does Salesforce Offer for Merging PDFs?

Salesforce offers several ways to handle PDF merging, primarily through third-party AppExchange solutions, custom Apex code, and integration with external services. Here are detailed explanations for each approach:

  1. AppExchange Solutions: Salesforce’s AppExchange offers various third-party applications that support PDF merging. Popular options include:
    1. Conga Composer: Generate and merge PDFs with extensive customization options.
    2. Drawloop (Nintex): Automate document creation and merge PDFs seamlessly.
    3. S-Docs: Native Salesforce app for generating and merging documents directly within Salesforce.
  2. Apex Code: We can write custom Apex code to merge PDFs within Salesforce. This involves:
    1. Retrieving PDF Files: Fetch PDFs for merge.
    2. Merging PDFs: Use libraries like PDF-Lib to combine PDFs programmatically.
    3. Returning Merged PDF: Return the merged PDF as a base64-encoded string for display.
  3. External APIs: Integrate with external PDF merging services using APIs. Examples include:
    1. Adobe PDF Services API: Send PDFs to Adobe for merging and retrieve the combined document.
    2. PDF.co API: Use PDF.co’s API to merge PDFs and return the merged file to Salesforce.

In this blog post, I will use PDF-Lib to merge PDFs.

Automation Champion Approach (I-do):

In this blog post, I will guide you through the process of merging PDFs within Salesforce using the PDF-Lib JavaScript library. This approach ensures that sensitive data remains within your Salesforce environment, which is crucial for projects in healthcare, financial, or public sectors.

The reason for selecting PDF-Lib is that business stakeholders prefer not to send data outside or share it with any third party. This is a common scenario when working with healthcare, financial, or public sector projects. JavaScript libraries like PDF-Lib allow us to upload them as a static resource and merge PDFs within Salesforce without sending data outside. For testing purposes, I successfully merged 3 PDFs (each 40 pages, 355 KB per PDF) without any issues.

For this solution, I am not storing the merged PDF in Salesforce. If you want to store the merged PDF in Salesforce, please modify the code accordingly.

How It Works

  1. User Action: The user clicks on a button on the Opportunity record page.
  2. LWC Initialization: This action triggers a Lightning Web Component (LWC) that passes the Opportunity record ID to an Apex class.
  3. Apex Class Processing: The Apex class retrieves the related PDF files, converts them to base64, and returns this data to the LWC.
  4. PDF Merging: The LWC uses PDF-Lib to merge the PDF files on the client side.
  5. Display: Finally, the merged PDF is displayed within the LWC for the user to view.

This solution ensures that all PDF merging and displaying processes are handled securely within Salesforce, adhering to data privacy requirements.

Step 1: Upload PDF-Lib as a Static Resource

  1. Download PDF-Lib:
    1. Go to the official PDF-Lib website.
    2. Download the latest version of the PDF-Lib library, typically provided as pdf-lib.min.js.
  2. Compress (zip) the pdfLib folder. 
    1. Create a new folder on your computer and name it pdfLib.
    2. Move the pdf-lib.min.js file into the pdfLib folder.
    3. Compress (zip) the pdfLib folder.
  3. Upload to Salesforce:
    1. Click Setup.
    2. In the Quick Find box, type Static Resources and click on the Static Resources.
    3. Click the New button to create a new static resource.
    4. Enter a meaningful name for the static resource, such as PdfLib.
    5. Click Choose File to upload the pdfLib.zip file.
    6. Set the cache control to Public.
    7. Click Save to upload the static resource.

By completing these steps, you have successfully uploaded PDF-Lib as a static resource in Salesforce, making it available for use in your Lightning Web Component (LWC) and Apex.

Step 2: Create an Apex Class for PDF Retrieval

The purpose of creating an Apex class for PDF retrieval is to handle the backend logic required to retrieve the PDFs associated with quotes for a given Opportunity. This class will interact with Salesforce’s database to retrieve the PDF files, convert them into base64 format, and pass this data to the Lightning Web Component (LWC) for merging and displaying.

In a typical scenario, an Opportunity in Salesforce can have multiple quotes, each potentially having one or more PDFs attached. Manually navigating through each quote and its attached PDFs can be time-consuming and inefficient for Sales reps. The Apex class automates this process by programmatically fetching all the necessary PDF files related to an Opportunity’s quotes. This ensures that Sales reps can quickly access all relevant documents without having to search through individual records.

public class QuoteFileService {
    @AuraEnabled(cacheable=true)
    public static List<Map<String, String>> getPdfFilesWithIdsAsBase64(Id opportunityId) {
        // List of statuses in the required order
        List<String> statusOrder = new List<String>{'Accepted','Denied','Rejected','Approved','In Review','Needs Review','Draft'};
                    
        // Find all quotes related to the given opportunity
        List<Quote> quotes = [SELECT Id, Status FROM Quote WHERE OpportunityId = :opportunityId];
        
        // Collect all quote IDs
        Set<Id> quoteIds = new Set<Id>();
        Map<Id, String> quoteStatusMap = new Map<Id, String>();
        for (Quote quote : quotes) {
            quoteIds.add(quote.Id);
            quoteStatusMap.put(quote.Id, quote.Status);
        }
        
        // Find all content document links related to the quotes
        List<ContentDocumentLink> contentDocumentLinks = [
            SELECT ContentDocumentId, LinkedEntityId
            FROM ContentDocumentLink
            WHERE LinkedEntityId IN :quoteIds
        ];
        
        // Collect all content document IDs from the links
        Set<Id> contentDocumentIds = new Set<Id>();
        for (ContentDocumentLink cdl : contentDocumentLinks) {
            contentDocumentIds.add(cdl.ContentDocumentId);
        }
        
        // Find all content versions based on the content document IDs
        List<ContentVersion> contentVersions = [
            SELECT ContentDocumentId, VersionData, CreatedDate
            FROM ContentVersion
            WHERE ContentDocumentId IN :contentDocumentIds
            ORDER BY CreatedDate DESC
        ];
        
        // Map content document ID to its content versions
        Map<Id, List<ContentVersion>> documentVersionsMap = new Map<Id, List<ContentVersion>>();
        for (ContentVersion version : contentVersions) {
            if (!documentVersionsMap.containsKey(version.ContentDocumentId)) {
                documentVersionsMap.put(version.ContentDocumentId, new List<ContentVersion>());
            }
            documentVersionsMap.get(version.ContentDocumentId).add(version);
        }
        
        // Prepare the list of files with their IDs and base64 data
        List<Map<String, String>> pdfFilesWithIds = new List<Map<String, String>>();
        for (String status : statusOrder) {
            for (Quote quote : quotes) {
                if (quote.Status == status) {
                    List<ContentVersion> allVersionsForQuote = new List<ContentVersion>();
                    for (ContentDocumentLink link : contentDocumentLinks) {
                        if (link.LinkedEntityId == quote.Id && documentVersionsMap.containsKey(link.ContentDocumentId)) {
                            allVersionsForQuote.addAll(documentVersionsMap.get(link.ContentDocumentId));
                        }
                    }
                    allVersionsForQuote.sort();
                    
                    for (ContentVersion version : allVersionsForQuote) {
                        Map<String, String> pdfData = new Map<String, String>();
                        pdfData.put('ContentDocumentId', version.ContentDocumentId);
                        pdfData.put('Base64Data', EncodingUtil.base64Encode(version.VersionData));
                        pdfFilesWithIds.add(pdfData);
                    }
                }
            }
        }
        
        return pdfFilesWithIds;
    }
}

The Apex class serves as the bridge between the Salesforce data and the LWC, ensuring that the necessary data is processed and prepared correctly for the user interface to display the merged PDFs.

Step 3: Create a Lightning Web Component (LWC) to Merge and Display PDFs

The purpose of creating a Lightning Web Component (LWC) is to provide a user-friendly interface that can display the merged PDFs directly within Salesforce. This component will interact with the Apex class to fetch the necessary PDF data, merge the PDFs using the PDF-Lib library, and present the merged document to the user.

If you don’t know how to create a lightning component, please review this developer guide, Create a Lightning Web Component.

mergePDFs.html

The HTML template creates a “Merged PDF Viewer” card. Within this card, it uses conditional rendering to display content based on whether mergedPdfUrl is available. If mergedPdfUrl is true, an iframe displays the merged PDF. If mergedPdfUrl is false, a lightning spinner with the humorous alternative text “Hang tight, merging magic in progress…” is shown, indicating that the PDFs are still being merged.

<template>
    <lightning-card title="Merged PDF Viewer">
        <template if:true={mergedPdfUrl}>
            <iframe src={mergedPdfUrl} width="100%" height="1400px"></iframe>
        </template>
        <template if:false={mergedPdfUrl}>
            <lightning-spinner alternative-text="Hang tight, merging magic in progress..."></lightning-spinner>
        </template>
    </lightning-card>
</template>

mergePDFs.js

This JavaScript code uses the PDF-Lib library to merge PDF files. It loads the library as a static resource, retrieves PDF files using an Apex method, merges them on the client side, and displays the merged PDF in an iframe. The renderedCallback ensures the PDF-Lib library is loaded, and the @wire decorator fetches the necessary PDF data.

import { LightningElement, api, wire } from 'lwc';
import getPdfFilesWithIdsAsBase64 from '@salesforce/apex/QuoteFileService.getPdfFilesWithIdsAsBase64';
import pdfLib from '@salesforce/resourceUrl/pdfLib';
import { loadScript } from 'lightning/platformResourceLoader';

export default class MergePdfs extends LightningElement {
    @api recordId;
    isLibLoaded = false;
    mergedPdfUrl;
    pdfLibInstance;

    renderedCallback() {
        if (this.isLibLoaded) {
            return;
        }
        loadScript(this, pdfLib + '/pdf-lib.min.js')
            .then(() => {
                if (window['pdfLib'] || window['PDFLib']) {
                    this.isLibLoaded = true;
                    this.pdfLibInstance = window['pdfLib'] || window['PDFLib'];
                    this.loadPdfs();
                } else {
                    console.error('PDF-LIB not loaded correctly.');
                }
            })
            .catch(error => {
                console.error('Error loading PDF-LIB:', error);
            });
    }

    @wire(getPdfFilesWithIdsAsBase64, { opportunityId: '$recordId' })
    wiredPdfs({ error, data }) {
        if (this.isLibLoaded && data) {
            this.mergePDFs(data);
        } else if (error) {
            console.error('Error fetching PDFs:', error);
        }
    }

    async mergePDFs(pdfFiles) {
        if (!this.pdfLibInstance) {
            console.error('PDF-LIB instance is not defined.');
            return;
        }

        const { PDFDocument } = this.pdfLibInstance;

        const mergedPdf = await PDFDocument.create();
        for (let pdfFile of pdfFiles) {
            const pdfBytes = Uint8Array.from(atob(pdfFile.Base64Data), c => c.charCodeAt(0));
            const pdfDoc = await PDFDocument.load(pdfBytes);
            const copiedPages = await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
            copiedPages.forEach(page => mergedPdf.addPage(page));
        }

        const mergedPdfBytes = await mergedPdf.save();
        this.mergedPdfUrl = URL.createObjectURL(new Blob([mergedPdfBytes], { type: 'application/pdf' }));
    }
}

mergePDFs.js-meta.xml

This XML configuration file marks the component as exposed, making it available for use. The component is targeted for “lightning__RecordAction,” allowing it to be used as a quick action on record pages. Additionally, it is configured specifically for the Opportunity object, ensuring that the component can be triggered within the context of Opportunity records.

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>60.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordAction</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordAction">
            <objects>
                <object>Opportunity</object>
            </objects>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Step 4: Create a Quick Action to Launch the Lightning Web Component

The next step is to create a Quick Action on the Opportunity object to launch the mergePDFs Lightning Web Component. Salesforce will automatically pass the Opportunity ID to the recordId variable in the LWC.

  1. Click Setup.
  2. In the Object Manager, type Opportunity.
  3. Select Buttons, Links, and Action, then click New Action.
  4. Input the following information:
    1. Select Lightning Web Component as Action Type.
    2. Select mergePDFs as Lightning Web Component.
    3. Enter Label (Merge and View PDFs) the Name will auto-populate.
  5. Click Save.
  6. In the end, make sure to add the newly created quick action to the Opportunity Lightning record page.

Proof of Concept

Going forward, if a sales rep wants to view all quote PDFs for a given opportunity, they need to navigate to the Opportunity, click on the button Merge and View PDFs, which will display the quote PDFs in the following order based on the quote record status: ‘Accepted’, ‘Denied’, ‘Rejected’, ‘Approved’, ‘In Review’, ‘Needs Review’, ‘Draft’. If a quote has more than one file, the files will be displayed in the order they were created, with the most recently created file first.

Formative Assessment:

I want to hear from you!

What is one thing you learned from this post? How do you envision applying this new knowledge in the real world? Feel free to share in the comments below.

Go back

Your message has been sent

Have feedback, suggestions for posts, or need more information about Salesforce online training offered by me? Say hello, and leave a message!

Warning
Warning
Warning
Warning
Preferred Timing(required)
Warning
Warning

Warning.

20 thoughts on “A Step-by-Step Guide to Merging and Displaying PDFs in Salesforce

  1. I’m also getting TypeError: this.loadPdfs is not a function error when called in renderedCallback. How can I fix that?

  2. Hi Rakesh ,
    Thanks for the sharing this with us
    I just facing an error unchaught Event Listener,proxy because of package it’s the last version i appreciate if you could help me

    1. Thanks for reaching out. The “Uncaught Event Listener Proxy” error often occurs when there’s a compatibility issue with the latest version of the package. Let me know if you still need help.

  3. Hi Rakesh! I am very interested in this functionality, but I would like to use it on ticket reports. I would like to have the user select which files they want to merge on a single opportunity. Do you think that would be possible?

  4. Hi Rajesh, im getting the same error Minhee Kim and Sachin did “TypeError: this.loadPdfs is not a function” .

    Any ideas ?

    regards

  5. Sir, I have followed the every steps as per blog but I am getting error server responded with a status of 404 (Not Found)’.
    Can you please help me with which file do i need to download from pdf-lib website

  6. hello this article was very helpful.
    I tried the above code, but there’s a problem.
    when I click the pdf button twice, the error comes up.

    TypeError: this.loadPdfs is not a function

    how can i fix it?

    1. Thank you for trying out the functionality and for your comment, ANil. The error you’re encountering, Failed to load resource: the server responded with a status of 404 (Not Found), indicates that the resource (in this case, pdf-lib.min.js) could not be found on the Static Resource.

      Here are a few steps to troubleshoot and resolve this issue:

      1. Check Static Resources, steps from blog post
      2. Adjust Static resource URL if Necessary in lwc.js

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.