Forum Discussion
Creating downloadable pdf files in Storyline - an update on earlier methods
๐ณ๐๐พ๐๐พ ๐๐บ๐๐พ ๐ป๐พ๐พ๐ ๐๐บ๐๐ ๐พ๐๐ผ๐พ๐ ๐ ๐พ๐๐ ๐๐๐๐๐ ๐๐๐พ๐ ๐๐๐พ ๐๐พ๐บ๐๐ ๐๐ ๐ผ๐๐พ๐บ๐๐๐๐ ๐๐ฝ๐ฟ ๐ฟ๐๐ ๐พ๐ ๐ป๐ ๐๐๐๐๐ ๐ฉ๐บ๐๐บ๐ฒ๐ผ๐๐๐๐ ๐ฟ๐๐๐ ๐๐๐๐๐๐ ๐ฒ๐๐๐๐๐ ๐๐๐พ (๐พ.๐. Saving Storyline Variables to a PDF - Articulate Storyline Discussions - E-Learning Heroes) ๐บ๐๐ฝ ๐จ ๐๐บ๐๐พ ๐๐บ๐ฝ ๐๐พ๐ ๐ ๐ฟ๐๐๐ ๐๐บ๐๐ ๐๐พ๐๐๐พ๐ ๐๐ ๐๐๐๐ ๐๐๐๐พ ๐๐๐พ๐ ๐จ ๐๐บ๐ ๐๐๐๐๐๐บ๐ ๐ ๐ ๐๐๐๐๐๐ ๐๐ ๐ฟ๐๐๐๐๐พ ๐๐ ๐๐๐ ๐ฟ๐๐ ๐๐๐๐พ๐ ๐ฟ.
๐ฌ๐๐๐ ๐๐ฟ ๐๐๐พ ๐บ๐๐๐๐ผ๐ ๐พ๐ (๐ ๐๐๐พ ๐๐๐พ ๐๐๐พ ๐บ๐ป๐๐๐พ) ๐บ๐๐พ ๐ฟ๐บ๐๐๐ ๐ ๐๐ ๐ฝ ๐๐๐ ๐บ๐๐ฝ ๐บ๐ฝ๐๐๐ผ๐บ๐๐พ ๐๐๐๐๐ ๐๐๐พ ๐ฉ๐บ๐๐บ๐ฒ๐ผ๐๐๐๐ ๐ผ๐๐ฝ๐พ ๐ ๐๐ป๐๐บ๐๐ "๐๐๐ฏ๐ฝ๐ฟ". ๐ ๐๐๐๐๐ผ๐บ๐ ๐บ๐๐๐๐๐บ๐ผ๐ ๐๐๐๐ ๐ฝ ๐ป๐พ ๐๐ ๐ผ๐บ๐๐๐๐๐พ ๐๐๐๐ ๐ ๐พ๐บ๐๐๐พ๐'๐ ๐๐๐๐๐ ๐๐๐๐๐ ๐๐พ๐๐ ๐พ๐๐๐๐ ๐๐บ๐๐๐บ๐ป๐ ๐พ๐ ๐๐ ๐ฒ๐๐๐๐๐ ๐๐๐พ - ๐บ๐ ๐๐๐พ ๐พ๐๐ฝ ๐๐ฟ ๐๐๐พ ๐ผ๐๐๐๐๐พ ๐๐๐ ๐๐๐ผ๐ ๐๐ฝ๐พ ๐บ ๐ป๐๐๐๐๐ ๐๐๐บ๐ ๐๐๐๐๐๐พ๐๐ ๐๐๐๐พ ๐ฉ๐บ๐๐บ๐ฒ๐ผ๐๐๐๐ ๐๐๐๐ผ๐ ๐๐๐๐ ๐ฝ ๐๐๐พ๐ ๐๐พ๐๐๐๐พ๐๐พ ๐๐๐พ ๐ฒ๐๐๐๐๐ ๐๐๐พ ๐๐บ๐๐๐บ๐ป๐ ๐พ๐, ๐๐พ๐บ๐ฝ ๐๐ ๐บ ๐๐๐ ๐๐ ๐๐๐ ๐๐๐บ๐๐พ ๐๐ฟ ๐บ ๐๐ฝ๐ฟ ๐ฟ๐๐ ๐พ ๐๐บ๐๐พ, ๐๐๐๐พ๐๐ ๐๐๐พ ๐๐พ๐๐๐๐๐พ๐ฝ ๐๐พ๐๐ ๐บ๐ ๐๐๐พ ๐บ๐๐๐๐๐๐๐๐บ๐๐พ ๐ ๐บ๐๐ฝ ๐ ๐ผ๐๐๐๐ฝ๐๐๐บ๐๐พ๐ ๐๐ ๐๐๐พ ๐๐บ๐๐พ, ๐บ๐๐ฝ ๐๐๐พ๐ ๐ผ๐บ๐ ๐ ๐บ ๐ฟ๐๐๐ผ๐๐๐๐ ๐๐ "๐๐บ๐๐พ" ๐๐๐พ ๐๐พ๐๐๐ ๐๐บ๐๐ ๐ผ๐๐๐๐ ๐พ๐๐พ๐ฝ ๐๐ฝ๐ฟ.
๐ณ๐๐พ ๐ ๐พ๐บ๐๐๐พ๐ (๐ฝ๐พ๐๐พ๐๐ฝ๐๐๐ ๐๐ ๐๐๐พ๐๐ ๐ป๐๐๐๐๐พ๐) ๐๐๐๐ ๐ฝ ๐ป๐พ ๐๐๐พ๐๐พ๐๐๐พ๐ฝ ๐๐๐๐ ๐บ๐ ๐๐๐๐๐๐ ๐๐ '๐๐บ๐๐พ' ๐๐ '๐ฝ๐๐๐๐ ๐๐บ๐ฝ' ๐๐๐พ ๐ฟ๐๐ ๐พ.
๐ข๐๐พ๐บ๐๐๐๐ ๐๐๐ ๐๐๐๐ ๐พ ๐๐บ๐๐พ ๐๐ฝ๐ฟ'๐ ๐๐บ๐ ๐ผ๐๐๐๐ ๐๐ผ๐บ๐๐พ๐ฝ - ๐ป๐๐ ๐บ๐ ๐๐ ๐๐๐๐๐๐ป๐ ๐พ. ๐จ'๐๐พ ๐๐พ๐๐๐๐๐บ๐ ๐ ๐ ๐ฝ๐๐๐พ ๐๐บ๐๐ ๐๐๐ผ๐ผ๐พ๐๐๐ฟ๐๐ ๐๐๐๐๐พ๐ผ๐๐ ๐๐๐๐๐ ๐๐๐๐ ๐บ๐๐๐๐๐บ๐ผ๐ ๐ฟ๐๐ ๐ฝ๐๐๐๐ ๐๐บ๐ฝ๐๐๐ ๐ผ๐๐๐๐๐พ ๐๐๐๐พ๐, ๐๐๐๐๐พ๐ ๐๐พ๐๐๐ ๐๐ ๐๐ ๐ผ๐พ๐๐๐๐ฟ๐๐ผ๐บ๐๐พ๐.
๐ฆ๐ผ ๐๐ต๐ ๐ฎ๐บ ๐ ๐ป๐ผ๐ ๐ฎ๐ฑ๐๐ผ๐ฐ๐ฎ๐๐ถ๐ป๐ด ๐ฎ ๐ฑ๐ถ๐ณ๐ณ๐ฒ๐ฟ๐ฒ๐ป๐ ๐ฎ๐ฝ๐ฝ๐ฟ๐ผ๐ฎ๐ฐ๐ต?
๐ถ๐พ๐ ๐ , ๐ ๐๐๐พ ๐๐๐พ ๐บ๐๐๐๐ผ๐ ๐พ๐, ๐๐๐ฏ๐ฝ๐ฟ ๐๐ ๐บ ๐ ๐๐ป๐๐บ๐๐ ๐๐๐บ๐ ๐๐บ๐ ๐ป๐พ๐พ๐ ๐บ๐๐๐๐๐ฝ ๐๐๐๐พ ๐๐๐๐พ. ๐จ๐ ๐ฝ๐๐พ๐ ๐บ ๐๐๐๐ฝ ๐๐๐ป ๐บ๐๐ฝ ๐๐๐๐๐๐บ๐๐ฝ๐ ๐๐ฟ ๐ฉ๐บ๐๐บ๐ฒ๐ผ๐๐๐๐ ๐๐๐๐๐๐บ๐๐๐พ๐๐ ๐๐พ๐ ๐ ๐๐ ๐๐. ๐จ๐ ๐ผ๐บ๐ ๐ผ๐๐พ๐บ๐๐พ, ๐บ๐๐ฝ ๐๐บ๐๐พ ๐๐ฝ๐ฟ'๐ ๐๐๐๐ ๐ฟ๐๐๐พ. ๐ง๐๐๐พ๐๐พ๐, ๐บ ๐๐๐๐พ ๐๐พ๐ผ๐พ๐๐ ๐ฉ๐บ๐๐บ๐ฒ๐ผ๐๐๐๐ ๐ ๐๐ป๐๐บ๐๐ "๐๐ฝ๐ฟ-๐ ๐๐ป" ๐๐บ๐ ๐บ ๐ซ๐ฎ๐ณ ๐๐๐๐พ ๐ฟ๐๐๐ผ๐๐๐๐๐บ๐ ๐๐๐. ๐จ๐ ๐๐บ๐๐๐๐ผ๐๐ ๐บ๐, ๐๐ ๐ผ๐บ๐
๐๐๐ฝ๐๐ฟ๐ ๐พ๐๐๐๐๐๐๐ ๐๐ฝ๐ฟ'๐
๐ผ๐๐พ๐บ๐๐พ, ๐ฟ๐๐ ๐ ๐บ๐๐ฝ ๐ฟ๐ ๐บ๐๐๐พ๐ ๐๐ฝ๐ฟ ๐ฟ๐๐๐๐
๐บ๐ฝ๐ฝ, ๐๐๐๐พ๐๐ ๐บ๐๐ฝ ๐๐พ๐๐๐๐พ ๐๐บ๐๐พ๐ ๐ฟ๐๐๐ ๐บ ๐๐ฝ๐ฟ
...๐บ๐๐ฝ ๐บ ๐๐๐๐ ๐พ ๐ ๐๐ ๐๐๐๐พ.
๐ณ๐๐๐ ๐๐พ๐บ๐๐ ๐๐๐บ๐ ๐๐๐๐๐พ๐บ๐ฝ ๐๐ฟ ๐๐บ๐๐๐ฟ๐๐ ๐ ๐ ๐๐๐๐๐๐๐ ๐๐๐ ๐๐๐พ๐๐พ ๐๐๐ ๐๐พ๐พ๐ฝ ๐๐ ๐๐ ๐บ๐ผ๐พ ๐๐๐พ ๐๐พ๐๐ ๐๐ ๐พ๐บ๐ผ๐ ๐๐บ๐๐พ ๐๐ฟ ๐๐๐พ ๐๐ฝ๐ฟ, ๐๐๐ ๐ผ๐บ๐ ๐๐๐๐๐ ๐ ๐ฝ๐พ๐๐๐๐ ๐บ ๐๐ฝ๐ฟ ๐ฟ๐๐๐, ๐๐บ๐๐พ ๐๐๐พ ๐ฟ๐๐พ๐ ๐ฝ๐ ๐บ๐๐ฝ ๐๐๐พ๐ ๐๐๐พ ๐๐๐พ ๐๐พ๐๐๐๐พ๐๐พ๐ฝ ๐๐บ๐๐๐บ๐ป๐ ๐พ๐ ๐ฟ๐๐๐ ๐ฒ๐๐๐๐๐ ๐๐๐พ ๐๐ '๐ฟ๐๐ ๐ ' ๐๐๐ ๐๐๐พ ๐ฟ๐๐๐.
๐ถ๐พ ๐๐บ๐๐พ ๐ป๐พ๐พ๐ ๐บ๐๐บ๐๐พ ๐๐ฟ ๐๐ฝ๐ฟ-๐ ๐๐ป ๐ฟ๐๐ ๐๐๐๐พ ๐๐๐๐พ ๐ป๐๐ ๐๐พ๐๐พ๐ ๐ฟ๐๐๐๐ฝ ๐๐๐พ ๐๐๐๐พ ๐๐ ๐ฝ๐ ๐บ๐๐๐๐๐๐๐ ๐บ๐ป๐๐๐ ๐๐ (๐๐๐๐๐ ๐๐๐พ ๐๐ ๐ฝ ๐๐๐๐๐๐บ๐๐๐พ๐๐' ๐บ๐ฝ๐บ๐๐พ "๐๐ฟ ๐๐๐๐ ๐ผ๐๐ฝ๐พ ๐๐ ๐๐๐๐๐๐๐ ๐ฝ๐๐'๐ ๐๐๐ ๐บ๐๐ฝ ๐ฟ๐๐ ๐๐"). ๐ฎ๐๐พ๐ ๐๐๐พ ๐ข๐๐๐๐๐๐๐บ๐ ๐ป๐๐พ๐บ๐ ๐๐-๐ป๐พ๐๐๐พ๐พ๐ ๐ผ๐พ๐ ๐พ๐ป๐๐บ๐๐๐๐๐ (๐บ๐๐ฝ ๐๐๐๐ป๐บ๐ป๐ ๐ ๐ฟ๐๐พ๐ ๐พ๐ฝ ๐ป๐ ๐๐๐ ๐๐๐ผ๐ ๐ฟ๐พ๐๐๐๐๐พ ๐๐๐๐๐๐ ๐๐ ๐๐ผ๐ผ๐บ๐๐๐๐๐) ๐๐พ ๐๐พ๐๐พ ๐บ๐ป๐ ๐พ ๐๐ ๐๐พ๐ ๐บ ๐๐พ๐ (๐ป๐๐ ๐๐พ๐๐ ๐ป๐บ๐๐๐ผ) ๐ฝ๐พ๐๐๐๐๐๐๐บ๐๐๐๐ ๐๐๐๐๐๐๐ ๐๐๐๐ผ๐ ๐ผ๐บ๐ ๐ป๐พ ๐๐๐พ๐๐พ๐ฝ ๐๐พ๐๐พ:
https://demo5.profilelearning.com
๐ฎ๐, ๐๐๐๐ ๐๐๐'๐ ๐บ ๐๐พ๐๐ ๐๐๐๐๐พ๐๐๐๐๐พ ๐ฝ๐พ๐๐ - ๐ป๐๐ ๐๐พ ๐บ๐๐พ ๐๐๐๐๐๐๐ ๐๐ ๐๐ - ๐บ๐๐ฝ ๐๐๐ ๐ ๐๐๐ป๐ ๐๐๐ ๐๐๐ ๐๐ ๐๐๐๐พ๐พ ๐๐๐๐พ ๐บ๐๐๐๐ผ๐ ๐พ๐ ๐พ๐๐๐ ๐บ๐๐๐๐๐ ๐บ ๐ป๐๐ ๐๐๐๐พ ๐บ๐ป๐๐๐ ๐๐๐๐ ๐ผ๐๐ฝ๐พ ๐ ๐๐ป๐๐บ๐๐ ๐บ๐๐ฝ ๐บ๐ ๐๐ ๐บ๐ป๐๐๐ '๐๐๐ฝ๐พ๐๐ ๐ฉ๐บ๐๐บ๐ฒ๐ผ๐๐๐๐'
๐๐๐๐ ๐๐ผ ๐๐ต๐ฒ๐ ๐๐ผ๐๐ฟ ๐ฎ๐ฝ๐ฝ๐ฒ๐๐ถ๐๐ฒ ๐ฎ ๐น๐ถ๐๐๐น๐ฒ ๐ณ๐๐ฟ๐๐ต๐ฒ๐ฟ ๐๐ผ ๐ฏ๐ผ๐ผ๐ธ๐บ๐ฎ๐ฟ๐ธ ๐๐ต๐ถ๐ ๐ฝ๐ผ๐๐ - ๐๐ฒ ๐ฎ๐น๐๐ผ ๐ต๐ฎ๐๐ฒ ๐๐ต๐ฒ ๐ฎ๐ฏ๐ผ๐๐ฒ ๐ฑ๐ฒ๐บ๐ผ๐ป๐๐๐ฟ๐ฎ๐๐ถ๐ผ๐ป ๐๐ผ๐ฟ๐ธ๐ถ๐ป๐ด ๐ถ๐ป ๐ฅ๐๐ฆ๐....
- JohnCooper-be3cCommunity Member
Hi
Your reply prompted me to tidy the demo up a bit - same link as above.
https://demo5.profilelearning.com
I am more than happy to explain how this is done - and share the Storyline file for this demo. But I would ask a favour in return - we have set up a new LinkedIn company page and will be publishing articles there on all things learning technology - it's new and needs followers!!
https://www.linkedin.com/company/profilelearning/
OK - so the heart of the JavaScript works this way:
Firstly it retrieves the Storyline variables it requires
//Retrieve Player Variables
var player = GetPlayer();
learnerName = player.GetVar("Name");
learnerTitle = player.GetVar("Title");
learnerNotes = player.GetVar("Notes");the code then dynamically loads the "pdf-lib" JavaScript library using an import() construction. This is part of 'modern JavaScript' and can only be used to load ES format or UMD format modules. By doing it this way you load the library only when the code is executed - you don't need to edit the story,html file to include a "src =" statement...
NOTE: I am loading the library from the on-line code repository unpkg.com and, by default I load the latest version. In a production environment it is always safer to load a specific version in case changes to the latest version messes your code up!
This bit inside the fillForm function uses the pdf-lib code to define a new pdf document instance PDFDocument and then uses a fetch command to read the pdf template file 'NotesTemplate.pdf' into a buffer array - i.e. it just reads it as a set of binary bytes. It then uses the pdf-lib 'load' method to create a pdf document pdfDoc and then the .getform method to read the pdf form data (fields etc.) from this pdf into a form called 'form'
//Read pdf form
const { PDFDocument } = PDFLib;
const formUrl = 'NotesTemplate.pdf';
const formPdfBytes = await fetch(formUrl).then (res => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formPdfBytes);
const form = pdfDoc.getForm();
NOTE: the pdf template 'NotesTemplate.pdf' in this code is read from the root directory - so you will have to copy it to the root (where the story.html file is) after you have published the course - you could put it on a server with public access to the file...Now you can create variables for each fieldname you want to fill in within the form (you need to know what fieldnames you have created in the pdf form template - in this case LName, LTitle and LNotes)
//Get field names
const nameField = form.getTextField('LName');
const titleField = form.getTextField('LTitle');
const notesField = form.getTextField('LNotes');You can then write your variables into the the newly created pdf form:
//Fill in form
nameField.setText(learnerName);
titleField.setText(learnerTitle);
notesField.setText(learnerNotes);The rest of the code converts the form back to a byte array and then uses another JavaScript library 'tiny-save-as' which is, again called dynamically. The file is output as a binary 'blob'.
I've attached the story file and the NotesTemplate.pdf form file. Just publish to the web - copy the pdf template into the root folder and it should work.
NOTE: you can modify the pdf template and add all sorts of stuff in it without changing your JavaScript code - as long as you don't change the field names. And, yes, it could be a multi-page document.
I know this looks daunting if your JavaScript is not so good - but, trust me, it is a lot easier than the previous methods I have used for multi-page pdf's.
- YvonneUrra-B206Community Member
Thank you for sharing this method, John! It has greatly enriched my process for exporting course participant entries into a PDF file.
Paired with this thread with solutions posed by Steve Gannon (https://community.articulate.com/discussions/articulate-storyline/pdf-publishable-certification-checklist), I've been able to "export" both TextEntry and CheckBox data to PDF.
- SaraBean-da7305Community Member
Hi everyone. I read the notes on this forum extensively (thank you all!) and have talked to John to work out a bug I was having and LOVE this functionality. I'm including a link to a resource that I made for my team showing how to do this. I'm a senior instructional designer at New York Life and often train my team on different bits and bobs of the technical aspects of the job so I put this little thing together for them and held a short training session.
I'll put a link to download the .story and the PDF file in the comments on Review.
https://360.articulate.com/review/content/3b3e97ba-fa9a-4dcd-a378-aba59cde4898/review
- RobSkeetCommunity Member
This is big news and a welcome addition. It would be wonderful if SL could incorporate this into upcoming versions.
I take it, there is no way to publish this in Review correct?
My project needs to be launched in a SCORM package on an LMS. Are there any issues with launching that way? assuming you add the NotesTemplate.pdf to the root of the zip?
- Dave-RuckleyCommunity Member
When you go to publish to review there's an option for "Publish locally for manual upload". That'll generate a zip file that you can add the PDF template to and then in Review you can upload the file using the option at the top left of the page.
Worked perfectly for me and has made creating PDF's so much easier!
- JohnCooper-be3cCommunity Member
Hi Sherri
The problem is in the pdf file template. Sorry, my fault I should have formatted it correctly.
All you need to do is open the pdf template you are using in Adobe Acrobat - select Prepare Form and right click on the field you want to left justify. Then select Properties and then Options:
You can then click the Multi-line setting - this should left justify the field when it is filled in and allow for text wrapping....
- SherriSagers-caCommunity Member
That works perfectly! Thanks so much for the help on how to edit the form in Adobe, too... I wouldn't have figured that out without your help, either. You're awesome!
- AnnemarieReb048Community Member
Hi John,
That sounds amazing. Can you show how you set up the example or provide the source file? I'm looking for the exact functionality that you are showing here!
Thanks!
- SherriSagers-caCommunity Member
And one last thing! ... for anyone who needs an uneditable PDF...
I didn't want our learners to be able to edit any of the fields (like their scores, etc.) after printing, so I found that I could make the PDF uneditable by "flattening" it. It just takes one line of code right before the code to save the file:
form.flatten();
- RobSkeetCommunity Member
You are a hero among heroes!
How do you go about formatting the PDF to add more fields?
(Yes, I liked your LinkedIn Page!)
- JohnCooper-be3cCommunity Member
Hi Rob
Glad you liked this demo. Thanks for viewing the LinkedIn page.
Adding new fields to the PDF and JavaScript is pretty simple. Just start with the NotesTemplate.pdf - open it with Adobe Acrobat (there are probably other pdf form editors but I only use Acrobat). Then select the "Prepare Form" option. You can now resize the boxes - edit the text - add new fields and even add additional pages if you wish. (see screenshot attached)
When you add new fields you will need to give them a name - make a note of the fields you add. e.g newtemplateField
Now add the text entry or variables you need to capture in Storyline - again note the field names. e.g newstoryField
Then edit the JavaScript - very simple...
var player = GetPlayer();
learnerName = player.GetVar("Name");
learnerTitle = player.GetVar("Title");
learnerNotes = player.GetVar("Notes");newJSField = player.GetVar("newstoryField");
This grabs your new Storyline variable from Storyline and puts it in a JavaScript variable...
Then add the pdf template fields you have added to this section of code that gets the field names from the pdf template:
//Get field names
const nameField = form.getTextField('LName');
const titleField = form.getTextField('LTitle');
const notesField = form.getTextField('LNotes');const newField = form.getTextField('newtemplateField');
Then just add you new variable when you fill the form out:
//Fill in form
nameField.setText(learnerName);
titleField.setText(learnerTitle);
notesField.setText(learnerNotes);newField.setText(newJSField);
Simple! If I've got that right - I always need to check which is the pdf field name, which is the Storyline field name, which is the JS variable I'm using to store the Storyline variable and which is the JS variable I'm using to point to the field in the template.... But once you have your head around that it's simple :)
- IestynRimmerCommunity Member
Hi,
I have followed the steps above but am getting an issue with CORS with it trying to gain access to the local file - No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Would this be fixed by hosting the file on a cloud storage system or is there something else that I'm missing?
Thanks
- JohnCooper-be3cCommunity Member
Hi lestyn
Sorry not to reply earlier.
Yes. I should have made the point that the Storyline example I loaded "NoteTaker.story" doesn't work if you compile it as a web app and then run it on your 'local' device. The problem is precisely as you describe - you get a CORS error.
It's a little ironic really that you cannot access a local data file from JavaScript because of a cross origin resource sharing error (unless that file is being served by a web server on the same system)... Basically it is a security issue. The browser executes the JavaScript and sends an 'Origin Header' with any 'fetch' or HTTP request. If the server's response doesn't include the appropriate CORS header, the browser will not allow the request and throws the CORS error you saw.
If you think about - it would be a little unsafe if I could just execute JavaScript code to access any data file on the local system!
If you load the Storyline output onto a web page on another server (as per my demo) everything will be fine.
There are services like Ngrok you can use to test your code locally if necessary.