JavaでPDF操作してみたい

## はじめに
ふとJavaでPDFファイルを操作できないかと調べたところ、Apache PDFBoxライブラリが便利そうだということがわかりました。この記事では、PDFBoxを使って何ができるのか備忘録としてまとめていきます。

## Mavenプロジェクトに追加
まずは、PDFBoxを使えるようにするため依存関係を追加します。
```pom.xml
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>3.0.3</version>
</dependency>
```

## PDFファイルの作成
下記はPDFファイルを作成できた処理です。
実行結果も載せています。
```PdfApplication.java
package com.example.pdf;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;

public class PdfApplication {
	
	public static final String PATH = "出力先のパス";

	public static void main(String[] args) {
		try {
			PDDocument document = new PDDocument();

			PDPage page = new PDPage();
			document.addPage(page);
        
			PDPageContentStream contentStream = new PDPageContentStream(document, page);
			
            contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.COURIER), 20);
            contentStream.beginText();
            contentStream.newLineAtOffset(0, 0);
            contentStream.showText("This is a PDF file");
            contentStream.endText();
            
            contentStream.close();

            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
            String timestamp = LocalDateTime.now().format(formatter);
            String fileName = "sample_" + timestamp + ".pdf";
            
            document.save(PATH + fileName);
            document.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
```
こちらが作成したPDFです。
![pdf1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/b3da5d1e-627c-3cbe-d5f0-d1b2780c0857.png)

### PDDocumentクラス
```java
PDDocument document = new PDDocument();
```
PDFドキュメント全体を管理するための基本的なクラスです。インスタンスを作成した時点では空なので、ページを追加する必要があります。

メソッド例
* addPage(PDPage page)
    * 指定したページをドキュメントに追加します
* removePage(PDPage page)
    * 指定したページをドキュメントから削除します
* removePage(int pageNumber)
    * インデックス(0始まり)を指定してページをドキュメントから削除します
* save(String fileName)
    * ドキュメントを指定したファイル名で保存します
* close()
    * ドキュメントの操作を終了し、関連するリソースを解放します

### PDPageクラス
```java
PDPage page = new PDPage();
```
PDFの1ページを表しているクラスです。デフォルトで設定されているページのサイズはU.S. Letter (8.5 x 11 inches)です。
例えばA4サイズで作成したい場合は下記のように記述します。
```java
PDRectangle pageSize = PDRectangle.A4;
PDPage page = new PDPage(pageSize);
```

### PDPageContentStreamクラス
```java
PDPageContentStream contentStream = new PDPageContentStream(document, page);
```
ページにコンテンツ（テキストなど）を描画するためのクラスです。
メソッド例
* setFont(PDType1Font font, float size)
    * フォントとサイズを設定します
* beginText()
    * テキスト描画を開始します
* newLineAtOffset(float x, float y)
    * テキストを描画する位置を指定します
* showText(String text)
    * 実際にテキストをページ上に描画します
* endText()
    * テキスト描画を終了します
* close()
    * コンテンツストリームを閉じて、ページへの描画を完了します

## 描画位置
(0, 0)と指定することで左上に描画される想定でしたが、実際に出力した結果は左下でした。
```java
contentStream.newLineAtOffset(0, 0);
```
左上を指定する場合のy座標はページの高さを用いて計算します。
(0, pageHeight)にすると文字がページ外はみ出してしまったのでフォントのサイズ分下げてみました。
```java
// ページの高さを取得
float pageHeight = page.getMediaBox().getHeight();
contentStream.newLineAtOffset(0, pageHeight - 20);
```
これで左上に揃えることができました。
![pdf2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/eac9746d-7b81-4696-65a7-738e1e10f564.png)

## 複数ページ
複数ページ作成したいときはPDPageインスタンスを複数生成します。
2ページ目はA4を指定しサイズが異なることも確認できました。
```java
PDPage page = new PDPage();
document.addPage(page);
			
PDRectangle pageSize = PDRectangle.A4;
PDPage page2 = new PDPage(pageSize);
document.addPage(page2);
```
![pdf3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/48bc4fa9-3cbc-ffd0-df75-de4f4fb29215.png)

## 画像を描画
テキストだけではなく画像を描画することもできました。
こちらも(0, 0)だと左下の位置を指定するようです。
```java
File imageFile = new File(IMAGE_PATH);
PDImageXObject image = PDImageXObject.createFromFile(imageFile.getAbsolutePath(), document);

PDPageContentStream contentStream2 = new PDPageContentStream(document, page2);

contentStream2.drawImage(image, 0, 0);
contentStream2.close();
```
![pdf5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/88410c65-04e5-67db-1acf-24e1624c41ec.png)

## PDFの読み込み
既存のPDFを読み込むこともできます。
新しいファイルとしてPDFを読み込み、さらにページを複製してみました。
```java
File file = new File("任意のパス");
PDDocument document = Loader.loadPDF(file);

// 既存のPDFの1ページ目を取得
PDPage page = document.getPage(0);

// 新しいページを作成
document.importPage(page);
```
2ページ目ができています。
![pdf6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967914/7b94c4e4-00ee-c721-8499-d0a8543065f6.png)

## あとがき
ttfファイルを読み込んでフォントを好きなように変えてみたかったのですがそれは次回試してみます。

Apache PDFBoxのバージョン3前後で処理内容が結構変わっているようですね。

https://javadoc.io/doc/org.apache.pdfbox/pdfbox/latest/index.html