どうも。Kenny(tsujikenzo)です。このシリーズでは、 「GASでPDFを分割・結合しよう」 を全3回でお送りします。今日は2回目です。
前回のおさらい
前回は、PDF-LIBの紹介と、下準備をしました。
今回は、GASでPDFを結合しよう、をお届けします。
全体的な流れ(要件定義)
PDF-LIBは、単ページを取得したり、ページーズ(単ページの配列)を取得したり、ページインデックスを取得できたり、さまざまなことができます。
なので、効率の良い、早い、おおまかな処理の流れを決めてしまいましょう。
- 結合したいPDF(何枚あってもOK)を「PDF結合機」というフォルダに投入する
- 「PDF結合機」フォルダにPDFファイルがあるか確認する
- あれば、結合用の空のPDFファイルを作る
- 「PDF結合機」フォルダにあるすべてのPDFを配列に格納する
- 各PDFファイルのすべてのページをコピーしながら、マージPDFにページを追加していく
- for文で回す
- 結合後のファイルに名前を付けて「結合済み」フォルダに保存する
- 元のPDFファイルは「処理済み」フォルダに移動する
- 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を分割しよう」をお届けします。お楽しみに。