Using jsPDF from html in sandbox

Hello,

I’m having some trouble, and a quick overview of the problem:

I need to create a printable packing slip from an app I’ve created.

Using the utils.downloadPage() doesn’t work, because it turns the entire page into a single-page PDF, and for any packing slip with more than 20 items (in this case, the items are very small, so one box may fit hundreds of unique items) cannot be split into multiple pages - it can only be shrunk to fit a page, making the text unreadable.

I attempted to use the markdown pdf exporter as a native query type, but, even with markdown tables, it’s pretty useless as there is little to no control over the formatting and the users have said they’d rather create packing lists by hand.

So, in an effort to create a multi-page PDF that can be appropriately printed and have formatting necessary for a packing slip, I added jsPDF, along with html2canvas and dompurify as required for HTML conversion in the libraries section. So far so good, and a basic plain-text PDF succeeds using this.

However, trying to use the HTML to PDF conversion function generates a chain of javascript errors in the console, specifically all related to:

Uncaught (in promise) DOMException: Blocked a frame with origin "null" from accessing a cross-origin frame.

This comes from html2canvas. I’m assuming this is from html2canvas attempting to write to a canvas in a sandbox.

Here is a minimal example this triggers this condition:

const doc = new jsPDF();

var html = '<h1>Title.</h1><p>A paragraph</p>';

doc.html(html, {
  callback: function (doc) {
    utils.downloadFile(doc.output(), 'testpdf', 'pdf');
  }
});

Is there any way you guys know of that I could work around this, or if there is another way to resolve the printing issues discussed above?

I’ve worked around the issue by using a 3rd-party REST API for generating PDFs for the time being. However, I’m struck with a new challenge, that is potentially easier to resolve:

I can retrieve PDF as base64 from the API, and display it in the PDF viewer component.

However, attempting to save it into a file (for printing, etc.) results in a corrupted file. That is, it’s a PDF, but it’s not the data displayed in the pdf viewer.

I’ve tried the following methods of converting the base64 to a binary file and downloading, all using the utils.downloadFile() method:

utils.downloadFile(atob(pdfCrowdWriter.data.base64Binary), 'testpdf', 'pdf');

The second method, is to use a standalone Buffer library, and then do:

utils.downloadFile(Buffer.from(pdfCrowdWriter.data.base64Binary, 'base64').toString('utf8'), 'testfoo5', 'pdf');

However, in both methods, the binary data in the saved file is markedly different than when downloaded directly from the API (via cUrl), and is visually broken when loading.

Is there a better way for me to convert and save the data that will result in a non-corrupted PDF file when coming from base64 encoded data?

Hi @church!

What is the lay out of the ~20 items? Are the separate components? Does the pdf need to be an exact copy of the how the items are displayed in Retool, or are you looking for the data to be displayed otherwise?

In terms of your returned base64 data, if you use a base64 data -> pdf (outside of Retool), does this give you the results you would expect?

Hi Ben,

The items are in a listview, with multiple text components per item. I'm ok with it being an exact copy, but being to use an html string would provide some better formatting for print options - I can go either way there.

With the base64 data coming from a pdf API, it shows properly within the PDF viewer component in retool, and when downloaded as raw base64 text, it decodes properly using CLI tools (e.g. base64 -d), and shows the PDF as expected. However, when using utils.downloadFile() with the binary string (whether using atob() or Buffer to convert from base64 to binary), I'm finding that there are byte-wise differences in the output after downloading the file, from one either downloaded raw as binary from the API, or downloaded as base64 (from retool) and converted locally.

e.g., when comparing the two files (good and bad):

Thanks!

Hi there, any thoughts on how I could effectively save a PDF via retool? If I could find some way to save or even just print the PDF I’m able to display in the PDF viewer, that would be perfect for us.

I was able to find a workaround, long story short:

  1. Use a custom component
  2. Set ‘allow popups to escape sandbox’ on for the custom component
  3. Provide an HTML template for your PDF in the model
  4. Add a button, call it download PDF or something
  5. Add an onclick handler for the button that uses jspdf to convert the html template to a PDF (using html() method)
    5.1. In the success callback from jspdf.html(), call window.open(doc2.output(‘bloburl’)); (where doc2 is the name of your jspdf object)

Now, the file looks good, has page breaks where you need them, and can be printed or saved from the new window.

3 Likes

I was able to use Carbone.io to create a PDF and download it. The “file” that I get from carbone is a JS object which I believe is fairly standard:

{   base64Binary: "JVBERi0xLjYK...", 
    fileName: "Whatever", 
    fileType: "pdf"
}

I have to pass this entire object to the utils.downloadFile() method:

restGetInvoicePDF.trigger({
    additionalScope: {
  	    renderId: data.data.renderId 
    },
    onSuccess: function(data) {
        utils.downloadFile(data, tblOrders.selectedRow.data.invoice_id,"pdf")
    }
})

Just passing the base64 did not work.

My problem is trying to print it directly from Retool. I would rather not save it and just print it. You seem to have away to get around the sandbox so I can open a page with just the PDF in the browser and can print it from there. But I have not dealt with customer components yet so I have some lernin’ to do.

If you would be willing to share your custom component that would be awesome!

My problem is trying to print it directly from Retool. I would rather not save it and just print it. You seem to have away to get around the sandbox so I can open a page with just the PDF in the browser and can print it from there. But I have not dealt with customer components yet so I have some lernin’ to do.

A feature for custom components which was just released today should help with printing from custom components:
image

This toggle adds allow-modals to the sandbox property of the iframe, allowing you to use any of the following features to produce modal dialogs:

  • window.alert()
  • window.confirm()
  • window.print()
  • window.prompt()
  • the beforeunload event

Of interest in your use case will probably be the window.print() method.

1 Like

Hello, can you share more technical details?

I have done the steps but in the window.open command my custom control gives an error.

how @church would u like to tell me how did u add jsPDF ?

I have added in libraries but it is not working ....

and I am using it here

It says jsPDF is not defined

This must be done in a custom component.

Here is a fully fleshed out module with all of the code working you can download and add to your account:

1 Like

What do u mean by custom component @bradlymathews

the script is written in button's click

A Custom Component is a method for building your own component with HTML/JS or React when Retool does not have the functionality you need, like creating PDFs.

1 Like