前書き
Rails5 + Reactの環境でHTML文字列をPDFにして、フロントでダウンロードさせる処理を実装したので
そのやり方についてご紹介します。
Rails5 + Reactと言いましたが、ReactとしてはRailsにHTML文字列をPOSTで渡すだけです。
なので、正確にはRails5にHTMLをPDFにするAPIを作り、それをReactで呼ぶようにする。って感じです。
環境の作成
すでに、EC2、またはLinuxサーバなどでRailsが動作している環境を前提とします。
wkhtmltopdf
wkhtmltopdfは、HTMLを解釈してPDFヘ変換してくれるライブラリです。
W eb K it + html + to + pdf
名前見た感じで、Webkit使って、HTMLをPDFに変換してくれそうです(実際そうです)
導入は簡単です。
# 本体のダウンロード
$ wget https://downloads.wkhtmltopdf.org/0.12/0.12.4/wkhtmltox-0.12.4_linux-generic-amd64.tar.xz
# 解凍
$ xz -dv wkhtmltox-0.12.4_linux-generic-amd64.tar.xz
# インストール
$ tar -xf wkhtmltox-0.12.4_linux-generic-amd64.tar
上記でインストール完了です。
下記のコマンドで確認してください。
同じように表示されればインストール完了です。
$ wkhtmltopdf -V
wkhtmltopdf 0.12.4 (with patched qt)
※注意事項
wkhtmltopdfは内部でWebKitを使っているのですが、それのバージョンが割と古いもので
最新のHTMLやCSSは基本的に対応していません。
今回の案件では、ハガキ印刷のため縦書きを実現しようと思ったのですが、writing-modeが利用できませんでした。(他はflex等です。)
そのため、後述しますが、少し頭を古く考えて、縦書き用JSライブラリを使用しています。
他に、最新のCSSだからできないっ、という場合にはJSライブラリを探してみることを推奨します。
ちなみに、CDNは問題なく利用できます。
(新しいHTMLをPDFにしてくれるやつが早く欲しいです。。。)
PDFkitの導入
Gemfile追記
Gemfileに下記を追記し、bundle installしてください。
pdfkitはwkhtmltopdfをRailsから利用しやすくしたラッパーです。
wkhtmltopdf-binayはwkhtmltopdfを利用するのに必要なライブラリです。
gem 'pdfkit'
gem 'wkhtmltopdf-binary'
初期化準備
プロジェクトのconfig/initializers/ に pdfkit.rb を作成し
以下を記載してください。
# config/initializers/pdfkit.rb
PDFKit.configure do |config|
config.wkhtmltopdf = '{wkhtmltopdfのパス}'
config.default_options = {
:page_size => 'A4',
:print_media_type => true,
:no_background => true,
}
end
wkhtmltopdfのパスは、以下のコマンドで確認できます。
$ which wkhtmltopdf
API作成
Rails側でAPI用にコントローラを作成します。
内容は以下です。
class PrintController < ApplicationController
protect_from_forgery with: :null_session
# Index
def index
# パラメータ取得
@html = params['html']
@orientation = params['orientation']
@page_size = params['pageSize']
@page_width = params['pageWidth']
@page_height = params['pageHeight']
@margin_top = params['marginTop']
@margin_left = params['marginLeft']
@margin_right = params['marginRight']
@margin_bottom = params['marginBottom']
# PDF変換
pdf = PDFKit.new(@html,
:encoding => 'UTF-8',
:orientation => @orientation,
:page_size => @page_size,
:page_width => @page_width,
:page_height => @page_height,
:margin_top => @margin_top,
:margin_left => @margin_left,
:margin_right => @margin_right,
:margin_bottom => @margin_bottom,
)
# PDF出力
send_data pdf.to_pdf,
type: 'application/pdf',
disposition: "inline"
end
end
@htmlにReactからのHTML文字列が格納されます。
他のパラメータは基本的に自由に記載してください。
だいたい、上の感じで思った感じのPDFは表示されると思います。(結構癖があるのでレイアウト修正は大変ですが。。。)
実装
では、実際にAPIをReactから呼び出して見ます。
今回は以下のようなHTMLを用意しました。
<!DOCTYPE html>
<html lang = "ja">
<head>
<meta charset = "utf-8">
<title></title>
<script src="https://cdn.rawgit.com/tategakibunko/old-nehan/master/nehan.js" ></script>
<link href="https://cdn.rawgit.com/tategakibunko/old-nehan/master/nehan.css" />
<script>
window.onload = function () {
Nehan.LayoutMapper.start('div', {});
};
</script>
</head>
<body>
Hello HTML TO PDF!!
<div class="lp-vertical lp-height-700 lp-font-size-20">
縦書きだ 日本語ならば 縦書きだ
</div>
</body>
</html>
Nehan.jsは、JavaScriptで簡単に縦書きが実装できるライブラリです。
CSSのwriting-modeが利用できないため急遽利用しました。
上記のHTMLを実際にReactからAPIへ文字列としてPOST通信を行います。
axios.post(PRINT_API_URL, {
html: {HTMLSTRING},
orientation: 'portrait',
pageSize: 'Letter',
pageWidth: '100',
pageHeight: '148',
marginTop: '0',
marginLeft: '0',
marginRight: '0',
marginBottom: '0',
},
{
responseType: 'blob'
}
).then((res) => {
const blob = res.data;
const url = window.URL || window.webkitURL;
const blobURL = url.createObjectURL(blob);
const at = document.createElement('a');
at.download = 'sample.pdf';
at.href = blobURL;
at.click();
});
非同期通信で先ほどのAPIを呼び出します。
htmlに、HTML文字列を配置し、あとは必要なパラメータを付与して呼び出します。
ReactでHTML文字列を取得するには、innerHTMLなりの方法があります。
自分は、Bodyの中はReactでhiddenの中にレンダリングして、あとで
document.getElementByIdで取得して
通信を走らせる前にhtmlのheadの部分とhtmlの閉じタグを文字列結合しています。
いろいろやり方はありそうですね!
PDFのバイナリデータがresponseとして帰ってくるので、responseTypeをblobとすることで
あとの、thenの中の処理が行いやすくなります。
thenの中では、responseのPDFを保存するリンクを追加したaタグを作成し、あとでclickするという原始的な方法です。
これで、落ちてきたPDFが下記になります。
以上です。
後書き
ここまでくれば、HTMLをPDFにすることに関しては、ほとんど問題はないと思います。
wkhtmltopdfとか、いろんなライブラリが今後出ればもっと楽にできるようになると思いますが
さくっと、実装するには現状はこれがベストなのかなと、思います。