SpringBootを使って、タダ（無償）でHTMLファイルをPDFにする

# はじめに
SpringBootを使った、管理サイトを構築予定です。
その中の要件としてPDFを出力したいというものがあったので調べてみました。
もちろん有償のライブラリを使えばできるのは知ってましたが、Google Maps Platformを
使ったりランニングコストがかさむので、無償にこだわって調査から開始

# やってみる
## 環境
Spring Boot 3.3.0

PDFの作成には、以下を利用しています。

https://github.com/danfickle/openhtmltopdf

## 日本語フォントファイルを格納
無料で使えるフォントファイルを取得します。

https://nako-itnote.com/noto-sans-cjk-jp-download/

ダウンロードしたファイルを解凍して、以下の通り格納
src/main/resources/fonts/NotoSansJP-Regular.ttf

## Gradleの設定
PDF変換のために、以下の2行を追加してください。

```gradle
    implementation 'com.openhtmltopdf:openhtmltopdf-core:1.0.10'
    implementation 'com.openhtmltopdf:openhtmltopdf-pdfbox:1.0.10'
```

## Controllerの準備
コントローラを作っていきます。
generate-pdfをGetで呼び出すと、PDFを返却するように実装していきます。

```java
package com.example.demo.controller;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;

@Controller
public class PdfController {

    @Autowired
    private SpringTemplateEngine templateEngine;

    @GetMapping("/generate-pdf")
    public ResponseEntity<InputStreamResource> generatePdf(Model model) throws IOException {
        // テンプレートエンジンにデータを渡す
        Context context = new Context();
        context.setVariable("message", "こんにちは、PDFBox!");

        // HTMLを生成
        String html = templateEngine.process("pdf-template", context);

        // HTMLをPDFに変換
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PdfRendererBuilder builder = new PdfRendererBuilder();

        builder.withHtmlContent(html, null);

        builder.useFont(() -> getClass().getResourceAsStream("/fonts/NotoSansJP-Regular.ttf"), "Noto Sans CJK JP");
        builder.toStream(outputStream);
        builder.run();

        ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());

        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Disposition", "inline; filename=example.pdf");

        return ResponseEntity.ok()
                .headers(headers)
                .contentType(MediaType.APPLICATION_PDF)
                .body(new InputStreamResource(inputStream));
    }

}
```


## CSSの準備
CSSファイルを作っていきます。
src/main/resources/static/css/style.css
に格納してください。

```css

body {
    font-family: 'Noto Sans CJK JP', sans-serif;
}

.container {
    width: 80%;
    margin: 50px auto;
    padding: 20px;
    border: 1px solid #333;
    border-radius: 10px;
}

.header {
    text-align: center;
    margin-bottom: 20px;
    font-size: 24px;
}

.content {
    line-height: 1.6;
}

table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 20px;
}

table, th, td {
    border: 1px solid #333;
    padding: 10px;
    text-align: center;
}

th {
    background-color: #f2f2f2;
}
```

## Imageの準備
サンプルなので画像はなんでもいいのですが、以下に格納します。
src/main/resources/static/images/example.jpg

## Thymeleafの準備
以下のような内容で、ファイルを作成し
src/main/resources/templates/pdf-template.html
に、格納します。

ポイントは、CSSとか、画像のパスの指定の仕方ですね。

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
	<meta charset="UTF-8" />
	<title>PDF Example</title>

	<link rel="stylesheet" type="text/css" href="classpath:/static/css/style.css" />	
</head>

<body>
	<div class="container">
		<div class="header">
		</div>
		<div class="content">
			<p th:text="${message}"></p>
			<table>
				<thead>
					<tr>
						<th>列1</th>
						<th>列2</th>
						<th>列3</th>
					</tr>
				</thead>
				<tbody>
					<tr>
						<td>データ1</td>
						<td>データ2</td>
						<td>データ3</td>
					</tr>
					<tr>
						<td>データ4</td>
						<td>データ5</td>
						<td>データ6</td>
					</tr>
				</tbody>
			</table>
			<div class="image-container">
				<img src="classpath:/static/images/example.jpg" alt="Sample Image" />
				<p>このように、画像をPDFに含めることができます。</p>
			</div>
		</div>
	</div>
</body>

</html>
```


## 実行結果
http://localhost:8080/generate-pdf
へアクセスするとこんな結果がPDFとして表示できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132118/8001345c-1bb6-644f-229d-980835b549b1.png)

# 感想
CSSを使ってHTMLを成型できるので、ほぼどんなPDFでも作成できそうな感覚。
画像ファイルと、CSSをクラスパス上のものを設定するのに結構苦戦しましたが一度できてしまえば、たいしたことはなさそうです。あとは繰り返し項目とかの設定ができるとかなり実用的かなと思います。