Creating PDF Forms from Google Slide Template using Google Apps Script

Gists

This is a sample script for creating PDF forms from a Google Slide template using Google Apps Script.

Recently, I had a situation where it is required to create a custom PDF form. In that case, I thought that when a PDF form can be created from a template, it might be useful. So, I created the following Class with Google Apps Script. When this Class is used, a custom PDF form can be easily created from a Google Slide as a template.

Class CreatePdfFormBySlides

In this Class, pdf-lib is loaded and PDF form is created 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 creating a PDF Form using a Google Slide as a template using Google Apps Script.
 *
 * Author: Tanaike ( https://tanaikech.github.io/ )
 */
class CreatePdfFormBySlides {
  /**
   * ### Description
   * Constructor of this class.
   *
   * @param {Object} obj Object for setting of custom font.
   * @return {void}
   */
  constructor(obj = {}) {
    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.loadPdfLib_();
    this.standardFont = null;
    this.customFont = null;

    if (obj.standardFont && typeof obj.standardFont == "string") {
      this.standardFont = obj.standardFont;
    } else if (obj.customFont && obj.customFont.toString() == "Blob") {
      this.customFont = obj.customFont;
      this.cdnFontkit = "https://unpkg.com/@pdf-lib/fontkit/dist/fontkit.umd.min.js";
      this.loadFontkit_();
    }
  }

  /**
   * ### Description
   * Convert Google Slide to PDF FOrm using pdf-lib.
   *
   * @param {String} id File ID of Google Slide template.
   * @param {Object} object Object for setting.
   * @return {Object} Blob of PDF including the created PDF Form.
   */
  run(id, object) {
    if (!id || id == "") {
      throw new Error("Please set the file ID of Google Slide including the template for PDF Form.");
    }
    if (!object || !Array.isArray(object) || object.length == 0) {
      throw new Error("Please set the file ID of Google Slide including the template for PDF Form.");
    }
    return new Promise(async (resolve, reject) => {
      try {
        const { obj, blob } = this.getObjectFromSlide_(id, object);
        const pdfDoc = await this.PDFLib.PDFDocument.create();
        const form = pdfDoc.getForm();
        if (this.standardFont || this.customFont) {
          await this.setCustomFont_(pdfDoc, form);
        }
        const pdfData = await this.PDFLib.PDFDocument.load(new Uint8Array(blob.getBytes()));
        const numberOfPages = pdfData.getPageCount();
        const pages = await pdfDoc.copyPages(pdfData, [...Array(numberOfPages)].map((_, i) => i));
        const xAxisOffset = 0.5;
        const yAxisOffset = 0.5;
        for (let i = 0; i < numberOfPages; i++) {
          const pageNumber = i + 1;
          const page = pdfDoc.addPage(pages[i]);
          const pageHeight = page.getHeight();
          const yOffset = pageHeight;
          // const form = pdfDoc.getForm();
          obj[i].forEach((v, k) => {
            if (k == "checkbox") {
              v.forEach(t => {
                t.forEach(u => {
                  const checkbox = form.createCheckBox(u.title);
                  checkbox.addToPage(page, { x: u.leftOffset - xAxisOffset, y: yOffset - u.topOffset - u.height + yAxisOffset, width: u.width, height: u.height });
                  this.setStyles_(checkbox, u);
                });
              });
            } else if (k == "radiobutton") {
              v.forEach((t, kk) => {
                const radio = form.createRadioGroup(`radiobutton.${kk}.page${pageNumber}`);
                t.forEach(u => {
                  radio.addOptionToPage(u.title, page, { x: u.leftOffset - xAxisOffset, y: yOffset - u.topOffset - u.height + yAxisOffset, width: u.width, height: u.height });
                  this.setStyles_(radio, u);
                });
              });
            } else if (k == "textbox") {
              v.forEach(t => {
                t.forEach(u => {
                  const textBox = form.createTextField(u.title);
                  textBox.addToPage(page, {
                    x: u.leftOffset - xAxisOffset,
                    y: yOffset - u.topOffset - u.height + yAxisOffset,
                    width: u.width,
                    height: u.height,
                  });
                  this.setStyles_(textBox, u);
                });
              });
            } else if (k == "dropdownlist") {
              v.forEach(t => {
                t.forEach(u => {
                  const drowdown = form.createDropdown(u.title);
                  drowdown.addToPage(page, {
                    x: u.leftOffset - xAxisOffset,
                    y: yOffset - u.topOffset - u.height + yAxisOffset,
                    width: u.width,
                    height: u.height
                  });
                  this.setStyles_(drowdown, u);
                });
              });
            }
          });
        }
        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
   * Get PDF document object using pdf-lib.
   *
   * @param {Object} blob Blob of PDF data by retrieving with Google Apps Script.
   * @return {Object} PDF document object using pdf-lib.
   */
  async getPdfDocFromBlob_(blob) {
    if (blob.toString() != "Blob") {
      throw new error("Please set PDF blob.");
    }
    return await this.PDFLib.PDFDocument.load(new Uint8Array(blob.getBytes()), { updateMetadata: true });
  }

  /**
   * ### Description
   * Set styles to the field of PDF form.
   *
   * @param {Object} instance Instance of field.
   * @param {Object} u Methods and values for setting the styles.
   * @return {Object} Object for creating the fields of PDF Form.
   */
  setStyles_(instance, u) {
    if (u.description.methods && u.description.methods.length > 0) {
      u.description.methods.forEach(({ method, value }) => instance[method](value || null));
    }
  }

  /**
   * ### Description
   * Get an object for creating the fields of PDF Form from Google Slide.
   *
   * @param {String} id File ID of Google Slide template.
   * @return {Object} Object for creating the fields of PDF Form.
   */
  getObjectFromSlide_(id, object) {
    const inputObj = object.reduce((o, e) => (o[e.shapeTitle] = e, o), {});
    const slide = SlidesApp.openById(id);
    const slides = slide.getSlides();
    const ar = slides.map((s, i) =>
      s.getShapes().reduce((ar, e) => {
        const t = e.getTitle().trim();
        if (t && inputObj[t]) {
          const page = i + 1;
          const [type, group, name] = t.split(".").map(f => f.trim());
          let setMethods = inputObj[t];
          if (type == "radiobutton" && inputObj[t] && inputObj[t].methods && inputObj[t].methods.length > 0) {
            const temp = JSON.parse(JSON.stringify(inputObj[t]));
            temp.methods.forEach(f => {
              if (f.method == "select") {
                f.value = `${f.value}.page${page}`;
              }
            });
            setMethods = temp;
          }
          ar.push({
            title: `${t}.page${page}`,
            page,
            type,
            group,
            name,
            shape: e,
            topOffset: e.getTop(),
            leftOffset: e.getLeft(),
            width: e.getWidth(),
            height: e.getHeight(),
            description: setMethods,
          });
        }
        return ar;
      }, [])
    );

    const duplicateCheckObj = ar.reduce((m, e) => {
      e.forEach(({ title }) => m.set(title, m.has(title) ? m.get(title) + 1 : 1));
      return m;
    }, new Map());
    const duplicateCheck = [...duplicateCheckObj].filter(([_, v]) => v > 1);
    if (duplicateCheck.length > 0) {
      temp.setTrashed(true);
      throw new Error(`Duplicate titles were found. The duplicated titles are "${duplicateCheck.map(([k]) => k).join(",")}".`);
    }

    ar.forEach(page => page.forEach(({ shape }) => shape.remove()));
    slide.saveAndClose();

    const obj = ar.map(d => d.reduce((m, e) => m.set(e.type, m.has(e.type) ? [...m.get(e.type), e] : [e]), new Map()));
    obj.forEach(page =>
      page.forEach((v, k, m) => m.set(k, v.reduce((mm, e) => mm.set(e.group, mm.has(e.group) ? [...mm.get(e.group), e] : [e]), new Map())))
    );
    const blob = DriveApp.getFileById(id).getBlob();
    return { obj, blob };
  }

  /**
   * ### Description
   * Set the custom font to PDF form.
   *
   * @param {Object} pdfDoc Object of PDF document.
   * @param {Object} form Object of the PDF form.
   * @return {void}
   */
  async setCustomFont_(pdfDoc, form) {
    let customfont;
    if (this.standardFont) {
      customfont = await pdfDoc.embedFont(this.PDFLib.StandardFonts[this.standardFont]);
    } else if (this.customFont) {
      pdfDoc.registerFontkit(this.fontkit);
      customfont = await pdfDoc.embedFont(new Uint8Array(this.customFont.getBytes()));
    }

    // Ref: https://github.com/Hopding/pdf-lib/issues/1152
    const rawUpdateFieldAppearances = form.updateFieldAppearances.bind(form);
    form.updateFieldAppearances = function () {
      return rawUpdateFieldAppearances(customfont);
    };

  }
}

Limitations

  • In the current stage, the fields of the textbox, the checkbox, the dropdown list, and the radio button of the PDF Form can be created.

Preparation

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

In the template slide, please set shapes and please set the shape titles. The shape title is required to be the unique value in the slide. Please be careful about this. The following script converts the template shapes to the fields of the PDF form using the shape titles. The flow of this is as follows.

  1. Create a new Google Slide.

  2. Put texts and shapes on the slide.

    • Texts are used as the text in PDF.
    • Shapes are converted to the fields in PDF form.
  3. Set the ID to the shape title (Alt Text).

  • There is a rule regarding the format of ID. Please check the section “Rule of shape title (Alt Text)”.

Rule of shape title (Alt Text)

The format of ID (shape title (Alt Text)) is as follows.

{field type}.{group name}.{field name}

When the above sample Google Slide template is used, the IDs of fields of PDF form are as follows.

  • Textbox
    • textbox.sample1.sample1
    • textbox.sample1.sample2
  • Checkbox
    • checkbox.sample2.checkbox1
    • checkbox.sample2.checkbox2
    • checkbox.sample2.checkbox3
  • Dropdown
    • dropdownlist.sample4.sample1
  • Radiobutton
    • radiobutton.sample5.radiobutton1
    • radiobutton.sample5.radiobutton2
    • radiobutton.sample5.radiobutton3

In the case of radiobutton.sample5.radiobutton1, radiobutton is a type of field. sample5 is a group of fields. radiobutton1 is a unique name of the group. radiobutton.sample5 has 3 fields of radiobutton.sample5.radiobutton1, radiobutton.sample5.radiobutton2, and radiobutton.sample5.radiobutton3.

Create a PDF form from the Google Slide template with the default font

function sample1() {
  const templateId = "###"; // Please set the file ID of your Google Slide template.

  const object = [
    {
      shapeTitle: "textbox.sample1.sample1",
      methods: [{ method: "setText", value: "sample text1" }],
    },
    {
      shapeTitle: "checkbox.sample2.checkbox1",
      methods: [{ method: "enableRequired" }],
    },
    {
      shapeTitle: "checkbox.sample2.checkbox2",
      methods: [{ method: "enableRequired" }, { method: "check" }],
    },
    {
      shapeTitle: "checkbox.sample2.checkbox3",
      methods: [{ method: "enableRequired" }, { method: "check" }],
    },
    {
      shapeTitle: "textbox.sample1.sample2",
      methods: [
        { method: "setText", value: "sample text2" },
        { method: "enableMultiline" },
        { method: "setFontSize", value: "12" },
      ],
    },
    {
      shapeTitle: "dropdownlist.sample4.sample1",
      methods: [
        {
          method: "setOptions",
          value: [
            "sample option1",
            "sample option2",
            "sample option3",
            "sample option4",
            "sample option5",
          ],
        },
        { method: "enableEditing" },
        { method: "enableMultiselect" },
        { method: "select", value: "sample option3" },
      ],
    },
    {
      shapeTitle: "radiobutton.sample5.radiobutton1",
      methods: [{ method: "enableRequired" }],
    },
    {
      shapeTitle: "radiobutton.sample5.radiobutton2",
      methods: [
        { method: "enableRequired" },
        { method: "select", value: "radiobutton.sample5.radiobutton2" },
      ],
    },
    {
      shapeTitle: "radiobutton.sample5.radiobutton3",
      methods: [{ method: "enableRequired" }],
    },
  ];

  const templateSlide = DriveApp.getFileById(templateId);
  const folder = templateSlide.getParents().next();
  const temp = templateSlide.makeCopy("temp", folder);
  const tempId = temp.getId();
  const CPS = new CreatePdfFormBySlides();
  CPS.run(tempId, object)
    .then((res) => {
      folder.createFile(res);
      temp.setTrashed(true);
    })
    .catch((err) => console.log(err));
}
  • shapeTitle: Shape title (Alt Text).
  • method in methods: Method names of Class PDFCheckBox, PDFDropdown, PDFRadioGroup, and PDFTextField.
  • value in methods: Arguments of the methods of Class PDFCheckBox, PDFDropdown, PDFRadioGroup, and PDFTextField.

When this script is run with the above Google slide template, the following result is obtained.

Create a PDF form from a Google Slide template with a custom font

When the text of the placeholder is set, there is a case that the custom font is required to be used. For example, in Japan, when the Japanese language is required to be used, it is required to use a custom font. In this script, a PDF form is created using the custom function.

function sample2() {
  const templateId = "###"; // Please set the file ID of your Google Slide template.
  const fileIdOfFontFile = "###"; // File ID of ttf and otf file you want to use.

  const object = [
    {
      shapeTitle: "textbox.sample1.sample1",
      methods: [
        { method: "setText", value: "さんぷる テキスト1" },
        { method: "setFontSize", value: "16" },
      ],
    },
    {
      shapeTitle: "checkbox.sample2.checkbox1",
      methods: [{ method: "enableRequired" }],
    },
    {
      shapeTitle: "checkbox.sample2.checkbox2",
      methods: [{ method: "enableRequired" }, { method: "check" }],
    },
    {
      shapeTitle: "checkbox.sample2.checkbox3",
      methods: [{ method: "enableRequired" }, { method: "check" }],
    },
    {
      shapeTitle: "textbox.sample1.sample2",
      methods: [
        { method: "setText", value: "さんぷる テキスト2" },
        { method: "enableMultiline" },
        { method: "setFontSize", value: "12" },
      ],
    },
    {
      shapeTitle: "dropdownlist.sample4.sample1",
      methods: [
        {
          method: "setOptions",
          value: [
            "sample option1",
            "sample option2",
            "オプション3",
            "sample option4",
            "sample option5",
          ],
        },
        { method: "enableEditing" },
        { method: "enableMultiselect" },
        { method: "select", value: "オプション3" },
      ],
    },
    {
      shapeTitle: "radiobutton.sample5.radiobutton1",
      methods: [{ method: "enableRequired" }],
    },
    {
      shapeTitle: "radiobutton.sample5.radiobutton2",
      methods: [
        { method: "enableRequired" },
        { method: "select", value: "radiobutton.sample5.radiobutton2" },
      ],
    },
    {
      shapeTitle: "radiobutton.sample5.radiobutton3",
      methods: [{ method: "enableRequired" }],
    },
  ];

  const templateSlide = DriveApp.getFileById(templateId);
  const folder = templateSlide.getParents().next();
  const temp = templateSlide.makeCopy("temp", folder);
  const tempId = temp.getId();
  const customFont = DriveApp.getFileById(fileIdOfFontFile).getBlob();
  const CPS = new CreatePdfFormBySlides({ customFont });
  CPS.run(tempId, object)
    .then((res) => {
      folder.createFile(res);
      temp.setTrashed(true);
    })
    .catch((err) => console.log(err));
}
  • shapeTitle: Shape title (Alt Text).

  • method in methods: Method names of Class PDFCheckBox, PDFDropdown, PDFRadioGroup, and PDFTextField.

  • value in methods: Arguments of the methods of Class PDFCheckBox, PDFDropdown, PDFRadioGroup, and PDFTextField.

  • In order to use the custom font, please prepare the font you want to use. TTF and OTF files can be used.

  • When the standard font of pdf-lib, please modify the above script as follows. The value can be known at here. For example, when you want to use “TimesRoman”, please set { standardFont: "TimesRoman" }.

    • From

      const CPS = new CreatePdfFormBySlides({ customFont });
      
    • To

      const CPS = new CreatePdfFormBySlides({ standardFont: "###" });
      

When this script is run with the above Google slide template, the following result is obtained. You can see the Japanese characters using the custom font.

Note

  • 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!