基本的な出力方法
まず、「pom.xml」 に必要なライブラリを追加し、依存関係を追加します。
依存関係(Dependency)とは、あるクラスやモジュールが、他のクラスやモジュールを利用する関係のことです。
簡単に言うと、「AクラスがBクラスに頼って動いている状態」です。
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.3</version>
</dependency>
今回、Apache PDFBoxライブラリを使います。
「com/cmps/spring/controller」パッケージ内に「PdfTestController」を作成し以下のソースを記述しましょう。
import java.io.File;
import java.nio.file.Paths;
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;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PdfTestController {
@GetMapping("/pdfTest")
public String pdfTest(Model model) {
// 出力先の設定
// resourcesフォルダの絶対パスを取得
String resourcePath = Paths.get("src/main/resources/PDF/").toAbsolutePath().toString();
// 出力先フォルダを確認し、存在しない場合は作成
File outputDir = new File(resourcePath);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
// 出力ファイルパス・ファイル名も設定
File outputFile = new File(outputDir, "output.pdf");
try {
// ドキュメントオブジェクトの作成
PDDocument document = new PDDocument();
// ページオブジェクトの作成
PDPage page = new PDPage();
document.addPage(page);
// ドキュメントの保存
document.save(outputFile);
document.close();
model.addAttribute("text","成功しました。");
return "pdf/pdfTest";
}
catch (Exception e) {
e.printStackTrace();
model.addAttribute("text","失敗しました。");
return "pdf/pdfTest";
}
}
}
次に読み込み用のviewファイルを用意しましょう。
「src/main/resources/templates/pdf」フォルダを作成、「pdfTest.html」を作成してください。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ja">
<head>
<meta charset="UTF-8">
<title>PDF出力テスト</title>
</head>
<body>
<h1>PDF出力テスト</h1>
<p th:text="*{text}"></p>
</body>
</html>
また、PDFの出力先としてsrc/main/resources直下に「PDF」フォルダを作成しておきましょう。
Springを起動し、http://localhost:8080/pdfTestを開くと、PDFフォルダ内に1ページだけのファイルが出来ました。
次に文字を出力してみましょう。
tryの中、ページオブジェクトの作成の下、「document.save();」より上部に追加しましょう。
// 文字出力処理
PDPageContentStream contentStream = new PDPageContentStream(document, page);
contentStream.beginText();
// フォント指定
contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.COURIER), 20);
// 出力位置指定
contentStream.newLineAtOffset(0f, 0f);
// 出力文字列
contentStream.showText("Hello World!");
contentStream.endText();
contentStream.close();
実行すると以下の画像のような「test.pdf」が出力されたと思います。

では、解説をしていきます。
PDDocumentクラス
PDDocument document = new PDDocument();
PDFドキュメント全体を管理するための基本的なクラスです。
インスタンスを作成した時点では空ですので、ページを追加する必要があります。
主なメソッド
| addPage(PDPage page) | 指定したページをドキュメントへ追加 |
| removePage(PDPage page) | 指定したページをドキュメントから削除 |
| removePage(int pageNumber) | インデックスを指定してページをドキュメントから削除 |
| save(String fileNme) | ドキュメントを指定したファイル名で保存 |
| close() | ドキュメントの操作を終了し、関連するリソースを解放 |
PDPageクラス
PDPage page = new PDPage();
PDFの1ページを表しているクラスです。デフォルトで設定されているページのサイズはU.S. Letter (8.5 x 11 inches)です。
例えばA4サイズで作成したい場合は下記のように記述します。
第二引数を変えることで、他の用紙サイズでも出力することが出来ます。
PDRectangle pageSize = PDRectangle.A4;
PDPage page = new PDPage(pageSize);
PDPageContentStreamクラス
PDPageContentStream contentStream = new PDPageContentStream(document, page);
ページにコンテンツ(テキストなど)を描画するためのクラスです。
主なメソッド
| setFont(PDType1Font font, フォントサイズ) | フォントとサイズを設定 |
| beginText() | テキスト出力を開始 |
| newLineAtOffset(float x, float y) | テキストを出力する位置を指定 ページの左下が(0,0)である点に注意 |
| showText(String text) | 出力したい文字列を記述 |
| .setLeanding() | 行間を設定(一般的にフォントサイズの高さ) |
| .newLine() | 改行 |
| endText() | テキスト出力を終了 |
| close() | コンテンツストリームを閉じて、ページへの描画を完了 |
複数ページの出力
複数ページ作成したいときはPDPageインスタンスを複数生成します。
PDPage page = new PDPage();
document.addPage(page);
// 2ページ目出力
PDPage page2 = new PDPage();
document.addPage(page2);
2ページ目にも文字を出力したい場合は、PDPageクラスのインスタンス作成から
PDPageContentStreamクラスのcloseメソッドまで一連を再び記述します。
日本語の出力
日本語のフォントを利用する場合、フォントの宣言部分が異なります。
次のように PDType0Font クラスの load メソッドを呼出すことで、利用するフォントを指定することができます。
以下のソースはbeginText()メソッド(文字出力処理)より下に記述してください。
先ほどまでに記述した「フォント指定」の箇所はコメントアウトしておきましょう。
contentStream.beginText();
// 日本語の場合
// コンピューターにインストールされているフォントへのパスを記入
File file = new File("C:/Windows/Fonts/msmincho.ttc");
// TTCに含まれているフォント名を指定
TrueTypeCollection collection = new TrueTypeCollection(file);
PDFont font = PDType0Font.load(document, collection.getFontByName("MS-Mincho"), true);
// 出力時のフォントとサイズを指定
contentStream.setFont(font, 12);
// 以下は先程書いたものをそのまま使用できます。
contentStream.newLineAtOffset(0f, 0f);
contentStream.showText("こんにちは"); // 日本語へ書き換え
contentStream.endText();
contentStream.close();
showTextメソッドに適当な日本語を記述し、出力されたPDFファイルを見てみましょう。
日本語で出力されましたか?
TTCフォントファイルの場合、
getFontByName を用いてフォントの名称を指定(上記でいえば「MS-Mincho」)する必要があります。
PDFont font = PDType0Font.load(document, collection.getFontByName("MS-Mincho"), true);
日本語はマルチバイトであるために、PDType1Font ではなく PDType0Font を使う必要があります。
フォントの名称ですが、以下のソースを用いて出力可能です。
// TTCに含まれるフォントを表示
try {
TrueTypeCollection tcc = new TrueTypeCollection(Files.newInputStream(Paths.get("フォントのパス")));
tcc.processAllFonts((TrueTypeFont font) -> {
try {
System.out.println("Font Name: " + font.getName());
} catch (IOException e) {
e.printStackTrace();
}
});
tcc.close();
} catch (Exception e) {
e.printStackTrace();
}
「.ttc」とはTrueType Collection(トゥルー・タイプ・コレクション)の略で、TrueTypeフォントのデータが複数格納されたファイルに付けられることの多い拡張子です。
TrueTypeフォントのファイルにつくことが多い拡張子は「.ttf(TrueType Font(トゥルー・タイプ・フォント))」です。
なお、TTFフォントファイルの場合は以下の様にloadメソッドの引数にパスを記述することで使用できます。
// TTFフォントファイルの場合
PDFont font2 = PDType0Font.load(document, new File("C:/Windows/Fonts/ENGR.TTF"));
HTMLファイルを出力
ここではHTMLファイルをPDFに変換する方法として、iTextというJavaコードを使用して PDF ドキュメントを生成できる Java PDF 生成ライブラリ(Flying Saucer)を使用します。
ITextRenderer – flying-saucer-pdf 9.1.21 javadoc
Flying Saucer は、レイアウトとフォーマットに任意の整形式 XML (または XHTML) をレンダリングし、PDFや画像に出力することができます。
flyingsaucerproject/flyingsaucer: XML/XHTML and CSS 2.1 renderer in pure Java
まず、「pom.xml」 に必要なライブラリを追加し、依存関係を追加します。
<!-- Thymeleaf to PDF (flying-saucer) -->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.lowagie/itext -->
<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>3.3.4</version>
</dependency>
続いて、「src/main/resources/static」直下に「fonts」フォルダを作成し、
以下のフォントをリンク先の「Get font」→「Download all」でダウンロードしてください。
ダウンロードが完了したら「すべて解凍」クリックで解凍し、
ファイル内の「NotoSansJP-VariableFont_wght.ttf」をfontsフォルダ内へ配置しましょう。

「src/main/resources/templates/pdf」フォルダ内に
ダウンロード画面である「sample.html」、PDF出力用のテンプレートファイルである「content.html」を配置しましょう。
・sample.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ja">
<head>
<meta charset="UTF-8">
<title>PDF出力</title>
<style>
body {
font-family: 'NotoSansJP', sans-serif;
}
h1 {
text-align: center;
}
.download {
display: flex;
}
form {
margin: 0 auto;
}
button {
background-color: orange;
border: none;
padding: 10px;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);;
}
button:hover {
box-shadow: none;
}
</style>
</head>
<body>
<!--PDFダウンロードボタン -->
<h1>PDF出力</h1>
<div class="download">
<form th:action="@{/download-pdf}" method="get">
<button type="submit">PDFをダウンロード</button>
</form>
</div>
</body>
</html>
・content.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ja">
<head>
<meta charset="UTF-8" />
<title>PDF用画面</title>
<style>
/** スタイルシートの中でフォントを明示的に指定しないとpdf出力時に日本語が出ません **/
@font-face{
font-family: "NotoSansJP";
src: url("fonts/NotoSansJP-VariableFont_wght.ttf");
-fs-pdf-font-embed: embed;
-fs-pdf-font-encoding: Identity-H;
}
body {
font-family: 'NotoSansJP', sans-serif;
}
h1 {
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th,
td {
border: 1px solid black;
padding: 10px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
<body>
<h1>PDF-Test</h1>
<table>
<thead>
<tr>
<th>商品</th>
<th>価格</th>
<th>原産地</th>
</tr>
</thead>
<tbody>
<tr th:each="fruit : ${list}" th:object="${fruit}">
<td th:text="*{product}"></td>
<td th:text="*{price} + '円'"></td>
<td th:text="*{from}"></td>
</tr>
</tbody>
</table>
</body>
</html>
content.htmlのstyleタグ内の記述について
@font-face内、src: url(“fonts/NotoSansJP-VariableFont_wght.ttf”); の記述は、src/main/resources/static直下のfontsフォルダのファイルを読み込みしています。
最後に「com.cmps.spring.controller」パッケージに「PdfController」を作成し以下のソースを書きましょう。
・PdfController
package com.cmps.spring.controller;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
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.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import com.cmps.spring.service.CsvService;
import com.lowagie.text.DocumentException;
@Controller
public class PdfController {
@Autowired
CsvService csvService;
/**
* HTMLページ(ダウンロードページ)を表示
*
* @param model Model
* @return
*/
@GetMapping ("/pdfConvert")
public String index(Model model) {
return "pdf/sample";
}
/**
* PDFファイルをダウンロードさせるメソッド
*
* @return ResponseEntity<byte[]> PDFダウンロードの実行
* @throws IOException
* @throws DocumentException
*/
@GetMapping("/download-pdf")
public ResponseEntity<byte[]> downloadPdf() throws IOException, DocumentException {
// 初期設定したTemplateEngineオブジェクトを取得
TemplateEngine engine = initializeTemplateEngine();
// テンプレートに渡す変数をMapとして準備
Map<String, Object> datas = new HashMap<>();// contextに渡せる型はMap<String, Object>なので合わせている
// リストを取得してdatasマップに格納
List<Map<String, String>> list = getList();
datas.put("list", list);
// テンプレートを処理する際に使用するコンテキスト(テンプレート変数を保持するオブジェクト)
Context context = new Context();
// テンプレートに渡したいデータをセット
context.setVariables(datas);
// HTMLの内容を受け取り、レイアウト計算やレンダリングを行ってPDFを生成するクラス
ITextRenderer renderer = new ITextRenderer();
// HTMLテンプレートをHTML文字列としてレンダリング
String htmlContent = engine.process("pdf/content", context);
// PDF内で画像やCSSを参照できるよう、静的リソースのルートパスを取得
String baseUrl = new ClassPathResource("static/").getURL().toString();
// PDFを生成する設定をセット
renderer.setDocumentFromString(htmlContent, baseUrl);
// 設定されたドキュメントのレイアウト計算を実行(ファイルの生成前準備)
renderer.layout();
// バイトデータをメモリ上に書き込むための出力ストリーム。生成されたPDFのバイトデータを一時的に保持するために使用
ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
renderer.createPDF(byteOutStream);
// バイト配列として取得
byte[] pdfBytes = byteOutStream.toByteArray();
// ファイル名
String filename = "fruits_list.pdf";
// HttpHeadersインスタンスを取得、ResponseEntityを返却
HttpHeaders headers = csvService.createDownloadHeaders(filename, MediaType.APPLICATION_PDF);
return new ResponseEntity<>(pdfBytes, headers, HttpStatus.OK);
}
/**
* PDF生成に使用するためのTemplateEngineインスタンスを初期化・準備し、返却する
*
* @return TemplateEngineインスタンス
*/
private TemplateEngine initializeTemplateEngine() {
// エンジンをインスタンス化
final TemplateEngine templateEngine = new TemplateEngine();
// テンプレート解決子をインスタンス化(今回はクラスパスからテンプレートをロードする)
final ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
// テンプレートモードはXHTML
// Flying SaucerのPDFレンダリングはXHTML形式である必要があります。
// content.htmlは必ず正しい構文のHTMLで作成しましょう。
resolver.setTemplateMode("XHTML");
// クラスパスのtemplatesディレクトリ配下にテンプレートファイルを置くことにする
// src/main/resources/ はクラスパスのルートなので、Thymeleafから見た場合 templates/pdf/ が正解。
resolver.setPrefix("templates/");
// テンプレートの拡張子はhtml
resolver.setSuffix(".html");
resolver.setCharacterEncoding("UTF-8");
// テンプレート解決子をエンジンに設定
templateEngine.setTemplateResolver(resolver);
return templateEngine;
}
/**
* 果物の情報が入ったリストを生成して返す
*
* @return List<Map<String, String>>
*/
public List<Map<String, String>> getList() {
Map<String, String> apple = new HashMap<>();
apple.put("product", "リンゴ");
apple.put("price", "100");
apple.put("from", "青森県");
Map<String, String> banana = new HashMap<>();
banana.put("product", "バナナ");
banana.put("price", "200");
banana.put("from", "フィリピン");
Map<String, String> orange = new HashMap<>();
orange.put("product", "みかん");
orange.put("price", "350");
orange.put("from", "愛媛県");
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
list.add(apple);
list.add(banana);
list.add(orange);
return list;
}
}
ここでは「CSVファイル操作」で使用したcreateDownloadHeadersメソッドを流用し、同様にしてResponseEntityを返却してPDFファイルをダウンロードさせています。
ResponseEntityクラスに落とし込むために、PDFデータを最終的にバイト配列で取得しています。
「Spring○○Application」を選択し実行してみましょう。
content.htmlで作成したテーブルがPDFファイルでダウンロードできるのが確認できたでしょうか。
