[GAS ライブラリ]GASでPDFを結合しよう

GASでPDFを分割しようGAS

どうも。Kenny(tsujikenzo)です。このシリーズでは、 「GASでPDFを分割・結合しよう」 を全3回でお送りします。今日は2回目です。

前回のおさらい

前回は、PDF-LIBの紹介と、下準備をしました。

今回は、GASでPDFを結合しよう、をお届けします。

全体的な流れ(要件定義)

PDF-LIBは、単ページを取得したり、ページーズ(単ページの配列)を取得したり、ページインデックスを取得できたり、さまざまなことができます。

なので、効率の良い、早い、おおまかな処理の流れを決めてしまいましょう。

  1. 結合したいPDF(何枚あってもOK)を「PDF結合機」というフォルダに投入する
  2. 「PDF結合機」フォルダにPDFファイルがあるか確認する
  3. あれば、結合用の空のPDFファイルを作る
  4. 「PDF結合機」フォルダにあるすべてのPDFを配列に格納する
  5. 各PDFファイルのすべてのページをコピーしながら、マージPDFにページを追加していく
  6. for文で回す
  7. 結合後のファイルに名前を付けて「結合済み」フォルダに保存する
  8. 元のPDFファイルは「処理済み」フォルダに移動する
  9. 1.を時限トリガーでぶん回す

この要件定義(なんちゃって)を、生成AIにぶっこみます

今回は、ChatGPT 4oが、8割ぐらい使えるコードを出力しましたので、それを採用します。

残りの2割は、どこがダメだったのか、メモを書いておきます。

  • フォルダ名でフォルダを掴んでいたので、IDから掴むように
  • forEach()をfor of文に(forEachはasync/awaitが正しく扱えない)
  • 全体的に囲むtryCatchは嫌い
  • 結合後のファイル名にgetTitle()から取得したファイル名を
  • ファイル名を取得するときは、new Uint8Array()から
  • 後で読めるように簡潔にコメントを付ける
  • ドキュメンテーションコメントも付ける

ダメ出しと言いますが、わたしの要件定義がそうなってますもんね。AIは悪くないです。。。ごめんなさい。

できあがったコード

以下が完成したコードです。まずはGoogle Driveクラスや、Fileクラスで、ファイル操作の処理を行っていますね。

そして、PDFを取得したあとは、PDF-LIBのメソッドを呼び出しながら処理を行っています。

/** 結合専用フォルダにあるPDFファイルをすべて結合して移動する関数
 * @return {void} フォルダに保存するので戻り値はありません
 */
async function mergePdfs() {

//フォルダIDを取得する
 const pdfMergeFolderId = PropertiesService.getScriptProperties().getProperty("PDFMERGERFOLDER_ID");
 const mergedFolderId = PropertiesService.getScriptProperties().getProperty("MERGED_FOLDER_ID");
 const processedFolderId = PropertiesService.getScriptProperties().getProperty("PROCESSED_FOLDER_ID");

//フォルダIDが無いばあいのガード節
 if (!pdfMergeFolderId || !mergedFolderId || !processedFolderId) {
   console.error("❌ フォルダIDが見つかりません");
   return;
 }

//フォルダオブジェクトを取得する
 const pdfMergeFolder = DriveApp.getFolderById(pdfMergeFolderId);
 const mergedFolder = DriveApp.getFolderById(mergedFolderId);
 const processedFolder = DriveApp.getFolderById(processedFolderId);

//フォルダ内にあるすべてのPDFファイルを取得する
 const pdfFiles = pdfMergeFolder.getFilesByType(MimeType.PDF);

//空の配列を用意しておく
 const pdfFileList = [];

//配列にPDFファイルを格納する
 while (pdfFiles.hasNext()) {
   pdfFileList.push(pdfFiles.next());
 }

//フォルダにPDFがなかったときのガード節
 if (pdfFileList.length === 0) {
   console.log("📁 PDF結合機フォルダにPDFがありません");
   return;
 }

//結合するPDFファイル数をカウントしてログ出力
 console.log(`🔹 結合するPDF数: ${pdfFileList.length}`);

//空のPDFファイルを用意しておく
 const mergedPdf = await PDFLib.PDFDocument.create();

//先頭のPDFファイル名を取得しておく
 const firstFileName = pdfFileList[0].getName().replace(/\.pdf$/i, "");

//PDF配列をfor of文で回しながら、PDFをloadしながらcopyPageしながらaddPageする
 for (const file of pdfFileList) {
   const blob = file.getBlob();
   const bytes = blob.getBytes();
   const uint8Array = new Uint8Array(bytes); // ✅ Uint8Array に変換

   try {
     const pdfDoc = await PDFLib.PDFDocument.load(uint8Array);
     const pages = await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
     pages.forEach(page => mergedPdf.addPage(page));

     console.log(`✅ ${file.getName()} のページを追加しました`);

    //元のファイルを処理済みフォルダに移動する
     file.moveTo(processedFolder);
     console.log(`🛠️ ${file.getName()} を処理済みフォルダへ移動しました`);
   } catch (error) {
     console.error(`❌ ${file.getName()} の読み込みに失敗しました:`, error);
   }
 }

//.save()メソッドはUint8Array形式を返す
 const pdfBytes = await mergedPdf.save();

/先頭のファイル名と日付を結合後のファイル名として使用
 const formattedDate = Utilities.formatDate(new Date(), "JST", "yyyyMMdd_HHmmss");
 const mergedFileName = `結合済み_${formattedDate}_${firstFileName}.pdf`;

//blobを生成する
 const mergedBlob = Utilities.newBlob(pdfBytes, MimeType.PDF, mergedFileName);

//blobからPDFファイルを作成する
 mergedFolder.createFile(mergedBlob);
 console.log(`✅ ${mergedFileName} を保存しました`);

 console.log("🎉 処理完了");
}

AIすごいですね。

実行してみる

まず、各フォルダと結合用の元ファイルを、準備しましょう。 

mergePdfs()を実行します。手で

結合済みフォルダに、結合済みPDFが完成していれば成功です!(感動しますね) 

文字化けや、ページの欠けなどもなく、結合できているようですね。 

時限トリガー

社内でさまざまな運用ルールがあると思いますが、弊社ではPDFを結合する担当者は1人のため、複数人がアクセスするということがありません。

なので、mergePdfs()を、5分に1度実行する時限トリガーを設置して完了です。

トリガーの設置方法は、割愛しますからね。 

まとめ

以上で、「GASでPDFを結合しよう」をお送りしました。

次回は、最終回で「GASでPDFを分割しよう」をお届けします。お楽しみに。

このシリーズの目次

タイトルとURLをコピーしました