2017年の Advent Calendar の最終日に投稿することとなりました。
普段はサラリーマンをしつつ、アフター5をゆるふわに流しているおじいちゃんです。
最近では、トライアスリート( )として健康増進を、そしてまたある時はOSSとして開発しているE2D3~Excelで利用できる可視化ツール~にコントリビュートしている、そんな感じです。
ここでの手法をアレンジすれば、クライアントサイドのJavaScriptを扱うことが出来るWebサーバでPDF生成が実現できる手法を身に着けることが可能となりますので、ご参考いただけましたら幸いです。
この記事が目的とするところ
記事タイトルの通り、「jsPDFで、無理やり日本語出力を行ってみる」です。
が、このタイトルの意味が伝わりにくいという点と、どうやって既存のOSSを改造したかという2点で質問が来る可能性があるかなぁと考えました。(jsPDFは、そのままでは日本語出力が出来ず、文字をcanvasに変換して画像として取り扱うのが一般的な模様)
なので下記の章立てで構成することとして、記事を書いております。
-
- jsPDFについて(本家のjsPDFを紹介)
- 1.1 どうして使おうと思ったか
- 1.2 使う上での前提条件、メリット/デメリット
- 1.3 どんな機能があるか
- 1.4 使い方
-
- jsPDFを試しに使ってみる
- 2.1 そのまま素直に使ってみる
- 2.2 普通に日本語を使おうとするとどうなるか
- 2.3 日本語が出力できるよう改造して使ってみる
- 2.4 サンプルPDFを出力する
- 2.5 改造したjsPDFについて
・ 2.5.1 使い方
・ 2.5.2 制約事項
・ 2.5.3 応用事例について
・ 2.5.4 今後について
-
- 改造プロセスの概要
- 3.1 改造対象の検討
- 3.2 出力されるPDFフォーマットを分析する
- 3.3 改造方針
- 3.4 ソースコード改造作業について
-
- 参考資料
-
- おわりに
1. jsPDFとは(本家のjsPDFを紹介)
jsPDFは、クライアントサイドのJavaScriptだけでPDFを生成することが出来るライブラリです。GithubのjsPDFリポジトリでMITライセンスとして公開されているものです。
1.1 どうして使おうと思ったか
WebページでPDF帳票を生成させる際によく利用される方式としては、サーバサイドでスクリプト言語(Perl、Ruby、Python、PHP、etc.)により動作するPDFジェネレータ(PDF::API2、PDFKit、wkhtml2pdf、TCPDF、etc.)を用いる手法が一般的だと思います。
しかしこの手法で発生する悩みとして、普段はRubyのPDFKitを使っているんだけど、今度のお客さんのサーバはPHPしか動作しないんだけどなぁなど、サーバ縛り(言語縛り)のために過去の資産を活かせないなどのケースに遭遇したりします。
そこで、クライアントサイドJavaScriptだけでPDFが生成できるjsPDFに着目をしました。
1.2 使う上での前提条件、メリット/デメリット
jsPDFを利用するには、システムを利用する側および開発者それぞれに必要な要件があります。
- 必要な前提条件
- jsPDFを利用するための要件
・ HTML5アプリを利用できるブラウザが使えること
・ ブラウザでJavaScriptを有効化していること - 開発する人の要件
・ HTMLで簡単なWebページ作成が出来ること
・ クライアントサイドのJavaScriptを書けること
- jsPDFを利用するための要件
また、jsPDFを使う方式について、他の方式と比較した時の得失(メリット/デメリット)としては下記の点が考えられます。
- PDFジェネレータとしてのメリット/デメリット
-
メリット
・ 無料で使用できる(MITライセンス)。
・ 実はローカル(Webサーバにファイルを置かずとも)実行できるため、簡易PDFジェネレータとして利用できる。
・ PDFの標準的な機能だけで帳票を作るのであれば、簡単に使うことが出来る。
・ JavaScriptの利用が可能なレンタルサーバで利用できるため、多くのレンタルサーバやブログサービスで使用することが出来る。 -
デメリット
・ 多機能で便利なのだが、日本語の解説資料が少ない。(本記事が好評だったら別途作っときます。)
・ 改造するにはPDFのフォーマットに関する知識を使うので、勉強が必要。
・ クライアントサイドでの処理なので、DBの情報等を直接PDFで印字するなどの場合、ひと手間増える。
(最近だとPostgreSQLのPostgRESTのようなREST APIも出始めてるので、楽になりつつあるが、、)
-
1.3 どんな機能があるか
-
公式ドキュメント で紹介されていまが、主要な機能としては、次のものです。
- テキスト出力
- 基本図形(直線、円、楕円、四角形)出力機能
- 塗りつぶし図形出力機能
- 画像埋め込み機能
- PDFビューアー機能
1.4 使い方
-
必要な材料を集める(方法1または方法2のいずれか一方を選択してください)
-
【方法1】本家のjsPDF本体(Version1.3.2)のリポジトリから直接入手する。
(本日2017/12/25現在はversion1.3.5が最新ですが、本記事での日本語化改造のため1.3.2を使用します)
-
【方法2】改造後リポジトリからzipをダウンロードする。
(本記事の手法で改造した結果をそのまま使いたい場合はこちらで。)
-
-
ファイルの配置について
- 上記でダウンロードしたZIPファイルを解凍して、必要なファイルをWebサーバにアップロードします。
(Webサーバを利用できない方は、ローカルPCに解凍しても同じことを試すことが出来るはずです) - サンプルページと同じ配置 を下記に示します。ファイル名の先頭にアスタリスク * 表示があるものは、本家で入手できるものを変更していますので、サンプルページのデータを参考にコンテンツ作成やソースコードの改造をしてみてください。
- 上記でダウンロードしたZIPファイルを解凍して、必要なファイルをWebサーバにアップロードします。
https://junichiwatanuki.github.io/jsPDFjp/ver_1.3.2/
│
│ <ファイル名> # 本記事での動作確認のための補足説明
│
│* index.html # 『 2. jsPDFを試しに使ってみる 』でのサンプル
│* sample-01.html # 『 2.1 そのまま素直に使ってみる 』でのサンプル
│* sample-02.html # 『 2.2 普通に日本語を使おうとするとどうなるか 』でのサンプル
│* sample-03.html # 『 2.3 日本語が出力できるよう改造して使ってみる 』でのサンプル
│* sample-04.html # 『 2.4 サンプルPDFを出力する 』でのサンプル
│
└─module
└─libs # 本家で入手した「jsPDF-1.3.2.zip」の、「\jsPDF-1.3.2\libs」をそのままコピーする
│ css_colors.js
│ deflate.js
│ html2pdf.js
│ javascript.js
│* jspdf.debug.js # 本家で入手した「jsPDF-1.3.2.zip」の、「\jsPDF-1.3.2\libs」をそのままコピーする
│* jspdf.debug_original.js # 上記をコピーしたファイル。
│* jspdf.min.js # 本家で入手した「jsPDF-1.3.2.zip」の、「\jsPDF-1.3.2\libs」をそのままコピーする
│* jspdf.min_original.js # 上記をコピーしたファイル。
│ polyfill.js
│
├─canvg_context2d
│ (本家のファイル構成のままなので省略)
│
├─Downloadify
│ (本家のファイル構成のままなので省略)
│
├─html2canvas
│ (本家のファイル構成のままなので省略)
│
├─png_support
│ (本家のファイル構成のままなので省略)
│
└─require
(本家のファイル構成のままなので省略)
- 出力したいPDFのコンテンツについて
・配置したHTMLを編集することで、生成するPDFを作り上げていきます。
https://junichiwatanuki.github.io/jsPDFjp/ver_1.3.2/
│
│ <ファイル名> # 本記事での動作確認のための補足説明
│
│* index.html # 『 2. jsPDFを試しに使ってみる 』でのサンプル
│* sample-01.html # 『 2.1 そのまま素直に使ってみる 』でのサンプル
│* sample-02.html # 『 2.2 普通に日本語を使おうとするとどうなるか 』でのサンプル
│* sample-03.html # 『 2.3 日本語が出力できるよう改造して使ってみる 』でのサンプル
│* sample-04.html # 『 2.4 サンプルPDFを出力する 』でのサンプル
│
└─module
└─libs
│ (以下省略)
・具体的な作り方は、jsPDFのコマンドを参考にしながら、目的のPDFを作り上げていきます。
(最終的にjsPDFに渡せばよいので、Webとの連携などは自由に行うことが可能です。)
※ 下記でいうと、doc.textの引数部分で、PDFで出力する際の文字列を定義しています。
<body>
<!-- 出力したいPDFを定義する。 -->
<script type="text/javascript">
function createPDFObject(varIntX){
alert("作成に少々時間がかかる場合があります。\nこのまま少々お待ちくださいませ。");
//印字文字列情報の生成
var doc = new jsPDF('p', 'pt', 'a4', false);
doc.setFontSize(20);
doc.text(60, 150, 'Musashino-Advent-Calendar_2017');
doc.text(100, 200, 'Try Japanese output with jsPDF.');
doc.save( '武蔵野AdventCalendar_2017の最終日_001.pdf')
}
</script>
- jsPDFのコマンドについて
http://rawgit.com/MrRio/jsPDF/master/docs/global.html- 日本語を出力するように改造したバージョンについてはこちらも参照ください。
2. jsPDFを試しに使ってみる(どうやって使うものなのかを紹介)
投稿日が12/25でクリスマスということで、最終的に下記リンク先で生成するようなPDF(A5横)のクリスマスカードを生成したいと思います。
まずは生成したいクリスマスカードのイメージ画像を下記に掲載します。
(上記イメージを生成したPDF出力の見本は、こちらのダウンロードリンクから閲覧してください)
また、以降でjsPDFを使った出力例の紹介においては、上述した1.4でのファイル配置に合わせていただくことで、お手元の環境でも再現ができるかと思います。
※ なおクリスマスツリーの画像は、フリー素材の『イラストイメージ』さんのものを使わせていただきました。
画像のダウンロードリンクは、https://illustimage.com/?dl=1460です。
2.1 そのまま素直に使ってみる
まずは一切改造することなく、半角英数字のみの文章を生成してみて、基本機能を確認してみることにします。試しに生成するPDFを、下記の通りとします。
(上記イメージを生成したPDF出力の見本は、こちらのダウンロードリンク から閲覧してください)
- ・HTMLソース(中身の説明はこちらを参照願います)
<!DOCTYPE html>
<head>
<title>Qiita武蔵野AdventCalendar_2017の動作確認ページ1</title>
<meta charset="utf-8">
<!-- まずは、jsPDFを読み込んでおく。以降の改造の都合、debugのほうを利用する。 -->
<script src='./module/libs/jspdf.debug_original.js' type='text/javascript'></script>
</head>
<body>
<!-- 出力したいPDFを定義する。 -->
<script type="text/javascript">
function createPDFObject(varIntX){
alert("作成に少々時間がかかる場合があります。\nこのまま少々お待ちくださいませ。");
//印字文字列情報の生成
var doc = new jsPDF('p', 'pt', 'a4', false);
doc.setFontSize(20);
doc.text(60, 150, 'Musashino-Advent-Calendar_2017');
doc.text(100, 200, 'Try Japanese output with jsPDF.');
doc.save( '武蔵野AdventCalendar_2017の最終日_001.pdf')
}
</script>
<!-- 出力ページの見出しを定義する。 -->
<h1>jsPDFによる出力テストページ</h1>
<!-- createPDF関数を呼び出して、PDFを出力させるリンクを載せる。 -->
<a href="#" onclick="createPDFObject();">このリンクを開き、jsPDFで生成するPDFを取得する。</a>
</body>
</html>
2.2 普通に日本語を使おうとするとどうなるか
次に、本家で配布しているjsPDFをそのままとして、日本語の文章を出力対象とします。出力を期待するPDFとして下記をイメージします。(すでに日本語は文字化けすることがわかっているので、文字化け後のイメージを掲載しています。)
<!DOCTYPE html>
<head>
<title>Qiita武蔵野AdventCalendar_2017の動作確認ページ2</title>
<meta charset="utf-8">
<!-- まずは、jsPDFを読み込んでおく。以降の改造の都合、debugのほうを利用する。 -->
<script src='./module/libs/jspdf.debug_original.js' type='text/javascript'></script>
</head>
<body>
<!-- 出力したいPDFを定義する。 -->
<script type="text/javascript">
function createPDFObject(varIntX){
alert("作成に少々時間がかかる場合があります。\nこのまま少々お待ちくださいませ。");
//印字文字列情報の生成
var doc = new jsPDF('p', 'pt', 'a4', false);
doc.setFontSize(20);
doc.text(60, 150, '【2017年12月25日の投稿記事】');
doc.text(100, 200, '⇒jsPDFで、無理やり日本語出力を行ってみる');
doc.save( '武蔵野AdventCalendar_2017の最終日_002.pdf')
}
</script>
<!-- 出力ページの見出しを定義する。 -->
<h1>jsPDFによる出力テストページ</h1>
<!-- createPDF関数を呼び出して、PDFを出力させるリンクを載せる。 -->
<a href="#" onclick="createPDFObject();">このリンクを開き、jsPDFで生成するPDFを取得する。</a>
</body>
</html>
2.3 日本語が出力できるよう改造して使ってみる
-
jsPDF(JavaScript)改造後のソース(中身の説明はこちらを参照願います)
(※ 改造前との差分についてはこちらを参照願います)
<!DOCTYPE html>
<head>
<title>Qiita武蔵野AdventCalendar_2017の動作確認ページ3</title>
<meta charset="utf-8">
<!-- まずは、改造したjsPDFを読み込んでおく。ソースの中身を確認しながらなのでdebugのほうを利用する。 -->
<script src='./module/libs/jspdf.debug.js' type='text/javascript'></script>
<!-- 次に、改造後の仕様(半角文字を全角に置き換える関数を用意しておく) -->
<script type="text/javascript">
// utf16をHexにする。
function utf16_to_hexcode(str){
var strText = str.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
});
var tmpCode = "";
var strCode = "";
var strTmpCode = "";
var arr = strText.split('');
var intArr = Number(arr.length);
for( var i = 0; i < intArr; i ++ ){
strCode=strCode + escape(arr[i]).replace( /%u/g , "" );
}
return strCode;
};
// 半角を全角に変換する。
function han2zen(str) {
str = str.toString();
var zenkaku = '';
str.split('').forEach(function (s) {
zenkaku += String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
});
return zenkaku;
}
</script>
</head>
<body>
<!-- 出力したいPDFを定義する。 -->
<script type="text/javascript">
function createPDFObject(varIntX){
alert("作成に少々時間がかかる場合があります。\nこのまま少々お待ちくださいませ。");
//印字文字列情報の生成
var doc = new jsPDF('p', 'pt', 'a4', false);
doc.setFontSize(20);
doc.text(60, 150, utf16_to_hexcode('【2017年12月25日の投稿記事】'));
doc.text(100, 200, utf16_to_hexcode('⇒jsPDFで、無理やり日本語出力を行ってみる'));
doc.save( '武蔵野AdventCalendar_2017の最終日_003.pdf')
}
</script>
<!-- 出力ページの見出しを定義する。 -->
<h1>jsPDFによる出力テストページ</h1>
<!-- createPDF関数を呼び出して、PDFを出力させるリンクを載せる。 -->
<a href="#" onclick="createPDFObject();">このリンクを開き、jsPDFで生成するPDFを取得する。</a>
</body>
</html>
2.4 サンプルPDFを出力する
<!DOCTYPE html>
<head>
<title>Qiita武蔵野AdventCalendar_2017の動作確認ページ4</title>
<meta charset="utf-8">
<!-- まずは、改造したjsPDFを読み込んでおく。 -->
<script src='./module/libs/jspdf.debug.js' type='text/javascript'></script>
<!-- 次に、改造後の仕様(半角文字を全角に置き換える関数を用意しておく) -->
<script type="text/javascript">
// utf16をHexにする。
function utf16_to_hexcode(str){
var strText = str.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
});
var tmpCode = "";
var strCode = "";
var strTmpCode = "";
var arr = strText.split('');
var intArr = Number(arr.length);
for( var i = 0; i < intArr; i ++ ){
strCode=strCode + escape(arr[i]).replace( /%u/g , "" );
}
return strCode;
};
// 半角を全角に変換する。
function han2zen(str) {
str = str.toString();
var zenkaku = '';
str.split('').forEach(function (s) {
zenkaku += String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
});
return zenkaku;
}
</script>
</head>
<body>
<!-- 出力したいPDFを定義する。 -->
<script type="text/javascript">
var BackgroungImage = 'data:image/png;base64,iVBORw0KGgoAA■■■■■ 途中は長いので省略 ■■■■■5ErkJggg==';
//出力ページに対する引数
function createPDFObject(varIntX){
alert("作成に少々時間がかかる場合があります。\nこのまま少々お待ちくださいませ。");
//印字文字列情報の生成
var doc = new jsPDF('l', 'pt', 'a5', false);
doc.addImage(BackgroungImage, 'png', 0, 0, 595.28, 420.945, undefined, 'slow');
doc.setFontSize(20);
doc.text(10, 50, utf16_to_hexcode('メリークリスマス♪'));
doc.text(30, 80, utf16_to_hexcode('武蔵野ACからの'));
doc.text(30, 100, utf16_to_hexcode('クリスマスカードです。'));
doc.save( '武蔵野AdventCalendar_2017の最終日_004.pdf')
}
</script>
<!-- 出力ページの見出しを定義する。 -->
<h1>jsPDFによる出力テストページ</h1>
<!-- createPDF関数を呼び出して、PDFを出力させるリンクを載せる。 -->
<a href="#" onclick="createPDFObject();">このリンクを開き、jsPDFで生成するPDFを取得する。</a>
</body>
</html>
2.5 改造したjsPDFについて
2.5.1 使い方
1.4 で紹介したファイル配置を行うだけで使うことが出来ます。コンテンツについては、1.4の【方法2】改造後リポジトリからzipをダウンロードした際に含まれるhtmlファイルのパラメータを変更してみるところからお試しいただくのが近道かもしれません。
2.5.2 制約事項
jsPDFはもともと半角英数をPDFかするために作られているみたいであり、今回の改造ではすべてを全角で出力するよう改造しただけです。そのため、半角英数をPDF化したい場合はjsPDFに渡す場合に全角に変換してやる必要があります。
そのため、今回の事例でサポートしていない半角文字列(例えば半角スラッシュとか)を含む原稿をPDF化したい場合は、下記の関数などを適宜作り変えてご対応いただく必要があります。
// utf16をHexにする。
function utf16_to_hexcode(str){
var strText = str.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
});
var tmpCode = "";
var strCode = "";
var strTmpCode = "";
var arr = strText.split('');
var intArr = Number(arr.length);
for( var i = 0; i < intArr; i ++ ){
strCode=strCode + escape(arr[i]).replace( /%u/g , "" );
}
return strCode;
};
// 半角を全角に変換する。
function han2zen(str) {
str = str.toString();
var zenkaku = '';
str.split('').forEach(function (s) {
zenkaku += String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
});
return zenkaku;
}
2.5.3 応用事例について
実際に使われているシーンとしては、ぼくが実行委員をしている市民スポーツ大会(小金井アクアスロン大会)の完走証公開ページ があります。(しかし、前述でのデメリットで記載の通り、背景画像サイズが大きいため、Webブラウザの処理速度次第では生成完了まで数分かかる場合もあります。)
2.5.4 今後について
上述(2.5.3)の目的に合わせた最小限の改造なので、長期的には半角も全角も意識することなく動作するものを作りたい考えはある。(が、結構稼働がかかるかも~なので、定年退職以降のお楽しみとしてネタを溜めておくことにするかな。)
3. 改造プロセスの概要
改造プロセスは、目的に合わせたピンポイントで実施していました。(なので、概要にとどめておきます。)
将来の追加改造への備忘も兼ねて、実施記録を記録しておきます。
3.1 改造対象の検討
jsPDFでは文字列をPDFに変換する機能を利用していて、日本語出力における不具合を改修する方法を考えてみます。入力情報はHTML文字列(JavaScriptで定義する文字列)で出力されるのはPDFで表示される文字情報であるということから検討をしてみます。
HTMLやJavaScriptはUTF-8で作成していますが、ブラウザ上を経てjsPDFで出力される際は、PDFDocEncodingかUnicodeでエンコーディングされてしまいます。(UnicodeのUTF16BE)
そのため、『出力したい文字列』と『出力された文字列』の比較においては、文字コードやエンコーディングに注意して、想定通り変換されているか否かをチェックしていきます。
3.2 出力されるPDFフォーマットを分析する
pdf1.3の構造について、概要レベルで良いので知っておく必要があります。4章に参考資料を掲載しましたので不足する情報をつまみ食いしておくと作業が楽になります。(今回では取り扱わないパターンでPDFを出力したい場合などでは必要な局面も出てくると思います。)
例えば、2.1で確認した、doc.textの引数が、半角英字から全角日本語になったときの差分)と変更してみたときのPDFがどのように変更したかを見たいと思います。
doc.text(pos_horizontal ,pos_vertical , 'Musashino-Advent-Calendar_2017');
↓ ↓ ↓ ↓ ↓
doc.text(pos_horizontal ,pos_vertical , 'メリークリスマス♪');
上記の違いが、PDF出力の違いにどのように表れているかを見てみる(お手持ちのDiffツールでPDFの差分を取ってみてください。)
※ PDF1.3はアスキーとバイナリが混ざっていて、今回はテキストエディタでも違いがわかる部分です。
※ 上記をにらんでいると、jsPDFの出力部分を無理やりUnicode化し、中の文字列をUTF16のHEXにしてやればよいことが分かると思います。
3.3 改造方針
jsPDFでUnicode処理が実装されていない。そのため、jsPDFに文字情報を渡す前にUTF16に変換して、jsPDF本体を改造してPDFでUnicode出力する作戦とする。(好みの問題だが、jsPDFを直接改造するよりは精神的負担が軽いのでお気軽っていうだけ)
また、font周りについては、jsPDFで処理しなければならない。そのため、本体側を直接改造することとする。(改修ではなく改造行為です。)
実際の作業としては、下記のような工程を踏んでいます。(デバッグ作業に近い作業かも。)
- ひたすら関数、変数を追いかけ、出力されるPDFが想定された通りに表示されるかを確認する。の繰り返し。
- 動作試験まで完了したら minfyして完了。
(このあたりの作業は、人により様々なやり方があると思うので省略します。)
4. 参考情報
[参考1] PDFバージョンの比較サンプル
[参考2] 手書きPDF入門
[参考3] LEADTOOLS
[参考4] PDF構造 概要
[参考5] wikipedia-PDF
[参考6] 詳細PDF入門
[参考7] PDF構造解説 (O'reilly)
5. おわりに
最後のほうは話すと長くなってしまいそうでしたので簡潔にまとめてみました。(前半はすこし回りくどくなってしまい、わかりにくくなっている箇所があるかもしれません。)
今回の事例紹介は、自分がボランティア活動をしている団体の帳票システムをターゲットとしたものでしたが、反響があれば時間を作って本格版にチャレンジしてみたいと思っています。(が、トライアスロンのトレーニングが忙しくて、趣味のプログラミングに時間を割けないので、いつになるかはわかりません。)
ご興味をお持ちいただけましたら、新年に向けて、ブログサイトでPDF年賀状を生成させるJavaScriptを埋め込んでみるなど、お楽しみいただけましたら幸いです。
今年も残りわずかではありますが、皆さま良いお年をお迎えくださいませ。
最後までご覧いただきましてありがとうございました。