torutkのブログ

ソフトウェア・エンジニアのブログ

Apache PDFBoxライブラリを使ってPDF文書の表示(着手編)

ときおり、PDF文書から任意のページを切り出したいということがあります。PDF文書を扱うのはAcrobat(≠Acrobat Reader)を購入するのが一番でしょうし、フリーのツールも多々存在しますが、ここはJavaでツールを作成してみたいと思います。

まず、第一歩としてPDF文書をページ単位でJavaで表示するPDFのビューアーを作成し、次にページを指定して切り出す機能を付けるという段階で進めていこうと考えました。

JavaからPDFを扱うオープンソースライブラリとしては、昔からの定番のiTextと、ApacheのPDFBoxが著名でしょうか。ぐぐっていると次のまとめページがありました。このページは分かりやすいです。

iTextは商用ライセンスとAffero GPLのデュアルライセンスです。AGPLで利用するとGPLv2と互換性がないので他のライブラリを使うときにちょっと注意が必要です。
PDFBoxはApacheライセンス2.0ですから特に問題はなさそうです。上述ページで紹介しているJasperReportsはPDF出力だけらしく今回の用途には合わないかと思います。

そこで、Apache PDFBoxをつかって実装してみます。
Apache PDFBoxについては、日本語が扱えないといった記述があります。

MS932で出力すると日本語が扱えるという記事もあります。

開発中のバージョン2では対応しているとの記述もありす。

Apache PDFBoxで日本語の出力とフォントの埋め込み - Symfoware

今回は、PDFBox 2.0の開発版を使ってみることにします。

Apache PDFBox 2.0.0-SNAPSHOTとその依存ライブラリを入手する

PDFBox 2.0の依存関係については次のページに説明があります。
https://pdfbox.apache.org/2.0/dependencies.html

今回のプログラミングでは、ロートルな開発環境(Ant)を使うので、必要なライブラリを依存関係を含めてすべてダウンロードします。

まず、Apache PDFBox公式サイトにアクセスします。
https://pdfbox.apache.org/

左側メニューのDownloadsのリンクを辿り、Latest development snapshot buildのリンクを辿り、Downloads a snapshot buildのリンクを辿ります。
http://repository.apache.org/content/groups/snapshots/org/apache/pdfbox/

pdfboxのリンクを辿り、2.0.0-SNAPSHOTのリンクを辿ります。
最新の日付のjarファイルをダウンロードします。本日時点では次のファイルが最新でした。

  • pdfbox-2.0.0-20151002.190058-1737.jar
  • pdfbox-2.0.0-20151002.190058-1737-sources.jar

また、この場所にあるpomファイルを開いて依存関係をチェックします。すると次のライブラリへの依存が記述されていました。

<dependency>
  <groupId>org.apache.pdfbox</groupId>
  <artifactId>fontbox</artifactId>
  <version>${project.version}</version>
</dependency>
<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
</dependency>
<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcmail-jdk15on</artifactId>
  <optional>true</optional>
</dependency>
<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk15on</artifactId>
  <optional>true</optional>
</dependency>

fontboxは同じPDFBoxの一部なので、同様に、fontboxのリンクを辿り、2.0.0-SNAPSHOTのリンクを辿って最新の日付のjarファイルをダウンロードします。

  • fontbox-2.0.0-20151002.185929-1790.jar
  • fontbox-2.0.0-20151002.185929-1790-sources.jar

次に、commons-loggingはApacheプロジェクトのライブラリなので次からダウンロードします。
https://repository.apache.org/content/groups/public/commons-logging/commons-logging/1.2/

  • commons-logging-1.2.jar

org.bouncycastleの2つのライブラリは、Javaの暗号ライブラリで、PDFファイルの署名、暗号化に使用されます。署名のないPDFファイルを扱うときは不要です。
http://www.bouncycastle.org/java.html

上述URLの左側メニューlatest releasesのリンクを辿って、jarファイルを入手します。

  • bcmail-jdk15on-152.jar
  • bcprov-jdk15on-152.jar

もし、PDF文書で256bit AES暗号を使う場合は、OracleJDKダウンロードサイトから別途Java Cryptography Extension(JCE)をダウンロードし、\jre\lib\security\の中にあるlocal_policy.jarとUS_export_policy.jarと差し替えておきます。

NetBeans IDEへのライブラリの定義

以下に記述する開発環境は、次です。

OS Windows 7 64bit版
JDK Java SE Development Kit 8u60
IDE NetBeans IDE 8.1 開発版
GUIデザインツール Scene Builder 8.0.0(from Gluon)

今回のプログラム作成環境ではNetBeans IDEを使用します。
入手したjarファイル群を、一例としてC:\Program Files\Java\pdfbox\の下に置き、NetBeans IDE上で新規ライブラリの定義を作成します。[ツール]メニュー > [ライブラリ] で「Antライブラリ・マネージャ」ダイアログを開き、[新規ライブラリ]ボタンを押し、ライブラリ名に「PDFBox」と入力、クラスパスに、[JAR/フォルダの追加]ボタンを押して入手したjarファイルを登録します。ソースについても同様に入手したsources.jarを登録します。

PDFBox 2.0でPDF文書を表示するJavaプログラム

まずは、ページ単位に表示するJavaプログラムを作成します。GUIJDK 8標準搭載のJavaFXを使用し、ページめくりのGUIコントロールPaginationを使って実装します。

  • NetBeans IDEで[ファイル]メニュー > [新規プロジェクト]で「新規プロジェクト」ダイアログを開き、カテゴリ欄で[JavaFX]を選択、プロジェクト欄で[JavaFX FXMLアプリケーション]を選択し[次へ]ボタンを押します。
  • プロジェクト名に「PdfViewer」と入力、プロジェクトの場所に開発作業ディレクトリを指定します。
  • FXML名に「PdfView」と入力し[終了]ボタンを押します。

この時点で次のソースファイルがソースパッケージpdfviewerの下に生成されます。

  • PdfViewer.java
  • PdfView.fxml
  • PdfViewController.java

JavaFXアプリケーションのメインクラス(Applicationを継承したクラス)となるPdfViewer、GUIレイアウトを定義するPdfViwe FXMLファイル、ビューコントローラとなるPdfViewControllerです。
ここにはモデル相当のクラスがないので、Javaクラスを新規作成します。

  • NetBeans IDEのプロジェクトビューでソースパッケージpdfviewerを右クリックし、[新規] > [Javaクラス]をクリックします。「New Javaクラス」ダイアログが開かれます。
  • クラス名に「PdfModel」と入力、パッケージがpdfviewerであることを確認し[終了]ボタンを押します。

GUIレイアウトの作成

PdfView.fxmlをダブルクリックすると、Scene Builderが開きます。
NetBeans IDEが生成したデフォルトのfxmlは、AnchorPaneにLabelとButtonが貼ってある雛形です。

ちょっとしたGUIアプリケーション画面は、AnchorPaneよりもBorderPaneの方がいじりやすい(後々機能を付け足すのが容易)ので、今回はBorderPaneを使います。

  • Scene BuilderでAnchorPaneを右クリックし、[Delete]をクリック、すべてのGUI要素が削除されます。
  • Containersのアコーディオンを開き、BorderPaneを中央の画面にドラッグ&ドロップします。
  • Controlsのアコーディオンを開き、Paginationを中央の画面にドラッグ&ドロップします。
  • Paginationコントロールを画面いっぱいに表示させるため、Paginationを選択し、右側のレイアウトのアコーディオンを開き、Anchor Pane Constraintsの図の上下左右の棒をクリックし、それぞれの数値に0を指定します。
  • 左下のControllerアコーディオンを開き、Controller class欄にpdfviewer.PdfViewControllerを指定します。
  • Paginationを選択し、Codeアコーディオンを開き、fx:id欄にpagination と入力します。これは後ほどPdfViewControllerクラスのフィールドpaginationを定義しインジェクションする際の紐付けとなります。

PdfModelの作成

PDFBoxライブラリを使用するので、NetBeansのプロジェクトで使用するライブラリを追加します。PdfViewrプロジェクトを右クリックし[プロパティ]をクリックし、左側ペインで[ライブラリ]を選択、右側ペインの[コンパイル]タブを選択、[ライブラリの追加]ボタンを押し、先に定義したPDFBoxを追加します。

今回は、次のメソッドを持つモデルクラスを作成します。

  • PDF文書へのパスを引数に取るコンストラク
  • ページ数を返却するメソッド
  • 引数で指定されたページのImageを作成して返却するメソッド
package pdfviewer;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;

/**
 * PDF文書のモデルクラス。
 */
class PdfModel {    
    private PDDocument document;
    private PDFRenderer renderer;
    
    PdfModel(Path path) {
        try {
            document = PDDocument.load(path.toFile());
            renderer = new PDFRenderer(document);
        } catch (IOException ex) {
            throw new UncheckedIOException("PDDocument thorws IOException file=" + path, ex);
        }
    }

    int getPageCount() {
        return document.getPages().getCount();
    }   
    
    Image getImage(int pageNumber) {
        BufferedImage pageImage;
        try {
            pageImage = renderer.renderImage(pageNumber);
        } catch (IOException ex) {
            throw new UncheckedIOException("PDFRenderer throws IOException", ex);
        }
        return SwingFXUtils.toFXImage(pageImage, null);
    }
}

PDF文書を読み込むには、PDDocumentクラスのstaticメソッドloadを使用します。
ページの画像を生成するには、PDFRendererクラスのrenderImageメソッドを使用します。renderImageはBufferedImage型を返却するので、JavaFXのImageに変換するためSwingFXUtilsクラスのtoFXImageメソッドを使用します。

PdfViewControllerの作成

着手編なのでとりあえずPDF文書を読み込みページめくりと表示ができるところまでたどり着きたいので、PDF文書のパスをハードコーディングしています。次のバージョン以降でファイル選択ダイアログによるPDF文書の表示やドラッグ&ドロップ対応をしていきます。

package pdfviewer;

import java.net.URL;
import java.nio.file.Paths;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Pagination;
import javafx.scene.image.ImageView;

/**
 * PdfView.fxmlと対になるコントローラクラス。
 */
public class PdfViewController implements Initializable {
    
    @FXML
    private Pagination pagination;
    
    private PdfModel model;
    
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        model = new PdfModel(Paths.get("sample.pdf"));
        pagination.setPageCount(model.numPages());
        pagination.setPageFactory(index -> new ImageView(model.getImage(index)));
    }    
    
}

paginationフィールドは、FXMLで定義したPaginationのインスタンスをインジェクションするため@FXMLアノテーションを付け、FXMLのfx:idに指定した名前をフィールド名にしています。

PdfModelは、カレントディレクトリのsample.pdfを読み込む仮実装でインスタンスを生成しています。エラー処理を手抜きしているので、カレントディレクトリにsample.pdfが存在しないとランタイム例外(UncheckedIOException)を吐いて落ちます。

Paginationインスタンスには、全体のページ数と、各ページが選択されたときに表示するコンテンツ(Node)を生成するファクトリクラスのインスタンスを設定します。

ここまでできたら、任意のPDFファイルをsample.pdfの名前でカレントディレクトリ(NetBeansのPdfViewerプロジェクトのディレクトリ)に置きます。

実行すると、画面にPDF文書のページ画像とページめくりの制御が表示されます。

PdfViewer(メインクラス)の作成

NetBeansの生成した雛形のままでも実行可能ですが、ウィンドウのタイトル文字列設定だけ追記しました。
mainメソッドはなくても実行可能なので削除しています。

package pdfviewer;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class PdfViewer extends Application {
    
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("PdfView.fxml"));
        
        Scene scene = new Scene(root);
        
        stage.setScene(scene);
        stage.setTitle("PDF simple viewer by PDFBox");
        stage.show();
    }
    
}

実行

実行すると次のように表示されます(PDFコンテンツが大きいとページ制御が隠れてしまうのでウィンドウを適宜広げてください)。


感想のようなもの

PDFBoxの2.0は大分APIが変更になっています。
ページ数の取り方、画像(ページイメージ)の作成の仕方などもPDFBox 1.xの記述とは違っているので、APIドキュメントから適当に(Imageを検索して)探して試してみたら出ましたというレベルです。
出来てしまえばずいぶんシンプルになっていると思いますが、ここまで一日がかりでした。

JavaFXのPaginationは、最初使い方が分からず悩んでいましたが、概念が分かってしまえば簡単でした。

GitHubにこのプロジェクトをアップしました。
https://github.com/torutk/pdfviewer