PDFの出力

基本的な出力方法

まず、「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」でダウンロードしてください。

Noto Sans Japanese

ダウンロードが完了したら「すべて解凍」クリックで解凍し、
ファイル内の「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ファイルでダウンロードできるのが確認できたでしょうか。

タイトルとURLをコピーしました