javascriptgoogle-apps-scriptpdfweb-applicationspagination

How to add automatic page numeration when a table spans over several pages


I have an HTML in Google App Scripts web app that contains a table that might span over several pages, and I want to add page numeration at the bottom right of each page, something like "Page 3/4". The page numeration should include the current page and the total amount of pages.

This is a simple test code that reproduces the issue.

Script:

function doPost(e) {
  try {
    const template = HtmlService.createTemplateFromFile('test');
    const tableData = [];
    for (i = 0; i < 100; i ++) { // 100 could be any number
      tableData.push({ code: 1, product: 'shampoo'});
    }
    template.tableRows = tableData;
    const htmlOutput = template.evaluate().getContent();
    const pdfBlob = Utilities.newBlob(htmlOutput, 'text/html').getAs('application/pdf');
    pdfBlob.setName('some.pdf');
    const pdfBytes = pdfBlob.getBytes();
    const pdfBase64 = Utilities.base64Encode(pdfBytes);
    console.log(pdfBase64)
    return ContentService.createTextOutput(JSON.stringify({ pdf: pdfBase64 })).setMimeType(ContentService.MimeType.JSON);
  } catch (error) {
    console.error(error)
    return ContentService.createTextOutput().setMimeType(ContentService.MimeType.JSON);;
  }
}

test.html:

<!DOCTYPE html>
<html>

<head>
  <base target="_top">
</head>
<style>
  @page {
    size: A4;
    margin: 0;
    counter-increment: page;
  }

  .page-footer {
    position: fixed;
    bottom: 0;
    right: 0;
    margin: 10px;
    font-size: 10px;
  }

  .page-footer::after {
    content: counter(page);
  }
</style>
<body>
  <table>
    <thead>
      <tr>
        <th>Code</th>
        <th>Product</th>
      </tr>
    </thead>
    <tbody>
      <? tableRows.forEach(function (item) { ?>
      <tr>
        <td>
          <?= item.code ?>
        </td>
        <td>
          <?= item.product ?>
        </td>
      </tr>
      <? }); ?>
    </tbody>
    <div class="page-footer"></div>
</body>

</html>

This code produces a "0" at the bottom right at the end of each page, instead of the expected page numeration (i.e. 1, 2, 3, etc...).

enter image description here

How can I add automatic page numeration in this process of PDF generation from an html template?


Solution

  • Converting HTML to PDF with Page Numbers

    Here is a workaround! I tried your code and encountered a problem, I researched CSS - Footer Page Counter and it seems not to be working.

    I also modified for loop in your code, added some variables with limit per page andDriveApp where the pdf will be saved. I also included pdf merger Merging PDF Files in Google Drive using Google Apps Script.


    Sample Output:

    Sample 1

    sample 2

    Sample 3


    Code.gs

    const folderId = "--Folder Id --";
    
    async function combinePDFs(childFolderId) {
      const parentFolder = DriveApp.getFolderById(folderId);
      const childFolder = DriveApp.getFolderById(childFolderId);
      const files = childFolder.getFiles();
    
      // Merge PDFs.
      const cdnjs = "https://cdn.jsdelivr.net/npm/pdf-lib/dist/pdf-lib.min.js";
      eval(UrlFetchApp.fetch(cdnjs).getContentText()); // Load pdf-lib
      const setTimeout = function (f, t) {
        Utilities.sleep(t);
        return f();
      }
      const pdfDoc = await PDFLib.PDFDocument.create();
      while (files.hasNext()) {
        const file = files.next();
        Logger.log(file.getName() + ' ' + file.getMimeType());
        if (file.getMimeType() === 'application/pdf') {
          const pdfData = await PDFLib.PDFDocument.load(new Uint8Array(file.getBlob().getBytes()));
          const pages = await pdfDoc.copyPages(pdfData, [...Array(pdfData.getPageCount())].map((_, i) => i).sort());
          pages.forEach(page => pdfDoc.addPage(page));
        }
      }
      const bytes = await pdfDoc.save();
    
      // Create a PDF file.
      parentFolder.createFile(Utilities.newBlob([...new Int8Array(bytes)], MimeType.PDF, "Merged.pdf"));
      childFolder.setTrashed(true);
    }
    
    function doPost() {
      try {
        const parentFolder = DriveApp.getFolderById(folderId);
        const createChildFolder = parentFolder.createFolder("TempFiles").getId();
        const template = HtmlService.createTemplateFromFile('test');
        let tableData = [];
        const limit = 20; // Limit per page.
        const totalData = 100; // Total Data.
        const equation = totalData - Math.round((limit * (totalData / limit)));
        let page = Math.round(totalData / limit) + 1;
        for (i = 1; i < totalData + 1; i++) { 
          tableData.push({ code: 1, product: 'shampoo' });
          if (i % limit === 0) {
            createPdf()
          } else if (i > (totalData - equation) && tableData.length === equation) {
            createPdf();
          }
        }
    
        function createPdf() {
          template.tableRows = tableData;
          template.pages = page -= 1;
          const htmlOutput = template.evaluate().getContent();
          const pdfBlob = Utilities.newBlob(htmlOutput, 'text/html').getAs('application/pdf');
          pdfBlob.setName(`some${page}.pdf`);
          uploadFile(pdfBlob, createChildFolder);
          tableData = []
          console.log(page);
        }
        combinePDFs(createChildFolder)
    
    
      } catch (error) {
        console.error(error)
        return ContentService.createTextOutput().setMimeType(ContentService.MimeType.JSON);;
      }
    }
    
    function uploadFile(pdf, childFolder) {
      const folder = DriveApp.getFolderById(childFolder);
      const file = folder.createFile(pdf);
      file.setDescription("Temp Pdf");
      console.log("File Uploaded!");
    }
    

    test.html

    <!DOCTYPE html>
    <html>
    
    <head>
      <base target="_top">
    </head>
    <style>
      @page {
        size: A4;
        margin: 0;
      }
    
      .page-footer {
        position: fixed;
        bottom: 0;
        right: 0;
        margin: 10px;
        font-size: 10px;
      }
    </style>
    
    <body>
      <table>
        <thead>
          <tr>
            <th>Code</th>
            <th>Product</th>
          </tr>
        </thead>
        <tbody>
          <? tableRows.forEach(function (item) { ?>
          <tr>
            <td>
              <?= item.code ?>
            </td>
            <td>
              <?= item.product ?>
            </td>
          </tr>
          <? }); ?>
        </tbody>
        <div class="page-footer">
          <?= pages ?>
        </div>
    </body>
    
    </html>
    

    References: