Embedding Objects in PDF using Google Apps Script

Gists

This is a sample script for embedding the objects in PDF using Google Apps Script.

Recently, I had a situation where it is required to manage PDFs using Google Apps Script. At that time, I had a situation where it is required to embed objects of texts and images in PDF using Google Apps Script. So, I created the following Class with Google Apps Script. When this Class is used, the objects of texts and images can embed in PDF.

Class EmbedObjects

In this Class, pdf-lib is loaded and the objects of texts and images are embedded in a PDF with pdf-lib. When you test the following sample scripts, first, please copy and paste this Class to the script editor of Google Apps Script. The sample scripts call this Class.

/**
 * ### Description
 * This is a Class object for embedding objects into PDF using Google Apps Script.
 *
 * Author: Tanaike ( https://tanaikech.github.io/ )
 */
class EmbedObjects {
  /**
   * ### Description
   * Constructor of this class.
   *
   * @return {void}
   */
  constructor() {
    this.cdnjs = "https://cdn.jsdelivr.net/npm/pdf-lib/dist/pdf-lib.min.js"; // or "https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"
    this.cdnFontkit =
      "https://unpkg.com/@pdf-lib/fontkit/dist/fontkit.umd.min.js";
    this.loadPdfLib_();
  }

  /**
   * ### Description
   * Embed objects into PDF using pdf-lib.
   *
   * @param {String} id File ID of PDF file. And, when you give the file ID of Google Docs files (Document, Spreadsheet, Slide), those are automatically converted to PDF data. You can also use it.
   * @param {Object} object Objects for embedding.
   * @return {Object} Blob of PDF including the updated PDF.
   */
  run(id, object) {
    if (!id || id == "") {
      throw new Error(
        "Please set the file ID of PDF file. And, when you give the file ID of Google Docs files (Document, Spreadsheet, Slide), those are automatically converted to PDF data. You can also use it."
      );
    }
    if (!object || typeof object != "object") {
      throw new Error("Please an object for embeddig the objects.");
    }
    return new Promise(async (resolve, reject) => {
      try {
        const { updatedObject, customFontCheck } = this.updateObject_(object);
        const blob = DriveApp.getFileById(id).getBlob();
        const pdfData = await this.PDFLib.PDFDocument.load(
          new Uint8Array(blob.getBytes())
        );
        const numberOfPages = pdfData.getPageCount();
        const pdfDoc = await this.PDFLib.PDFDocument.create();
        if (customFontCheck) {
          this.loadFontkit_();
          pdfDoc.registerFontkit(this.fontkit);
        }
        const pages = await pdfDoc.copyPages(
          pdfData,
          [...Array(numberOfPages)].map((_, i) => i)
        );
        for (let i = 0; i < pages.length; i++) {
          const page = pages[i];
          const forPage = updatedObject[`page${i + 1}`];
          if (forPage) {
            for (let j = 0; j < forPage.length; j++) {
              const o = forPage[j];
              if (o.imageFileId) {
                const image = await pdfDoc[o.method](o.imageBytes);
                if (o.scale) {
                  const updatedImage = image.scale(o.scale);
                  o.width = o.width || updatedImage.width;
                  o.height = o.height || updatedImage.height;
                } else {
                  o.width = o.width || image.width;
                  o.height = o.height || image.height;
                }
                delete o.imageBytes;
                delete o.method;
                delete o.imageFileId;
                delete o.scale;
                page.drawImage(image, o);
              } else if (o.text) {
                if (o.standardFont || o.customFont) {
                  o.font = await pdfDoc.embedFont(
                    o.standardFont
                      ? this.PDFLib.StandardFonts[o.standardFont]
                      : o.customFont
                  );
                }
                page.drawText(o.text, o);
              }
            }
          }
          pdfDoc.addPage(page);
        }
        const bytes = await pdfDoc.save();
        const newBlob = Utilities.newBlob(
          [...new Int8Array(bytes)],
          MimeType.PDF,
          `new_${blob.getName()}`
        );
        resolve(newBlob);
      } catch (e) {
        reject(e);
      }
    });
  }

  /**
   * ### Description
   * Load pdf-lib. https://pdf-lib.js.org/docs/api/classes/pdfdocument
   *
   * @return {void}
   */
  loadPdfLib_() {
    eval(
      UrlFetchApp.fetch(this.cdnjs)
        .getContentText()
        .replace(
          /setTimeout\(.*?,.*?(\d*?)\)/g,
          "Utilities.sleep($1);return t();"
        )
    );
  }

  /**
   * ### Description
   * Load fontkit. https://github.com/Hopding/fontkit
   *
   * @return {void}
   */
  loadFontkit_() {
    eval(UrlFetchApp.fetch(this.cdnFontkit).getContentText());
  }

  /**
   * ### Description
   * Update object.
   *
   * @param {Object} object Object for embedding objects into PDF.
   * @return {void}
   */
  updateObject_(object) {
    let customFontCheck = false;
    const updatedObject = Object.fromEntries(
      Object.entries(object).map(([k, p]) => [
        k,
        p.map((e) => {
          if (e.imageFileId) {
            const imageBlob = DriveApp.getFileById(e.imageFileId).getBlob();
            const imageBytes = new Uint8Array(imageBlob.getBytes());
            const mimeType = imageBlob.getContentType();
            let method;
            if (mimeType == MimeType.PNG) {
              method = "embedPng";
            } else if (mimeType == MimeType.JPEG) {
              method = "embedJpg";
            } else {
              throw new Error("This image file cannot be used.");
            }
            e.method = method;
            e.imageBytes = imageBytes;
          } else if (e.text) {
            if (e.customFont) {
              customFontCheck = true;
              e.customFont = new Uint8Array(e.customFont.getBytes());
            }
          }
          return e;
        }),
      ])
    );
    return { updatedObject, customFontCheck };
  }
}

Limitations

In the current stage, the objects of texts and images can be embedded in a PDF.

Preparation

Before you test this script, please prepare a sample PDF. The image of the sample PDF is as follows.

In this sample PDF, there are 2 red squares and 1 blue square. In this sample script, the text and image objects are embedded into the red square and the blue square, respectively. The sample script is as follows.

Embed texts and image into PDF

function sample() {
  const pdfFileId = "### file ID of PDF file ###"; // Or, when you use the file ID of Google Docs files (Document, Spreadsheet, Slide), those files are automatically converted to the PDF format.
  const object = {
    page1: [
      {
        text: "sample text1",
        x: 150,
        y: 635,
        standardFont: "Helvetica",
        size: 30,
      },
      {
        text: "sample text2",
        x: 390,
        y: 602,
        standardFont: "TimesRoman",
        size: 16,
      },
      {
        imageFileId: "### file ID of image file ###",
        x: 175,
        y: 340,
        scale: 0.35,
      },
    ],
  };
  const folderId = "### folder ID ###"; // New PDF is created in this folder.
  const EO = new EmbedObjects();
  EO.run(pdfFileId, object)
    .then((res) => DriveApp.getFolderById(folderId).createFile(res))
    .catch((err) => console.log(err));
}

When this script is run to the above sample PDF, the following result is obtained.

Note

  • If you want to use the custom font, please use { text: "sample text", x: 100, y: 100, customFont: DriveApp.getFileById("### file ID of font file ###").getBlob(), size: 24 } in object.

  • You can use the standard font from here.

  • About the option of text, you can see the official document of pdf-lib. Ref

  • About the option of image, you can see the official document of pdf-lib. Ref

  • In this script, pdf-lib and fontkit are loaded in the Class object. Of course, in this case, you can also use the pdf-lib library and the script of fontkit by copying and pasting the script library retrieved from https://cdn.jsdelivr.net/npm/pdf-lib/dist/pdf-lib.min.js and https://unpkg.com/@pdf-lib/fontkit/dist/fontkit.umd.min.js in the script editor of Google Apps Script, respectively. In this case, the process cost for loading it can be reduced.

 Share!