[GAS ライブラリ]GASでPDFを分割しよう

GASでPDFを分割しようGAS

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

前回のおさらい

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

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

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

前回の要件定義と、ダメ出しを、そのまま生成AIにぶち込みます。

このようなプロンプトです。

結合はうまくいきました。それでは、これから分割をやります。
以下の結合についての要件定義を使って、分割についての要件定義を出力してください。

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

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

Geminiは、箇条書きの要件定義を出力しましたが、Chat GPT 4oは、より詳しい要件定義を出力しました。

これは、Chat GPT 4oの方を、採用しようとおもいます。

📄 PDF分割機の要件定義
✅ 基本要件
分割したいPDF(何枚でも可)を「PDF分割機」フォルダに投入する

ユーザーは任意のPDFファイルを指定のフォルダへアップロードします。
「PDF分割機」フォルダにPDFファイルがあるか確認する

フォルダ内にPDFが存在しない場合は処理をスキップし、ログに出力する。
各PDFファイルを1ページごとに分割する

分割対象のPDFファイルの全ページを取得し、各ページを新しいPDFとして保存する。
分割後のファイルを「分割済み」フォルダに保存する

ページごとにファイルを保存し、元のファイル名にページ番号を付与する。
元のPDFファイルは「処理済み」フォルダに移動する

分割が完了したファイルは、再処理防止のために「処理済み」フォルダへ移動する。
📂 フォルダ構成と管理
PDF分割機フォルダ: 分割対象のPDFファイルを格納するフォルダ
分割済みフォルダ: 分割後のPDFファイルを保存するフォルダ
処理済みフォルダ: 処理が完了した元のPDFファイルを保管するフォルダ
注意: フォルダは名前ではなくIDで管理する。

⚙️ 実装上の注意点
フォルダはIDで取得:
DriveApp.getFolderById() を使用してフォルダを取得する。

非同期処理は for...of で実装:
async/await を適切に機能させるため、forEach() は使用せず for...of ループを採用する。

エラーハンドリング:
try-catch を関数全体でラップせず、必要な箇所に限定して使用する。

ファイル名の付与:
分割後のファイル名には、元のファイル名とページ番号を組み合わせた形式を使用する。
例: Invoice_Page_1.pdf, Invoice_Page_2.pdf など。

ファイル名の取得:
PDFのタイトルが存在する場合は getTitle() を使用し、存在しない場合はファイル名を利用する。
データ取得時は new Uint8Array() を使用して正しい形式に変換する。

コメントの付与:
コード内には簡潔でわかりやすいコメントを記述し、関数にはドキュメンテーションコメントを追加する。

📦 ファイル名のルール
元のファイル名: Report.pdf
分割後のファイル名:
Report_Page_1.pdf
Report_Page_2.pdf
Report_Page_3.pdf ...
または、getTitle() で取得したタイトルがある場合は、タイトルを優先します。

🚀 処理フロー
フォルダIDの取得

PropertiesService から PDF_SPLITTER_FOLDER_ID, SPLIT_FOLDER_ID, PROCESSED_FOLDER_ID を取得する。
PDFファイルの取得

DriveApp.getFolderById() を使用し、PDFファイルのリストを作成。
PDFファイルのロード

各ファイルを Uint8Array に変換して PDFLib.PDFDocument.load() で読み込む。
ページごとの分割

copyPages() を使用して各ページを個別のPDFに複製し、新しいPDFとして保存。
ファイル名の作成

getTitle() から取得するか、元のファイル名+ページ番号で命名する。
分割ファイルの保存

DriveApp.createFile() で「分割済み」フォルダに保存。
元ファイルの移動

処理完了後、「処理済み」フォルダにファイルを移動。
ログの出力

各ステップで進捗ログを出力して、トラブルシューティングを容易にする。
📋 サンプルファイルの処理例
入力フォルダ: PDF分割機
ファイル: Contract.pdf (全3ページ)
処理後:

出力フォルダ: 分割済み

Contract_Page_1.pdf
Contract_Page_2.pdf
Contract_Page_3.pdf
元ファイル: Contract.pdf → 処理済み フォルダへ移動

これで、結合機の要件定義をベースにした分割機の要件定義が完成しました! 🚀

フォルダの準備

まずは、結合のときと同様に、処理を行うフォルダを作成します。 

そして、プロパティストアにフォルダIDを格納するコードを準備しましょう。

//分割機
 const PDF_SPLITTER_FOLDER_ID = "あなたのフォルダID";  // PDF分割機フォルダのID
 properties.setProperty("PDF_SPLITTER_FOLDER_ID", PDF_SPLITTER_FOLDER_ID);

 const SPLIT_FOLDER_ID = "あなたのフォルダID";      // 分割済みフォルダのID
 properties.setProperty("SPLIT_FOLDER_ID", SPLIT_FOLDER_ID);

 const PROCESSED_SPLIT_FOLDER_ID = "あなたのフォルダID"; // 分割後の処理済みフォルダのID
 properties.setProperty("PROCESSED_SPLIT_FOLDER_ID", PROCESSED_SPLIT_FOLDER_ID);

できあがったコード

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

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

/**
 * 📄 PDF分割機
 * 指定フォルダ内のPDFを1ページごとに分割し、分割済みフォルダに保存する。
 * 元のPDFファイルは処理済みフォルダへ移動する。
 */
async function splitPdfs() {
  const properties = PropertiesService.getScriptProperties();

//フォルダIDを取得する
 const pdfSplitterFolderId = properties.getProperty("PDF_SPLITTER_FOLDER_ID");
 const splitFolderId = properties.getProperty("SPLIT_FOLDER_ID");
 const processedFolderId = properties.getProperty("PROCESSED_SPLIT_FOLDER_ID");

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

//フォルダオブジェクトを取得する
 const pdfSplitterFolder = DriveApp.getFolderById(pdfSplitterFolderId);
 const splitFolder = DriveApp.getFolderById(splitFolderId);
 const processedFolder = DriveApp.getFolderById(processedFolderId);

//フォルダ内にあるすべてのPDFファイルを取得する
 const pdfFiles = pdfSplitterFolder.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配列をfor of文で回しながら、PDFをloadしながらcopyPageしながらaddPageする
 for (const file of pdfFileList) {
   const blob = file.getBlob();
   const bytes = blob.getBytes();
   const uint8Array = new Uint8Array(bytes);

   try {
     const pdfDoc = await PDFLib.PDFDocument.load(uint8Array);
     const totalPages = pdfDoc.getPageCount(); //PDFのページ数を取得しておく

    //先頭のPDFファイル名を取得しておく
     const fileTitle = pdfDoc.getTitle() || file.getName().replace(/\.pdf$/i, "");

     console.log(`📄 ${fileTitle} のページ数: ${totalPages}`);

    //分割後のファイル名に連番を振るためにイテレータが扱いやすいfor文を採用
     for (let i = 0; i < totalPages; i++) {
       const newPdf = await PDFLib.PDFDocument.create(); //空のPDFファイルを用意しておく
       const [page] = await newPdf.copyPages(pdfDoc, [i]);
       newPdf.addPage(page);

       const pdfBytes = await newPdf.save(); //.save()メソッドはUint8Array形式を返す
       const pageNumber = i + 1;

       const splitFileName = `${fileTitle}_Page_${pageNumber}.pdf`;
       const splitBlob = Utilities.newBlob(pdfBytes, MimeType.PDF, splitFileName);//Blobを生成する

       splitFolder.createFile(splitBlob);//Blobからファイルを作成する
       console.log(`✅ ${splitFileName} を保存しました`);
     }

    //元のファイルを処理済みフォルダへ移動
     file.moveTo(processedFolder);
     console.log(`🛠️ ${file.getName()} を処理済みフォルダへ移動しました`);

   } catch (error) {
     console.error(`❌ ${file.getName()} の分割に失敗しました:`, error);
   }
 }

 console.log("🎉 すべてのPDFを分割しました");
}

実行してみる

まず、分割用の元ファイルを、準備しましょう。(前回結合したPDFでいいと思います。) 

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

分割済みフォルダに、分割済みPDFが完成していれば成功です!ファイル名に連番も付与されていて、バッチリですね。 

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

時限トリガー

時限トリガーも設置しましょう。前回の結合を参照ください。割愛します。

まとめ

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

いかがでしたでしょうか。PDF-LIBの分割メソッドは、何ページから何ページまでを分割する、などの高度な設定も可能です。

みなさんが、退屈なPDFの結合や分割から解放されて、価値のある働き方ができることを願っています。

このシリーズの目次

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