JSP サイトを管理している知り合いが「PDF を動的に作成して表示したい」と言っていたので、ちょっと試してみました。
Apache FOP
PDF を動的に生成するライブラリは多くあります。有料のものですと iText とか、複雑な帳票に向いていそうな SVF とか。
でも今回はあまり複雑でない PDF を作成するようですし、OSS での実装を検討してみました。コスト削減できますし、何かあったらソースコード読んで確認できるのは嬉しいものです。
まずチェックしたのが Apache PDFBox なのですが、あまりにプリミティブといいますか、そのまま作りこんでしまうとデザインの更新が難しくなりそうなので、パス。上にもうひとつコンテンツ作成レイヤが必要な印象です。
そして見つけたのが XSL ベースでコンテンツを作成する Apache FOP です。XHTML の知識でコンテンツ作成が可能!これに PDF 形式の出力機能がありました。
実際の Web 画面
以下のような入力画面で適当な文字列を入力して「Submit」すると
入力した日時や文字列を含んだ PDF を自動的に生成して表示します。
開発環境
今回利用した開発環境は素の Eclipse 2020-09 (4.17.0) の Java EE パースペクティブで、サーバーに Tomcat v8.5 を追加指定したものです。まあ QRCodeの時 と同じですね。
赤枠が今回のために追加、作成したファイルになります。
(1) ですが Apache FOP のダウンロードページ からダウンロードしたもの (私の場合は fop-2.5-bin.zip) からコピーします。fop.xconf
ファイルもお忘れなく!
(2) と (3) の3つのファイルが今回の開発対象です。
実際のコード
入力ページ
入力ページは通常の html ページです。Bootstrap のテンプレート にフォーム要素を追加しただけの、非常にシンプルなものです。
QRCodeの時 のコードとほぼ同じ、タイトルと action を変えただけですね。手抜きですいません。。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Simple Apache FOP sample with JSP</title>
</head>
<body>
<div class="container">
<h2>Simple Apache FOP sample with JSP</h2>
<form action="/test02/pdfServlet">
<div class="form-group">
<label for="i_url">Target URL</label>
<input type="text" class="form-control" id="i_url" name="i_url">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
</body>
</html>
Servlet ページ
さてこちらは、入力を受けて PDF を動的に生成している部分です。Apache FOP: Servlets にあるサンプルコードを、わりと大胆に流用しています。
import java.io.*;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopConfParser;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.FopFactoryBuilder;
import org.apache.fop.apps.MimeConstants;
@WebServlet("/pdfServlet")
public class pdfServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private TransformerFactory tFactory = TransformerFactory.newInstance();
public pdfServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
String i_url = request.getParameter("i_url");
String xmlData = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<?xml-stylesheet type=\"application/xml\"?>"
+ "<users-data>"
+ "<date>" + (new Date()).toString() + "</date>"
+ "<body>" + (i_url == null ? "EMPTY" : i_url) + "</body>"
+ "</users-data>";
FopConfParser fopConfParser = new FopConfParser(new File(this.getServletContext().getRealPath("/WEB-INF/fop.xconf")));
FopFactoryBuilder fopFactoryBuilder = fopConfParser.getFopFactoryBuilder();
FopFactory fopFactory = fopFactoryBuilder.build();
ByteArrayOutputStream out = new ByteArrayOutputStream();
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, out);
Source xsl = new StreamSource(this.getServletContext().getRealPath("/WEB-INF/sample.xsl"));
Transformer transformer = tFactory.newTransformer(xsl);
Result res = new SAXResult(fop.getDefaultHandler());
transformer.transform(new StreamSource(new StringReader(xmlData)), res);
response.setContentType("application/pdf");
response.setContentLength(out.size());
response.getOutputStream().write(out.toByteArray());
response.getOutputStream().flush();
} catch (Exception ex) {
throw new ServletException(ex);
}
}
}
コード内部でデータとなる xmlData 文字列を生成して、sample.xsl
変換設定をもとに、FOP でコンテンツ変換を実施して PDF 形式で出力しています。
変換設定
今回の sample.xsl
は、このへん の一部をコピペして、エイヤ!と作成しました。なので生成された PDF の見た目は酷いものです… 実際に使用する時は ガイド に従ってしっかり作りましょう。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" version="1.0">
<xsl:output encoding="UTF-8" indent="yes" method="xml" standalone="no" omit-xml-declaration="no"/>
<xsl:template match="users-data">
<fo:root language="ja">
<fo:layout-master-set>
<fo:simple-page-master master-name="A4-portrail" page-height="297mm" page-width="210mm" margin-top="5mm" margin-bottom="5mm" margin-left="5mm" margin-right="5mm">
<fo:region-body margin-top="25mm" margin-bottom="20mm"/>
<fo:region-before region-name="xsl-region-before" extent="25mm" display-align="before" precedence="true"/>
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="A4-portrail" font-family="Meiryo,Hiragino">
<fo:static-content flow-name="xsl-region-before">
<fo:table table-layout="fixed" width="100%" font-size="10pt" border-color="black" border-width="0.4mm" border-style="solid">
<fo:table-column column-width="proportional-column-width(60)"/>
<fo:table-column column-width="proportional-column-width(30)"/>
<fo:table-body>
<fo:table-row>
<fo:table-cell text-align="center" display-align="center">
<fo:block font-size="150%">
<fo:basic-link external-destination="https://qiita.com/yamachan360">PDF サンプル</fo:basic-link>
</fo:block>
<fo:block space-before="3mm"/>
</fo:table-cell>
<fo:table-cell text-align="right" display-align="center" padding-right="2mm">
<fo:block>
Date: <xsl:value-of select="date"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
</fo:static-content>
<fo:flow flow-name="xsl-region-body" border-collapse="collapse" reference-orientation="0">
<fo:block><xsl:value-of select="body"/></fo:block>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
</xsl:stylesheet>
<xsl:value-of select="date"/>
とか <xsl:value-of select="date"/>
の部分にデータ用 XML の対応する要素が埋め込まれます。このあたりを深く知りたい場合は XSL や XSLT のワードでググってみましょう。
文字化けへの対応
今回のサンプルですが、最初は日本語が文字化けしていました。いろいろ試して、とりあえず以下の2点の修正で表示されました。
まずは fop.xconf
ファイルです。配布されたこのファイルはデフォルト値なので、そのまま読み込んでも動作は一緒!…ではありませんでした。理由までは調べていないのでわかりませんが、この設定ファイルをちゃんと配置し、実行時に読み込むことで文字化けが解消できました。
これに加えて、変換ファイルで日本語のフォント指定が必要でした。とりあえず今回は、以下のような手抜き設定で逃げています。
Web ページへの埋め込み
2022年7月、知り合いから「Web ページへの埋め込みしたい」と相談ありましたので追記します。
この投稿のサンプルは PDF データを生成して Web ブラウザでそのまま表示、もしくはダウンロードしてもらう想定で作成しました。一番シンプルでわかりやすいと思ったので。もちろん、生成したPDFファイルを他の方法で利用することができます。例えばWebページの一部として埋め込むとか。
今回もとても簡単な例で恐縮ですが、GET アクセスに対応して、PDF.js を用いて表示するサンプルコードを作成しましたので、ざっくりご紹介します。
GET アクセスへの対応
この投稿のサンプルは POST メソッドとして作成しましたが、GET アクセスで PDF 情報を得られるようにしておくと利用が楽で、より活用できます。サンプルには、以下のようなコードが既に用意してありましたので、これをそのまま用いましょう。
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
この doGet メソッド、単に doPost につなげるだけで、パラメータ処理を実装していないです。とはいえ GET アクセスは通常の Web アクセスと同じなので、世の中にはたくさんのサンプルが転がっています。なのでパラメータ処理は省略しますね。
※ セッション経由で情報を渡したり、URLパラメータで情報(機密情報であれば暗号化も)を渡す方法が一般的ですかね
ということで、pdfServlet.java はそのまま用います。ただ実行してみるとわかりますが、xml データ生成のあたりでパラメータがないのでNullPointerException(ぬるぽ) エラー出ます。デフォルト値を設定するよう適当に訂正してください。ここも簡単なのでコード紹介は省きますね。
PDF.js による表示
さて、HTML ページは新規に作成します。PDF.js を利用する部分は以下のページを参考にさせていただいています。というか、メイン部分はそのままコピペです。著者の わくい げんき さんに感謝しつつ利用しましょう。
実際の HTML ページのボディ部分は以下のような感じ。
<h1>An inline PDF file</h1>
<p>You can see the PDF below.</p>
<div id="pdf_viewer" style="border:solid 2px gray">
<div id="canvas_container">
<canvas id="pdf_renderer"></canvas>
</div>
</div>
<p>You can see the PDF above.</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.min.js" crossorigin="anonymous"></script>
<script>
var url = '/test02/pdfServlet';
var pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.7.570/pdf.worker.min.js';
var state = {
pdf: null,
currentPage: 1,
zoom: 1.0
}
pdfjsLib.getDocument(url).promise.then((pdf) => {
state.pdf = pdf;
render();
});
function render() {
state.pdf.getPage(state.currentPage).then(function(page) {
var canvas = document.getElementById("pdf_renderer");
var ctx = canvas.getContext('2d');
var viewport = page.getViewport({scale: state.zoom});
canvas.width = viewport.width;
canvas.height = viewport.height;
page.render({
canvasContext: ctx,
viewport: viewport
});
});
}
</script>
実際の表示
さて、上記の index2.html を Edge ブラウザで表示してみます。生成された PDF ファイルが、ページ中に埋め込まれているのが確認できますね。
以下は Chrome ブラウザで index2.html を表示して、開発者ツールでネットワークアクセスを表示したところです。 pdf.min.js が pdfServlet を呼び出して、PDF 情報を入手しているのがわかります。
以上ここまで、追記でした!
ライセンス
この投稿に含まれる私の作成した全てのコードは Creative Commons Zero ライセンスとします。自由にお使いください。あ、sample.xsl はコピペ含むので、ちゃんと書き直したほうが良さそう。
Enjoy!
以上、Servlet を用いて Web サイトで、PDF の動的生成を試してみました。これをベースに、いろいろ機能を追加して遊んでみてください。
【追記】続編 も公開しました
ではまた!