Forum Discussion

Phil_Wingfield's avatar
Phil_Wingfield
Community Member
15 days ago

Auto-Generate PDF Certificates in Storyline (No HTML Edits Needed)

I recently developed a way to generate a certificate PDF in Storyline without having to edit the published files. That means it works even in Storyline Preview and Review 360, and it's simple enough to use as a team-wide template.

I drew inspiration from Devlin Peck’s tutorial, but updated the code to follow current JavaScript standards and removed the need to modify output files manually. You’ll find the full code and a sample .story file at the bottom of this post. I you like it or have any suggestions for improvement, let me know!

You can also see an example version of it in action here: https://certificate-examples.s3.us-east-2.amazonaws.com/Certificate+Template+Example/story.html 

Table of Contents:

  • What You’ll Need
  • Define Your Storyline Variables
  • Certificate Background Image
  • Changing Orientation to Portrait
  • Placing Text on the Certificate
  • Naming the PDF File
  • Triggering the Script in Storyline
  • TL;DR
  • Full Code
  • .story File (attached to post)

 

What You’ll Need

  • Storyline (obviously!)
  • A background image for your certificate (.jpg or .png, set to A4 size)

Recommended:

  • Some sort of code reader such as Visual Studio Code
  • Somewhere to upload the certificate file for testing, such as AWS (though there are instructions for local storage below under the Certificate Background Image header)

Define Your Storyline Variables

You’ll need to set up the Storyline variables that will be pulled into the certificate.

Here are the variables I used:

Storyline Variable

Purpose

UserName

Learner’s name (typed in by the user)

CHANGECourseName

Course name (manually set per course)

CHANGECreditHours

Number of credit hours (manually set)

⚠️ Variable names are case-sensitive. UserName ≠ Username.

 

I made this as a template for our team, so the variables beginning with CHANGE are the ones they will change manually when using the template in a course by changing the values of these variables:

 

The current date is handled inside the JavaScript, so you don’t need to create a date variable in Storyline.

 

Here’s how the script pulls in those values:

This line gets us ready to pull the variables from storyline, and the next few lines redefine the Storyline variables as JavaScript variables:

const player = GetPlayer();

 

And then this line pulls the value of the UserName variable from Storyline and stores it in a new JavaScript constant called name (a constant is a type of variable that does not change later)

 

const name = player.GetVar(“UserName”);

 

So then we do that with all the variables:

const player = GetPlayer();
const name = player.GetVar("UserName");
const course = player.GetVar("CHANGECourseName");
const hours = String(player.GetVar("CHANGECreditHours"));

 

If you need to pull in other variables, follow the same pattern.

There is another variable, date, which is defined in the code itself in lines 20-24 and returns the current date. It does not need to be input to Storyline.

Certificate Background Image

You’ll need a background image for your certificate — It should be sized as A4 landscape (297mm x 210mm)***. Make sure to leave space for where the name, course name, date, and number of credit hours can go.

Here’s an example background you can view (hosted via S3):
📎 certificate_example.png

(Note: I may not maintain this file indefinitely.)

 

To load the image in the code, host it somewhere with public access (like AWS or Google Drive with a direct link), and reference the URL like this on line 2:

const CERT_BG_IMG_URL = "https://yourhost.com/your_image.png";

Alternatively, you can reference an image placed locally in the story_content folder after publishing and reference it as “/story_content/your_image_name.jpg”, but that will not work in Preview or Review 360.

 

***The defaults for a standard 8.5x11" are 216mmx280mm. jsPDF will convert your image to the correct size as defined in line 70, so the certificate may be slightly distorted if you don't use A4 sizing.

Changing Orientation to Portrait

There are 2 steps if you want to change your image to portrait instead of landscape.

  • In line 62: remove orientation: 'landscape' so it is just curly brackets: {}
    • The default for jsPDF is portrait A4, so we only need to define it as landscape if we want it that way.
  • In line 70: change the size of the imported image. Swap the 297 and 210 in that line (the x and y lengths) so it reads (img, 0, 0, 210, 297)
    • This will force whatever image you import to be 210mm wide and 297mm high, so make sure you change line 2 from the URL for my certificate to whatever certificate you have, or else it will be distorted.
    • Make sure that your portrait image is portrait A4 as well. 

🚧 CORS Warning

If you’re hosting the image online, make sure CORS is enabled in the hosting service so it can load locally and in preview mode.

For AWS:
🔗 Enable CORS in Amazon S3

If your PDF shows text but no background image, this may be why. The script is written to fall back and still display text if the image fails to load.

 

Placing Text on the Certificate

You’ll manually position the text using jsPDF’s .text() function. Here's an example:

doc.setFont("times", "bold");
doc.setFontSize(30);
doc.text(name, doc.internal.pageSize.width / 2, 130, { align: 'center' });

setFont() – sets the font and weight. There are 14 accepted fonts from jsPDF, and you can see the parameters in the image below, taken from the jsPDF documentation:

If you want to import custom fonts, Devlin Peck once again has a good tutorial: https://www.devlinpeck.com/content/jspdf-custom-font

 

setFontSize() – sets the font size

text() – 4 fields separated by commas which let you define the text (in this case it is the variable “name,” which was defined earlier in line 14), the x coordinate, the y coordinate, and then the alignment. Note that the x coordinate uses a formula to place it in the exact center of the document from left to right. The y coordinate, 130, means it is 130mm from the top.

To determine where your text should go:

  • Open your certificate background in Canva, GIMP, or another editor
  • Use guides or measurement tools to identify the X/Y positions. For example, in this screenshot, I had a line drawn in canva, opened the position editor, and could see the y coordinate at 129.97mm for the end where the name would go. So, it is entered as 130 in the text() function
  • Use those values in the .text() calls in your script

You can also preview your placement by:

  1. Pressing F12 to open the console in preview, review 360, or a published course
  1. Copying and pasting your entire code into the console. Then you can paste again and quickly adjust the x/y coordinates until you get it fine-tuned. This is also a good way to check for errors if the download is not happening.

 

Naming the PDF File

Line 54 defines the filename for the downloaded certificate:

    doc.save(`${name}_${course}_certificate.pdf`);

The ${} symbols simply allow you to call a variable

If you wanted to make it a static name, you could replace that with something like:

doc.save("Course Certificate.pdf");

 

Triggering the Script in Storyline

Use a Storyline trigger like this:

 

 

Make sure this happens after the learner has entered their name, or the UserName variable won’t have any value.

In my example file, I have another js script set to execute when the timeline begins on the slide - make sure you are editing the one tied to the download button. If the download button won't appear for some reason, then just set the button's initial state to normal instead of hidden.

TL;DR

  • Add a background image to your certificate
  • Set up your Storyline variables
  • Use jsPDF to place Storyline variables over it
  • Trigger the script
  • No need to modify published files
  • Works in Preview and Review 360 as well as published outputs

Full Code

// --- CONFIGURABLE SETTINGS --- //
const CERT_BG_IMG_URL = "https://certificate-examples.s3.us-east-2.amazonaws.com/certificate_example.png"; // Background image URL (A4 landscape: 297mm x 210mm)

// --- LOAD jsPDF LIBRARY DYNAMICALLY --- //
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/jspdf/3.0.1/jspdf.umd.min.js";

/**
 * Main logic to generate the certificate PDF after jsPDF loads.
 */
script.onload = () => {

  // --- GET VARIABLES FROM STORYLINE --- //
  const player = GetPlayer();
  const name = player.GetVar("UserName");
  const course = player.GetVar("CHANGECourseName");
  const hours = String(player.GetVar("CHANGECreditHours"));

  // --- FORMAT CURRENT DATE --- //
  const now = new Date();
  const dd = String(now.getDate());
  const mm = String(now.getMonth() + 1);
  const yyyy = now.getFullYear();
  const formattedDate = `${mm}/${dd}/${yyyy}`;

  /**
   * Draws the certificate text on the PDF.
   * @param {jsPDF} doc - The jsPDF document instance.
   */
  function drawText(doc) {

    // User Name
    doc.setFont("times", "bold");
    doc.setFontSize(30);
    doc.text(name, doc.internal.pageSize.width / 2, 130, { align: 'center' });

    // Credit Hours
    doc.setFont("times", "normal");
    doc.setFontSize(20);
    doc.text(hours, 233, 150, { align: 'left' });

    // Course Name
    doc.setFontSize(30);
    doc.setTextColor(0, 0, 0);
    doc.setFont("times", "bold");
    doc.text(course, doc.internal.pageSize.width / 2, 88, { align: 'center' });

    // Date
    doc.setFont("times", "normal");
    doc.setFontSize(24);
    doc.text(formattedDate, 210.64, 176.89, { align: 'left' });

    // Save PDF
    doc.save(`${name}_${course}_certificate.pdf`); // format of the saved document name
  }

  /**
   * Generates the PDF, adds background image, and overlays text.
   */
  async function generatePDF() {
    const { jsPDF } = window.jspdf;
    const doc = new jsPDF({ orientation: 'landscape'});

    // Load background image
    const img = new Image();
    img.src = CERT_BG_IMG_URL;
    img.crossOrigin = "anonymous";

    img.onload = function () {
      doc.addImage(img, 0, 0, 297, 210); // Defines position and size in mm or image
      drawText(doc);
    };

    // Skips background image loading if not available; check for CORS permissions
    img.onerror = function () {
      console.warn("Failed to load certificate background image. Continuing without it.");
      drawText(doc); // Proceed without background
    };
  }

  generatePDF();
};

// --- APPEND SCRIPT TO LOAD jsPDF --- //
document.head.appendChild(script);

 

4 Replies

  • I have managed to upload a new image with the certificate I have, but my certificate is in portrait position :/ I edited line 2 of the java code:  "Background image URL (A4 portrait: 210mm x 297mm)" but it did not make a difference. The certificate does not look right. Anything else you think I should adjust?

    • Phil_Wingfield's avatar
      Phil_Wingfield
      Community Member

      Karenuqkolav1uq​ 

      It sounds to me like you are editing the comment in line 2, which won't affect how the program runs. In javascript, anything following // is a comment and will not be considered code by the program reading it. 

      The 2 edits you make should instead be:

      • In line 62: remove orientation: 'landscape' so it is just curly brackets: {}
        • The default for jsPDF is portrait A4, so we only need to define it as landscape if we want that way.
      • The other thing is to change the size of the imported image in line 70. Swap the 297 and 210 in that line (the x and y lengths) so it reads (img, 0, 0, 210, 297)
        • This will force whatever image you import to be 210mm wide and 297mm high, so make sure you change line 2 from the URL for my certificate to whatever certificate you have or else it will be distorted.

       

      Let me know if this works! I see I didn't add instructions for making it portrait into my original post, so I'll edit it in.

      • Karenuqkolav1uq's avatar
        Karenuqkolav1uq
        Community Member

        Thank you so much Phil_Wingfield​ for answering my question. I have done the changes you told me, but I do not why when I test it the "download certificate" button does not appear 😭. I have done it several times as I really do not understand how this is affecting the appearance of the button. I have not altered any other section of the JavaScript. Line 2 with the link to the image, line 62 curly brackets and line 70 swap the numbers. Triggers have not been changed either. Here are some images. Any ideas of what the problem is?

        Thank you!