How to Merge Image Attachments into PDF in Salesforce Using a Lightning Web Component

Salesforce does not provide image-to-PDF merging by default, so build a Lightning Web Component that loads record images into Dynamsoft Document Viewer inside an iframe and exports them as a single PDF for printing and archiving.

Dynamsoft Document Viewer is used as the SDK for image editing and PDF generation. The SDK compiles several PDF and image processing libraries as WASM, which cannot be directly used in a lightning web component, but we can use iframe to bypass this restriction.

You can learn about how to start a lightning web component from scratch in the previous blog and the usage of Dynamsoft Document Viewer to merge images to PDF in another blog. We are going to focus on the implementation of the component.

Screenshot of the final result:

document viewer

What you’ll build: A Salesforce Lightning Web Component that loads images into an in-browser editor and merges them into a single PDF file using Dynamsoft Document Viewer inside an iframe.

Key Takeaways

  • Salesforce does not natively support merging image attachments into PDF; a Lightning Web Component with Dynamsoft Document Viewer fills this gap.
  • WASM-based libraries cannot run directly in LWC, but embedding them in an iframe loaded from a Static Resource is a reliable workaround.
  • The component communicates with the iframe via postMessage, sending a merge trigger and receiving the PDF as a dataURL.
  • This pattern applies to any Salesforce org where users attach receipts, invoices, or scanned documents that need PDF consolidation.

Common Developer Questions

How do I merge image attachments into a single PDF in Salesforce?

Load the record’s images into an iframe-hosted document editor, let the viewer assemble them into a multi-page document, and export that document as PDF when the user clicks merge. The LWC uses Dynamsoft Document Viewer as the image-to-PDF engine.

Can I use WebAssembly libraries inside a Salesforce Lightning Web Component?

Direct use can be difficult because Lightning imposes runtime and resource restrictions that do not always play well with WASM-based libraries. The workaround shown here is to run the WASM viewer inside an iframe backed by a Static Resource rather than inside the LWC itself.

How do I pass data between an LWC and an embedded iframe using postMessage?

The component communicates with the iframe via postMessage, sending a merge trigger and receiving the PDF as a dataURL. If the parent LWC does not receive the message, check that the targetOrigin is omitted during development or set to "*" for testing, then locked down to the correct Salesforce domain in production.

Prerequisites

  • A Salesforce Developer Edition or sandbox org with Lightning Web Components enabled.
  • Node.js and Salesforce CLI installed for local LWC development.
  • Basic familiarity with Lightning Web Components — see the previous blog for a from-scratch walkthrough.
  • A Dynamsoft Document Viewer license key. Get a 30-day free trial license for Dynamsoft Document Viewer.

Step 1: Create an HTML5 Page for Image-to-PDF Conversion

  1. Create a new HTML file with the following template:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <title>DDV - HelloWorld</title>
    </head>
    <style>
        html,body {
            width: 100%;
            height: 100%;
            margin:0;
            padding:0;
            overscroll-behavior-y: none;
            overflow: hidden;
        }
    
        #container {
            width: 100%;
            height: 100%;
        }
    </style>
    <body>
        <div id="container"></div>
    </body>
    <script type="module">
    </script>
    </html>
    
  2. Include dependencies.

    Include Dynamsoft Document Viewer in the page.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/ddv.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/ddv.css">
    
  3. Initialize Dynamsoft Document Viewer. You can apply for a license here.

    (async () => {
        // Public trial license which is valid for 24 hours
        // You can request a 30-day trial key from https://www.dynamsoft.com/customer/license/trialLicense/?product=dcv&package=cross-platform
        Dynamsoft.DDV.Core.license = "LICENSE-KEY";
        Dynamsoft.DDV.Core.engineResourcePath = "https://cdn.jsdelivr.net/npm/dynamsoft-document-viewer@1.1.0/dist/engine";// Lead to a folder containing the distributed WASM files
        await Dynamsoft.DDV.Core.init();
        Dynamsoft.DDV.setProcessingHandler("imageFilter", new Dynamsoft.DDV.ImageFilter());
    })();
    
  4. Create an EditViewer. We can add images into it.

    const editViewer = new Dynamsoft.DDV.EditViewer({
        container: "container",
    });
    
  5. Add an event listening to messages from another window. If it receives a message, save the images as PDF and post it as dataURL for the parent window.

    window.addEventListener(
      "message",
      async (event) => {
        console.log("received message in iframe");
        console.log(event);
        let pdf = await editViewer.currentDocument.saveToPdf();
        let dataURL = await blobToDataURL(pdf);
        window.parent.postMessage(dataURL);
      },
      false,
    );
    
    function blobToDataURL(blob) {
      return new Promise((resolve,reject)=>{
        var reader = new FileReader();
        reader.readAsDataURL(blob);
        reader.addEventListener("error",reject);
        reader.onload = function (e) {
          resolve(e.target.result);
        }
      })
    }
    

Step 2: Add the HTML5 Page as a Salesforce Static Resource

  1. Create a ddv folder under staticresources.

  2. Add ddv.resource-meta.xml as the meta file under staticresources.

    <?xml version="1.0" encoding="UTF-8"?>
    <StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
        <cacheControl>Private</cacheControl>
        <contentType>application/zip</contentType>
    </StaticResource>
    
  3. Add the HTML page as index.html in the ddv folder.

Step 3: Build the Lightning Web Component

  1. Create a new lightning web component.

  2. In the HTML, add a button to start the merging, a container for the HTML5 page and an anchor to store the PDF dataURL.

    <template>
      <lightning-card title="Dynamsoft Document Viewer" icon-name="custom:custom14">
        <div class="slds-m-around_medium">
          <lightning-button label="Merge into PDF" onclick={merge}></lightning-button>
          <div>
            <label>
              Received dataURL:&nbsp;
              <a target="_blank" href={dataURL}>{dataURLLabel}</a>
            </label>
          </div>
        </div>
        <div class="ddvViewer" id='ddvViewer' style="height:500px;width:400px;"></div>
      </lightning-card>
    </template>
    
  3. In the renderedCallback lifecycle event, append an iframe with its src set to the HTML5 page we wrote. We also add an event listening to dataURL messages from the iframe.

    import { LightningElement } from 'lwc';
    import ddv from '@salesforce/resourceUrl/ddv';
    export default class DocumentManager extends LightningElement {
      ddvInitialized = false;
      ddvFrame;
      dataURL = "";
    
      get dataURLLabel() {
        if (this.dataURL.length > 15) {
          return this.dataURL.substring(0,10) + "...";
        }else{
          return this.dataURL;
        }
      }
         
      renderedCallback() {
          if (this.ddvInitialized) {
              return;
          }
          this.ddvInitialized = true;
          window.addEventListener(
            "message",
            (event) => {
              this.dataURL = event.data;
            },
            false,
          );
          this.ddvFrame = document.createElement('iframe');
          this.ddvFrame.src = ddv + "/index.html";
          // div tag in which iframe will be added should have id attribute with value myDIV
          this.template.querySelector("div.ddvViewer").appendChild(this.ddvFrame);
    
          // provide height and width to it
          this.ddvFrame.setAttribute("style","height:100%;width:100%;");
      }
    }
    
  4. The merge function which is called when the merge images to PDF button is pressed.

    merge(){
      this.ddvFrame.contentWindow.postMessage("merge");
    }
    

All right, we’ve completed the component. We can take a step further to use APEX to get all the files on Salesforce. We may discuss this in the future.

Common Issues & Edge Cases

  • WASM fails to load in the iframe: Salesforce’s Content Security Policy can block external CDN scripts. Make sure the Dynamsoft Document Viewer JS and WASM files are included inside the Static Resource zip rather than loaded from a CDN, or add the CDN domain to your org’s CSP Trusted Sites.
  • postMessage not received by the parent LWC: Salesforce sandboxes the iframe origin. Always omit the targetOrigin check during development or set it to "*" for testing, then lock it down to the correct Salesforce domain in production.
  • Large images cause slow PDF generation: The WASM engine runs in the browser thread. If users routinely merge 20+ high-resolution photos, consider resizing images before adding them to the EditViewer, or show a loading spinner to signal progress.

Source Code

Get the complete sample project source code on GitHub