時々、業務で巨大なPDFに接する場面があります。昨今では、AIにPDFを解析させる場面もあり、巨大なPDFだと無駄が多いこともあります。そこで、今回は、Rustを使って手軽にPDFを数ページずつに分割するというコマンドラインツールを作ってみましょう。

  • PDFを分割するツールを作ろう - 巨大なPDFを30ページずつ区切ったところ

    PDFを分割するツールを作ろう - 巨大なPDFを30ページずつ区切ったところ

RustからPDFを操作するライブラリ「lopdf」

今回は、PDFを操作するライブラリとして「lopdf」を使ってみましょう。このライブラリを使えば、PDFの生成や結合、編集、分割が手軽にできるようになっています。

  • crates.ioのlopdfのページ

    crates.ioのlopdfのページ

このライブラリ(Rustではクレートと言います)を使って、PDFを分割するツールを作ってみましょう。実際、このライブラリを使えば、PDFを分割するのは簡単です。ここでは、Rustのプロジェクトにクレートを追加する手順や、クレートの使い方を紹介します。

Rustのプロジェクトを作成しよう

最初に、Rustのプロジェクトを作ってみましょう。Rustでは、プロジェクトのひな形を作ったり、クレートを追加したり、コンパイルしたりと、プロジェクト操作の大半の操作を行うのが「cargo」というコマンドです。なお、Rustをインストールすると最初から利用することができます。Rust本体のインストールについては、本連載の1回目を参考にしてください。

ターミナル(WindowsではPowerShell、macOS/Linuxではターミナルアプリ)を起動して、次のコマンドを実行しましょう。

# プロジェクトのフォルダを作成
mkdir split_pdf
# 作業フォルダを移動
cd split_pdf
# プロジェクトを初期化
cargo init

上記のコマンドを実行すると「split_pdf」という名前のプロジェクトフォルダが作成され、Rustのプロジェクトひな形が生成されます。

作成されたフォルダ「split_pdf」の構造を確認すると、下記のようになっています。

<split_pdfフォルダ>
├── Cargo.toml
└── src
    └── main.rs

プロジェクトにクレート「lopdf」を追加しよう

続いて、PDFを操作するためのクレート「lopdf」を追加してみましょう。プロジェクトにクレートを追加する方法は二つあります。一つは、Cargo.tomlに直接、利用したいライブラリを記述する方法、もう一つはcargoコマンドを使う方法です。今回は、cargoコマンドを使って追加してみましょう。

以下のコマンドは、lopdfクレートのバージョン0.44を追加するコマンドです。「cargo add クレート名@バージョン」の書式で記述します。下記のコマンドをターミナルで実行してみましょう。

# lopdfクレートを追加
cargo add lopdf@0.44

なお、このコマンドを実行すると、Rustでプロジェクト管理を行うための設定ファイル「Cargo.toml」に次のような設定が追加されるのを確認できます。

[package]
name = "split_pdf"
version = "0.1.0"
edition = "2024"

[dependencies]
lopdf = "0.44" # ←←← この行が追加されます

そのため、最初から上記のように、Cargo.tomlにlopdfクレートを追加しても問題ありません。

PDF分割プログラムを書こう

それでは、次に、PDF分割を行うプログラムを記述しましょう。メインの処理は、src/main.rsに記述します。まずは、下記のコードをコピーして、src/main.rsの内容を書き換えてください。

以下のコードは、PDFを分割するプログラムです。コマンドライン引数に、PDFファイルのパスと、分割するページ数を指定すると、指定したページ数ごとにPDFを分割して保存します。

use lopdf::Document;
use std::env;

fn main() {
    // コマンドライン引数から「入力ファイル」と「分割ページ数」を取得 --- (※1)
    let args: Vec<String> = env::args().collect();
    let input = &args[1];
    let pages_per_file: u32 = args[2].parse().unwrap();

    // PDFを読み込み、総ページ数を取得 --- (※2)
    let doc = Document::load(input).unwrap();
    let total_pages = doc.get_pages().len() as u32;

    // 指定ページ数ごとに分割して保存 --- (※3)
    let mut start = 1;
    let mut file_no = 1;
    while start <= total_pages {
        // 分割するページ範囲を計算 --- (※4)
        let end = (start + pages_per_file - 1).min(total_pages);

        // 元のPDFを複製し、対象範囲外のページを削除 --- (※5)
        let mut part = doc.clone();
        let remove_pages: Vec<u32> = (1..=total_pages).filter(|p| *p < start || *p > end).collect();
        part.delete_pages(&remove_pages);
        part.prune_objects();

        // ファイル名を生成して保存 --- (※6)
        let output = format!("output_{file_no}.pdf");
        part.save(&output).unwrap();
        println!("{output} を保存しました({start}〜{end}ページ)");

        start = end + 1;
        file_no += 1;
    }
}

プログラムを確認しましょう。上記のコードは、次のような処理を行っています。

(※1)では、コマンドライン引数から入力ファイル名と、何ページずつ分割するかという数値を受け取っています。env::args()は、プログラムを実行するときに指定するコマンドライン引数の一覧をVec型で取り出します。

(※2)では、指定されたPDFファイルを読み込み、そのPDFに全部で何ページあるかを調べます。Document::load(input)でPDFを開き、get_pages()でページ一覧を取得し、その個数を数えることで総ページ数を出しています。

(※3)では、実際にPDFを指定ページ数ごとに分割する本体の処理が始まっています。startは現在どのページから切り出すかを表していて、最初は1ページ目から始まります。file_noは保存するファイルの番号で、output_1.pdf、output_2.pdfのように使われます。while start <= total_pagesという条件によって、開始ページが総ページ数を超えるまで処理を繰り返します。つまりこの部分は、「1つ目のPDFを作る」「次のPDFを作る」という作業を最後まで順番に回していくための仕組みです。

(※4)では、今回作る分割ファイルが何ページ目から何ページ目までになるかを計算しています。開始ページはすでにstartに入っているので、終わりのページであるendを求めます。

(※5)では、元のPDFをいったん複製し、その中から今回必要なページだけを残しています。まずdoc.clone()で元のPDF全体をコピーしています。そのあと、全ページの中から「今回の範囲に入っていないページ」を探して一覧にし、それらをdelete_pages()で削除しています。そして、ページを削除した後で、prune_objects()を呼ぶことで、不要になったオブジェクトを取り除きます。

(※6)では、保存するファイル名を作って、実際にPDFを書き出しています。format!を使ってoutput_1.pdf、output_2.pdfのような連番ファイル名を作り、save()で保存しています。その後のprintln!では、どのファイルに何ページから何ページまでを保存したかを画面に表示しています。これにより、ユーザーは処理の進み具合や保存結果を確認できます。

プログラムを実行してみよう

今回のプログラムとサンプルのPDFを、こちらにアップロードしています。ダウンロードして、先ほど作ったRustのプロジェクトフォルダに置いてください。

サンプルPDFは、350ページあり、各ページには、ページ番号が印字されています。これを30ページずつに分割してみましょう。

# プログラムを実行してPDFを30ページにずつに分割する
cargo run test.pdf 30

次の画面は、上記のコマンドを実行したときの出力例です。30ページずつに分割され、output_1.pdf、output_2.pdf、output_3.pdf、output_4.pdf…と、12個のPDFが作成されます。

  • 350ページのPDFを30ページずつ区切ったところ

    350ページのPDFを30ページずつ区切ったところ

なお、上記のコマンドを実行すると、実行可能ファイルが「target/debug/split_pdf」に作成されます。

改良のヒント

なお、PDFの分割処理を作る場合、今回作ったように、PDF全体を複製してから不要なページを削除する方法と、必要なページだけを新しいPDFにコピーする方法の二つがあります。

後者の方が効率は良いのですが、PDFの内部には、ページ本体以外にも画像やフォント、参照情報などいろいろなオブジェクトがあるため、新規PDFを作って必要なページをコピーするという実装にすると、必要なオブジェクトを判別してコピーする必要があり処理が複雑になります。そのため、ここでは、全部をコピーした後で、不要なページを削除するという方法を取っています。

とは言え、毎回PDF全体を複製してから不要ページを削除しているため、大きなPDFでは処理が遅くなります。ページ数が多いファイルを頻繁に扱うなら、必要なページだけを新しいPDFにコピーする方法にすると良いでしょう。

この改良を加えたものを、オープンソースのPDF分割ツールとして公開しました。興味のある方は、GitHubのリポジトリを参考にしてください。

まとめ

以上、今回は、Rustを使って巨大なPDFを分割するコマンドラインツールを作ってみました。Rustのプロジェクトの作り方や、クレートの追加方法、PDF操作の基本的な流れを解説しました。Rustの公開クレートを使う方法を覚えると、PDF操作に限らず、さまざまな処理をRustで手軽に実装できるようになります。今回の内容を参考にして、Rustでの開発を楽しんでください。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。