David Fear
David Fear

Reputation: 33

Google Apps Script - use Google Drive pdf with PDF-lib.js

Background:
I am try to fill in a pdf file that:
a) has form fields.
b) that is stored on Google Drive.

with data that is stored in Google spreadsheet.

I am using:

  1. Google Apps Script.
  2. HtmlService
  3. PDF-lib.js in a htmlOutput object generated from a htmlTemplate.

The work flow is:

  1. The showModalDialog_downloadFilledPDFform_NAMOFFORM() function is called from a menu.

The function is:

function showModalDialog_downloadFilledPDFform_NAMOFFORM() {
  var pdf_template_file_url = getPDFfileURL("form1.pdf");
  var htmlTemplate = HtmlService.createTemplateFromFile('downloadFilledPDFformHTML');
  
  htmlTemplate.dataFromServerTemplate = { pdf_template_file: "form1.pdf", pdf_template_file_url: pdf_template_file_url };
  
  var htmlOutput = htmlTemplate.evaluate();
  htmlOutput.setWidth(648.1);
  htmlOutput.setHeight(286.300)
  SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Download filled PDF');
}

The url that is passed to the htmlTemplate is generated by: "fillPDFForm.gs"

function: fillPDFForm.gs:

var pdfFileNamesAndIDs = [ ]
  pdfFileNamesAndIDs.push(["form1.pdf", "1y8F5NgnK50mdtWSR6v1b8pELsbbBJert"])
  pdfFileNamesAndIDs.push(["form2.pdf", "1B4BOaI-BqFmhmnFx7FaT-yys-U0vkYKz"])
  pdfFileNamesAndIDs.push(["form3.pdf", "17LrJpRA5oBZBqw-2du1H74KxWIX55qYC"])

function getPDFfileURL(fileName) {
  var documentID = "";
  for (var i in pdfFileNamesAndIDs) {
    //console.log(pdfFileNamesAndIDs[i][0]);
    if (pdfFileNamesAndIDs[i][0] == fileName) {
      documentID = pdfFileNamesAndIDs[i][1];
      console.log("documentID: " + documentID);
    }
  }

  var documentFile = DriveApp.getFileById(documentID);
  var documentURL = documentFile.getDownloadUrl();
  Logger.log("documentURL = "+documentURL);

  return documentURL;
}

The Problem:
The URL generated by getPDFfileURL() doesn't work in the html file generated in showModalDialog_downloadFilledPDFform_NAMOFFORM().

The error in Chrome dev console is:

[email protected]:15 Uncaught (in promise) Error: Failed to parse PDF document (line:0 col:0 offset=0): No PDF header found
    at e [as constructor] ([email protected]:15:189222)
    at new e ([email protected]:15:190065)
    at e.parseHeader ([email protected]:15:401731)
    at e.<anonymous> ([email protected]:15:400782)
    at [email protected]:15:1845
    at Object.next ([email protected]:15:1950)
    at [email protected]:15:887
    at new Promise (<anonymous>)
    at i ([email protected]:15:632)
    at e.parseDocument ([email protected]:15:400580)

The basic concept for the html page (shown as a modal dialog box), came from: https://jsfiddle.net/Hopding/0mwfqkv6/3/

The contents of the htmlTemplate are:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">

    <!-- Add Stylesheet -->
    <?!= HtmlService.createHtmlOutputFromFile('downloadFilledPDFformCSS').getContent(); ?>

    <!-- Add pdf-lib and downloadjs libraries -->
    <!-- https://pdf-lib.js.org/ and https://github.com/rndme/download -->
    <!-- https://jsfiddle.net/Hopding/0mwfqkv6/3/ -->
    <script src="https://unpkg.com/[email protected]"></script>
    <script src="https://unpkg.com/[email protected]"></script>
  </head>

  <body>
    <h2 id="myTitle"></h2>
    <p>Click the button to fill form fields in an the following PDF document: <code id="pdf_template_file">pdf-lib</code></p>
    <button onclick="fillForm()">Fill PDF</button>
    <p class="small">(Your browser will download the resulting file)</p>
  </body>

  <script>
      const data = <?!= JSON.stringify(dataFromServerTemplate) ?>; //Stores the data directly in the javascript code

      function removeExtension(filename) {
        return filename.substring(0, filename.lastIndexOf('.')) || filename;
      }
      const pdf_template_file = data.pdf_template_file;
      const pdf_template_file_basename = removeExtension(pdf_template_file);

      // sample usage
      function initialize() {
          document.getElementById("myTitle").innerText = pdf_template_file;
          //or use jquery:  $("#myTitle").text(data.first + " - " + data.last);
          document.getElementById("pdf_template_file").innerText = pdf_template_file;
      }

      // use onload or use jquery to call your initialization after the document loads
      window.onload = initialize;
  </script>


  <script>
    const { PDFDocument } = PDFLib;

    async function fillForm() {
        // Fetch the PDF with form fields
      const formUrl = data.pdf_template_file_url
      //const formPdfBytes = await fetch(formUrl).then(res => res.arrayBuffer())
      const formPdfBytes = await fetch(formUrl, {
        redirect: "follow",
        mode: 'no-cors',
        method: 'GET',
        headers: {
          'Content-Type': 'application/pdf',
        }
      }).then(res => res.arrayBuffer());

      // Load a PDF with form fields
      const pdfDoc = await PDFDocument.load(formPdfBytes);

      // Get the form containing all the fields
      const form = pdfDoc.getForm()

      // Get all fields in the PDF by their names

      const invIDField = form.getTextField('invID')
      const makeAndModelField = form.getTextField('makeAndModel')
      const nameField = form.getTextField('name')
      const addressField = form.getTextField('address')
      const phoneNumberField = form.getTextField('phoneNumber')
      const emailAddressField = form.getTextField('emailAddress')
      const dateField = form.getTextField('date')

      // Output file name
      const INPUT_FNAME = "AN"
      const INPUT_LNAME = "Other"

      // Fill in the basic info fields
      invIDField.setText()
      makeAndModelField.setText()
      nameField.setText(INPUT_FNAME + " " + INPUT_LNAME)
      addressField.setText()
      phoneNumberField.setText()
      emailAddressField.setText()
      dateField.setText()

      // Serialize the PDFDocument to bytes (a Uint8Array)
      const pdfBytes = await pdfDoc.save({updateFieldAppearances: false})

      const outputPDFfilename = pdf_template_file_basename + "." + INPUT_FNAME + "_" + INPUT_LNAME + ".pdf"

            // Trigger the browser to download the PDF document
      download(pdfBytes, outputPDFfilename, "application/pdf");
    }
  </script>
</html>

I have replicated the contents of the html file on my testing webserver. The server has 3 files: index.html, stykesheet.css and form1.pdf

The pdf (on the web server) is the same pdf file that is stored on Google drive.

On my server the following works:

  1. if I use the pdf file that is in the same folder as the html and css files, a filled pdf is offered for download.

...but the following doesn't work:

  1. if I use the same URL that is generated by getPDFfileURL(), nothing happens and no filled pdf is offered for download.

So the question is:
How do I generate the correct URL (for the pdf file stored in Google Drive), so it can then be used by PDF-lib.js (in the htmlTemplate)?

Upvotes: 1

Views: 1787

Answers (1)

David Fear
David Fear

Reputation: 33

The answer is:

  1. fetch the pdf file from Google drive as raw bytes.
  2. encode the bytes as base64.
  3. pass the base64 string from the GAS function to the htmlTemplate.

This is the function that gets the pdf file and returns it as a base64 encoded string:

function getPDFfileAsBase64() {
  var fileId = "";
  var url = "https://drive.google.com/uc?id=" + fileId + "&alt=media";
  console.log("url: " + url);

  var params = {
    method: "get",
    headers: {
      Authorization: "Bearer " + ScriptApp.getOAuthToken(),
    },
  };
  var bytes = UrlFetchApp.fetch(url, params).getContent();
  var encoded = Utilities.base64Encode(bytes);
  //console.log("encoded: " + encoded);

  return encoded;
}

the function to create a modal dialog is:

function showModalDialog_downloadFilledPDFform_form1() {
  // Display a modal dialog box with custom HtmlService content.

  var pdf_template_file = "form1.pdf";
  var pdf_template_file_AsBase64 = getPDFfileAsBase64(pdf_template_file);
  var htmlTemplate = HtmlService.createTemplateFromFile('downloadFilledPDFformHTML');
  
  htmlTemplate.dataFromServerTemplate = { pdf_template_file: pdf_template_file,  pdf_template_file_AsBase64: pdf_template_file_AsBase64};
  
  var htmlOutput = htmlTemplate.evaluate();
  htmlOutput.setWidth(648.1);
  htmlOutput.setHeight(286.300)
  SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Download filled PDF');
} 

Upvotes: 1

Related Questions