Google Apps ScriptでPDFを操作する（PDF-LIB）

[PDF-LIB](https://pdf-lib.js.org/)を使えば、Google Apps ScriptでPDFを操作できます。Apache Licence 2.0のオープンソースです。ポイントは、PDF-LIBのコードからApps Scriptで使えない部分を削除することと、Googleドライブで扱える形式に変換すること、です。

ただし、日本語フォントで文字を入力する方法は分かりませんでした。

これから、PDF-LIBをコピーしたコード、PDF-LIBを使ってGoogleドライブから読み書きするコード、動作をテストするコード、をそれぞれ書いていきます。

# PDF-LIBのコードからApps Scriptで使えない部分を削除する

最初に、PDF-LIBのソースコードをコピーして、Apps Scriptから使えるようにします。まず、Googleドライブ上でApps Scriptのファイルを作ります。右クリックしてMoreからGoogle Apps Scriptを選びましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/a6e37e6f-311a-edfe-fe17-1a97d92b32d4.png)

作ったファイルを開いて、スクリプトエディターを開き、新しいスクリプトを追加します。Filesの右にあるプラスボタンからスクリプトを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/2ed71ccb-7fd7-d874-2eeb-88391b4e96ea.png)

ファイル名は何でもいいのですが今回は`PdfLib`にしましょう。`PdfLib.gs`として保存されます。

次に、PDF-LIBのソースコードを`PdfLib.gs`に貼り付けます。`function myFunction { }`と書いてあるのは消します。そして、 https://unpkg.com/pdf-lib/dist/pdf-lib.min.js をタブで開き、これをすべてコピーして`PdfLib.gs`に貼り付けます。

ここからが重要です。`PdfLib.gs`上で1つだけ文字列を置換します。以下の変換前の文字列を空白にします。

| 変換前 | 変換後 |
|---|---|
| On(e.start,"options.start",[[Object,"{ x: number, y: number }"]]),On(e.end,"options.end",[[Object,"{ x: number, y: number }"]]), | | 

`Ctrl + H`で文字列を置換できます。置換したら、`Code.gs`を保存しましょう。正しく保存できればOKです。なお、コードの量が多いため、保存には少々時間がかかります。

# Googleドライブで扱える形式に変換する

次に、PDFファイルをGoogleドライブに保存したり、Googleドライブから読み込めるよう、コードを追加します。

さらにスクリプトエディターでスクリプトを追加します。今回は`Pdf`にしましょう。`Pdf.gs`として保存されます。`Pdf.gs`に次のコードを記述します。

```javascript:Pdf.gs
// Apps ScriptにはsetTimeoutがないので作っておく。
function setTimeout(func, delayMillis) {
  func();
}

/**
 * PDFDocumentを生成する。PDFLib.PDFDocument.create()の代わりに使う。
 * @returns {PDFLib.PDFDocument} PDFDocument
 */
async function createPdfDocument() {
  return await PDFLib.PDFDocument.create();
}

async function getBlob_(pdfDoc) {
  const base64String = await pdfDoc.saveAsBase64();
  const data = Utilities.base64Decode(base64String);
  return Utilities.newBlob(data).setContentType(MimeType.PDF);
}

/**
 * PDFDocumentをGoogleドライブに保存する。PDFLib.PDFDocument.saveAsBase64()の代わりに使う。
 * @param {PDFLib.PDFDocument} pdfDoc PDFDocument
 * @param {String} filename オプション。ファイル名。デフォルトは unnamed.pdf。
 * @param {String} folderId オプション。保存先のフォルダーID。
 * @returns {DriveApp.File} DriveApp.File
 */
async function savePdfDocument(pdfDoc, filename = 'unnamed.pdf', folderId) {
  const blob = await getBlob_(pdfDoc);
  if (filename) blob.setName(filename);
  if (folderId) {
    return DriveApp.getFolderById(folderId).createFile(blob);
  }
  return DriveApp.createFile(blob);
}

/**
 * PDFDocumentをGoogleドライブから読み込む。PDFLib.PDFDocument.load()の代わりに使う。
 * @param {String} fileId GoogleドライブのファイルID。
 * @returns {PDFLib.PDFDocument} PDFLib.PDFDocument
 */
async function loadPdfDocument(fileId) {
  const blob = DriveApp.getFileById(fileId).getBlob();
  const bytes = blob.getBytes();
  const base64String = Utilities.base64Encode(bytes); 
  return await PDFLib.PDFDocument.load(base64String);
}

/**
 * 指定したGoogleドライブのファイルをPDFDocumentで上書きする。
 * @param {String} fileId 上書きするファイルのファイルID。
 * @param {PDFLib.PDFDocument} pdfDoc PDFLib.PDFDocument
 * @returns {DriveApp.File} DriveApp.File
 */
async function updatePdfDocument(fileId, pdfDoc) {
  const blob = await getBlob_(pdfDoc);
  return Drive.Files.update({ uploadType: 'media' }, fileId, blob);
}
```

# 動作するか確認する

最後に、`Code.gs`に以下のコードを書き込んで、`test()`を実行しています。結果としてマイドライブにPDFファイルが生成されます。Googleドライブへの保存、読み出し、上書きをそれぞれテストしています。

Googleドライブの操作以外は、[PDF-LIBのAPI](https://pdf-lib.js.org/docs/api/)をご覧ください。

```javascript:Code.gs
async function test() {
  // PDFLib.PDFDocument.create()の代わり。
  const pdfDoc = await createPdfDocument();

  // PDF-LIB APIを使う。1ページ目に文字と線を描く。
  const page = pdfDoc.addPage();
  const w = page.getWidth();
  const h = page.getHeight();
  page.drawText('Your First PDF File!', { x: 10, y: h / 2 });
  page.drawLine({
    start: { x: 10, y: h / 2 },
    end: { x: w - 20, y: h / 2 }
  });

  // PDFLib.PDFDocument.saveAsBase64()の代わり。
  const file = await savePdfDocument(pdfDoc, 'test.pdf');
  console.log(`PDFファイルを作成しました ${file.getUrl()}`);

  // PDFLib.PDFDocument.load()の代わり。
  const pdfDoc2 = await loadPdfDocument(file.getId());

  // PDF-LIB APIで2ページ目に文字を描く。
  const page2 = pdfDoc2.addPage();
  page2.drawText('The Next Page!', { x: 10, y: h / 2 });

  // ファイルに上書きする。
  await updatePdfDocument(file.getId(), pdfDoc2);
  console.log(`PDFファイルを更新しました ${file.getUrl()}`);
}
```

なお、async, awaitのつけ忘れには注意が必要です。
- PDF-LIBのAPIには、呼び出すときに`await`が必要なものがあります。`await`を呼び出す関数は、`async function`として定義する必要があります。
- 例えば`test()`は`async function test() {`として定義しています。
- `test()`の中で、`createPdfDocument()`、`savePdfDocument()`、`loadPdfDocument()`、`updatePdfDocument()`にそれぞれ`await`をつけていることに注意してください。

# 参考にしたドキュメント

https://officeforest.org/wp/2018/11/25/google-apps-script%E3%81%A7pdf%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B/#PDF-8

https://pdf-lib.js.org/