Apache PDFBoxでのテキスト装飾（ななめ・斜体・アンダーライン・文字間隔・白抜き文字・TTCフォントファイルの読み込み・ハイパーリンク）

[Apache PDFBoxが日本語出力できるようになった](http://qiita.com/ota-meshi/items/55a6c379544b33728b58)のもあり、今まで全然使ったことなかったけどPDFBoxどんなことできるの？っていうのを少しずつ調べています。

今回はテキスト出力時の装飾の方法についてです。

# TTCフォントファイルの読み込み
TTFフォントファイルの読み込みは[こちら](http://qiita.com/ota-meshi/items/55a6c379544b33728b58)で書いたのでここではTTCフォントファイルの読み込みについて書きます。

（Windowsで、C:/Windows/Fonts/msmincho.ttcにフォントがないと動きません。）

```java
	public static void main(String[] args) throws IOException {
		Path path = Paths.get("pdf.pdf");
		try (OutputStream out = Files.newOutputStream(path)) {
			make(out);
		}

		System.out.println(path.toAbsolutePath());
	}

	public static void make(OutputStream out) throws IOException {

		try (PDDocument doc = new PDDocument()) {
			float fontSize = 20;
			PDRectangle rectangle = PDRectangle.A4;

			File file = new File("C:/Windows/Fonts/msmincho.ttc");
			try (TrueTypeCollection collection = new TrueTypeCollection(file)) {//save前にcloseすると埋め込みできないので注意。
				PDFont font = PDType0Font.load(doc, collection.getFontByName("MS-Mincho"), true);//Font名から取得
				float fontHeight = getFontHeight(font, fontSize);

				PDPage page = new PDPage(rectangle);
				doc.addPage(page);

				try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
					contents.beginText();

					contents.setFont(font, fontSize);

					contents.setTextMatrix(Matrix.getTranslateInstance(10, rectangle.getHeight() - fontHeight - 10));
					contents.showText("MS明朝");

					contents.endText();
				}

				try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
					doc.save(bufferedOutputStream);
				}

			}
		}
	}

	/**
	 * フォントの高さを取得
	 *
	 * @see http://stackoverflow.com/questions/17171815/get-the-font-height-of-a-character-in-pdfbox
	 */
	private static float getFontHeight(PDFont font, float fontSize) {
		return font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
	}
```

TrueTypeCollectionというクラスを使います。
ただこれがCloseableの実装なのでcloseを必要とします。
しかし、PDDocument#saveの前にcloseしてしまうと、フォントの埋め込み[^1]で落ちます。
なので、closeのタイミングは注意が必要です。もしかしたら一回ByteArrayInputStreamにしてしまえばcloseの必要がないかもしれませんが確信はないです。

## 結果

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/6cc76ed4-8445-63ed-e99e-a8d8000b8f16.png)


# ななめ・斜体
PDFの仕様を理解できれば簡単にできると思いますが、全くわからない僕は苦労しました。
こちら参考にしました。
http://www.pdf-tools.trustss.co.jp/Syntax/text.html
「座標系」とある部分です。

```java
	public static void make(OutputStream out) throws IOException {

		try (PDDocument doc = new PDDocument()) {
			float fontSize = 20;
			PDRectangle rectangle = PDRectangle.A4;

			PDFont font = loadFont(doc);
			float fontHeight = getFontHeight(font, fontSize);

			PDPage page = new PDPage(rectangle);
			doc.addPage(page);

			PDFormXObject formXObject = new PDFormXObject(doc);
			formXObject.setBBox(new PDRectangle(100f, rectangle.getHeight()));

			try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
				contents.beginText();

				contents.setFont(font, fontSize);

				float offset = 0;
				float x = 100;
				float y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
				contents.setTextMatrix(Matrix.getRotateInstance(0, x, y));//傾かない
				contents.showText("テキスト Rotate 0");

				y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
				contents.setTextMatrix(Matrix.getRotateInstance(0.1, x, y));//ななめ
				contents.showText("テキスト Rotate 0.1");

				y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
				contents.setTextMatrix(Matrix.getRotateInstance(-0.1, x, y));//ななめ
				contents.showText("テキスト Rotate -0.1");

				y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
				contents.setTextMatrix(Matrix.getRotateInstance(-Math.PI, x, y));//逆さま
				contents.showText("テキスト Rotate -PI");

				y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
				contents.setTextMatrix(new Matrix(1, 0, 0, 1, x, y));//普通
				contents.showText("テキスト (1, 0, 0, 1, x, y)");

				y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
				contents.setTextMatrix(
						new Matrix(1, 0, (float) Math.tan(0.5), 1, x, y));//斜体
				contents.showText("テキスト (1, 0, (float) Math.tan(0.5), 1, x, y)");

				y = rectangle.getHeight() - fontHeight - 10 - (offset += 50);
				contents.setTextMatrix(
						new Matrix(1, (float) Math.tan(-0.5), 0, 1, x, y));//縦斜体
				contents.showText("テキスト(1, (float) Math.tan(-0.5), 0, 1, x, y)");

				contents.endText();
			}

			try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
				doc.save(bufferedOutputStream);
			}
		}

	}
```

Matrixというのを使います。文字列をななめにするだけなら標準のメソッドが用意されています。
斜体は標準メソッドが（たぶん）無いようなので[^2]自力で値を埋めます。

## 結果

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/d87df743-b038-d3e0-1cba-d4e6ba975086.png)

# アンダーライン
そういうの（たぶん）無いので文字に合わせて下部分に線を引きます。

```java
	public static void make(OutputStream out) throws IOException {

		try (PDDocument doc = new PDDocument()) {
			float fontSize = 20;
			PDRectangle rectangle = PDRectangle.A4;

			PDFont font = loadFont(doc);
			float fontHeight = getFontHeight(font, fontSize);

			PDPage page = new PDPage(rectangle);
			doc.addPage(page);

			try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
				contents.beginText();

				contents.setFont(font, fontSize);

				String text = "テキスト ABC";
				float x = 100;
				float y = rectangle.getHeight() - fontHeight - 10;
				contents.setTextMatrix(Matrix.getTranslateInstance(x, y));
				contents.showText(text);
				contents.endText();

				//アンダーラインを引く
				contents.setLineWidth(fontSize / 20);
				contents.moveTo(x, y);
				contents.lineTo(x + getFontTextWidth(font, text, fontSize), y);
				contents.stroke();
			}

			try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
				doc.save(bufferedOutputStream);
			}
		}

	}

	private static float getFontHeight(PDFont font, float fontSize) {
		return font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
	}

	/**
	 * Fontで文字列を扱った際の幅を取得
	 */
	private static float getFontTextWidth(PDFont font, String text, float fontSize) throws IOException {
		return font.getStringWidth(text) / 1000 * fontSize;
	}
```

## 結果

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/db1ae442-f44c-e2f1-f060-0cf47208133f.png)


## 文字間隔
PDFBox標準のAPIからは探し出せなかったので自力でPDF構文を埋め込みます。
PDF構文についてはこちら参考にしました。
http://www.pdf-tools.trustss.co.jp/Syntax/text.html

ただ、このPDF構文を埋め込むメソッドはPDPageContentStream#appendRawCommandsというメソッドを利用していて、これはDeprecatedで「いつか消すよ」っぽいことが書かれているので使う際は注意してください。

```java
	public static void make(OutputStream out) throws IOException {

		try (PDDocument doc = new PDDocument()) {
			float fontSize = 20;
			PDRectangle rectangle = PDRectangle.A4;

			PDFont font = loadFont(doc);
			float fontHeight = getFontHeight(font, fontSize);

			PDPage page = new PDPage(rectangle);
			doc.addPage(page);

			PDFormXObject formXObject = new PDFormXObject(doc);
			formXObject.setBBox(new PDRectangle(100f, rectangle.getHeight()));

			try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
				contents.beginText();

				contents.setFont(font, fontSize);

				contents.setLeading(fontHeight);
				float x = 10;
				float y = rectangle.getHeight() - fontHeight - 10;
				contents.setTextMatrix(Matrix.getTranslateInstance(x, y));
				contents.showText("テキスト");

				contents.newLine();
				contents.appendRawCommands("1 Tc ");//文字間隔「1」
				contents.showText("テキスト char spacing 1");

				contents.newLine();
				contents.appendRawCommands("2 Tc ");//文字間隔「2」
				contents.showText("テキスト char spacing 2");

				contents.newLine();
				contents.appendRawCommands("5 Tc ");//文字間隔「5」
				contents.showText("テキスト char spacing 5");

				contents.newLine();
				contents.appendRawCommands("10 Tc ");//文字間隔「10」
				contents.showText("テキスト char spacing 10");

				contents.newLine();
				contents.appendRawCommands("15 Tc ");//文字間隔「15」
				contents.showText("テキスト char spacing 15");

				contents.endText();
			}

			try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
				doc.save(bufferedOutputStream);
			}
		}

	}
```

## 結果

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/98a415af-5828-9241-91d1-69926ffb54fb.png)


# 白抜き文字（とか）
こちらもPDFBox標準のAPIからは探し出せなかったので自力でPDF構文を埋め込みます。

下記にも自力でガンバルようなことが書いてあります。
http://stackoverflow.com/questions/17788455/text-rendering-mode-with-pdfbox

PDF構文についてはこちら参考に（略
http://www.pdf-tools.trustss.co.jp/Syntax/text.html

ただ、このPDF構文を埋め込むメソッドはPDPageContentStream#appendRawCommandsという（略


```java
	public static void make(OutputStream out) throws IOException {

		try (PDDocument doc = new PDDocument()) {
			float fontSize = 20;
			PDRectangle rectangle = PDRectangle.A4;

			PDFont font = loadFont(doc);
			float fontHeight = getFontHeight(font, fontSize);

			PDPage page = new PDPage(rectangle);
			doc.addPage(page);

			try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
				contents.setLineWidth(1F);
				contents.beginText();

				contents.setNonStrokingColor(Color.RED);
				contents.setStrokingColor(Color.BLUE);
				//

				contents.setFont(font, fontSize);
				contents.setLeading(fontHeight);

				contents.setTextMatrix(Matrix.getTranslateInstance(10, rectangle.getHeight() - fontHeight - 10));
				contents.appendRawCommands(RenderingMode.FILL.intValue() + " Tr ");
				contents.showText("テキスト FILL");
				contents.newLine();
				contents.appendRawCommands(RenderingMode.STROKE.intValue() + " Tr ");
				contents.showText("テキスト STROKE");
				contents.newLine();
				contents.appendRawCommands(RenderingMode.FILL_STROKE.intValue() + " Tr ");
				contents.showText("テキスト FILL_STROKE");
				contents.newLine();
				contents.appendRawCommands(RenderingMode.NEITHER.intValue() + " Tr ");
				contents.showText("テキスト NEITHER");
				contents.endText();
			}

			try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
				doc.save(bufferedOutputStream);
			}
		}
	}
```

RenderingModeというenumがあったので値間違えないように、値を取得する為にだけ使っています。

## 結果

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/4bc7004c-9515-cc12-a143-fdd1346c65da.png)

最後の"テキスト NEITHER"は見えませんが選択はできます。

# ハイパーリンク
下記を参考にしました。
http://stackoverflow.com/questions/21021502/how-to-add-hyperlink-in-pdf-using-pdfbox

```java
	public static void make(OutputStream out) throws IOException {

		try (PDDocument doc = new PDDocument()) {
			float fontSize = 20;
			PDRectangle rectangle = PDRectangle.LETTER;

			PDFont font = loadFont(doc);
			float fontHeight = getFontHeight(font, fontSize);

			PDPage page = new PDPage(rectangle);
			doc.addPage(page);

			String text = "Qiita";
			Color color = Color.BLUE;
			float x = 10;
			float y = rectangle.getHeight() - fontHeight - 10;

			try (PDPageContentStream contents = new PDPageContentStream(doc, page)) {
				//テキストの描画
				contents.beginText();
				contents.setFont(font, fontSize);
				contents.setNonStrokingColor(color);
				contents.setTextMatrix(Matrix.getTranslateInstance(x, y));
				contents.showText(text);
				contents.endText();
			}

			//ハイパーリンクを配置
			PDAnnotationLink link = new PDAnnotationLink();

			PDBorderStyleDictionary borderULine = new PDBorderStyleDictionary();
			borderULine.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE);
			link.setBorderStyle(borderULine);

			PDColor pdColor = new PDColor(new float[] { color.getRed(), color.getGreen(), color.getBlue() },
					PDDeviceRGB.INSTANCE);
			link.setColor(pdColor);

			PDActionURI action = new PDActionURI();
			action.setURI("http://qiita.com");
			link.setAction(action);

			PDRectangle position = new PDRectangle();
			position.setLowerLeftX(x);
			position.setLowerLeftY(y);
			position.setUpperRightX(x + getFontTextWidth(font, text, fontSize));
			position.setUpperRightY(y + fontHeight);
			link.setRectangle(position);

			page.getAnnotations().add(link);

			try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(out)) {
				doc.save(bufferedOutputStream);
			}
		}

	}
```

## 結果

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/b0bde10a-35c2-6b8f-c55c-734bd163dda2.png)

クリックしたらQiitaが開きます。



---
以上です。

TcとTrは自力で埋め込まなくて済む方法ご存知の方ぜひ教えてください。


[^1]: PDType0Font#loadの第3引数をtrueにするとフォント埋め込みになる様子。
[^2]: 普通の斜体ならイタリックフォントで充分ですからね。