Generate PDF from HTML (improved community script)

Hi, I followed this tutorial but I noticed that it caused some problems, this is because in the code written by @church sometimes we entered an infinite loop, this is because when the input changed, the comparison between the old and the new input was not done correctly, we were comparing a raw html string against a formatted html string coming from the innerHTML method, it follows that the formatting, the tabulations, can be different, with the consequence that the comparison fails, entering an infinite loop and causing quite a few problems.

this is the code I added:

 temporaryTemplateContents = document.getElementById("temporaryTemplateContents");
 temporaryTemplateContents.innerHTML = model.content; 
 temporaryTemplateCSS = document.getElementById("temporaryTemplateCSS");
 temporaryTemplateCSS.innerHTML = model.css; 

and this is the modification on the comparison:

 if(startingContent == temporaryTemplateContents.innerHTML && startingCSS == temporaryTemplateCSS.innerHTML) { return; }

this is the code in full:

<html>
<head>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
       <!-- put any fonts you need here -->
  <style>
      <!-- here is where you'll need to style your html for printing, especially if you're also displaying it natively inside of your retool app -->
   
  </style>
</head>
<body>
   <script src="https://cdn.tryretool.com/js/react.production.min.js" crossorigin></script>
   <script src="https://cdn.tryretool.com/js/react-dom.production.min.js" crossorigin></script>
       <!-- get the HTML to PDF bundle -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js"></script>

<script>
const injectCSS = css => {
  let el = document.createElement('style');
  el.type = 'text/css';
  el.innerText = css;
  document.head.appendChild(el);
  return el;
};

var retoolModel;

// subscribe to model changes
window.Retool.subscribe(function(model) {
 if (!model) { return }
 retoolModel = model;

 var startingContent = document.getElementById("templateContents").innerHTML;
 var startingCSS = document.getElementById("templateCSS").innerHTML;

 // new logic for better comparation -- added by Panacci Karim at 16 October 2024 21:44 GMT+2 
 temporaryTemplateContents = document.getElementById("temporaryTemplateContents");
 temporaryTemplateContents.innerHTML = model.content; 
 temporaryTemplateCSS = document.getElementById("temporaryTemplateCSS");
 temporaryTemplateCSS.innerHTML = model.css; 
  
  
 //if the content has not changed, take no action. This prevents the component from entering an endless loop.
 if(startingContent == temporaryTemplateContents.innerHTML && startingCSS == temporaryTemplateCSS.innerHTML) { return; }
  // move our printed content into an element we can access later 
 document.getElementById("templateContents").innerHTML = model.content; 
 document.getElementById("templateCSS").innerHTML = model.css;
 injectCSS(model.css);
 pdprint();
});

function pdprint() {
     // our function to generate and display the pdf in a new window
   var opt = {
      margin:       0.5,
      filename:     "raw.pdf", //This doesn't matter since we're not actually downloading the file; we'll name the file when using utils.downloadFile with the base64 binary output of this component
      image:        { type: 'jpeg', quality: 0.98 },
      html2canvas:  { scale: 2 },
      jsPDF:        { unit: 'in', format: 'letter', orientation: 'portrait' }
  };

  window.html2pdf().set(opt)
    .from(document.getElementById("templateContents").innerHTML)
    .outputPdf() 
    .then(function(pdfas) {  
                              retoolModel.pdf_blob = btoa(pdfas);
                              window.Retool.modelUpdate(retoolModel);
                            } );
 }
</script>
  <!-- ... This stores the HTML content that we used to generate the PDF -->
  <div id="templateContents" style="display:none"></div>
  <div id="templateCSS" style="display:none"></div>
  <div id="temporaryTemplateContents" style="display:none"></div>
  <div id="temporaryTemplateCSS" style="display:none"></div>
 </body>
</html>
10 Likes

Welcome to the community, @karimpanacci! :wave: This is definitely appreciated - thanks for sharing.

2 Likes

Nice work! This seems quite cool.

2 Likes

Hi, we are using a pretty powerful and easy to implement solution for PDF generation : we run jsreport dockerized in the cloud and send html templates and data objects via http request to generate the pdfs. All for free (except the cloud hosting of course)

2 Likes

Here is an idea... What if you generated your report as a docx using docxtemplater (it's great!) and then saved those to pdf?

I know it's another step, but being able to define the layout in Word, makes generating the document more efficient.

Example

GetDocxTemplate.trigger({ // This is a Retool Storage query
  onSuccess: (data) => {
    const zip = new PizZip(data.base64Data, { base64: true });
    const doc = new docxtemplater(zip, {
      paragraphLoop: true,
      linebreaks: true,
    });
    
    doc.render(TemplateData.value); // Transformer with my template data
    
    const blob = doc.getZip().generate({
      type: "blob",
      mimeType:
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      compression: "DEFLATE",
    });
    
    utils.downloadFile(blob, `${OutputFilename.value}.docx`); // Text field in app
  }
});

you'll need to add to your libraries as well
image