この記事は、2021-06-05に別のブログ媒体に投稿してはてなブックマークで1,000以上ブックマークされた 記事のアーカイブです。
この記事で紹介した手順をライブラリ化して公開しました🎉
こちらの別記事 で使い方など詳しくご紹介していますので、ぜひご参照ください!
最新の登壇スライドバージョンはこちらです。
登壇時の様子がYouTubeに上がっているのでよろしければあわせてご覧ください。
https://www.youtube.com/watch?v=tIxd8C5IDLQ
https://twitter.com/ttskch/status/1397926291127508993
僕が考える現時点でのWebアプリでの帳票印刷のベストプラクティスは、
です。
色々試しましたが、
という条件を満たせる方法は今のところこれしかないという結論です。
この方法を使うと、例えばこんな感じの帳票も簡単かつ保守性高く作れます👍👍👍

下記に実際にアプリを動かせるデモ環境を用意しました。ぜひ触ってみてください。(Herokuの無料プランなので初回起動重いです)
https://svg-paper-example.herokuapp.com/
また、このデモのソースコードは以下のリポジトリで公開していますので、あわせてご参照ください。
デモはPHP(Laravel)で作ってありますが、知見そのものは他の言語・フレームワークでもそのまま流用できるかと思います。
https://github.com/ttskch/svg-paper-example
さて、実装方法について説明する前に、既存の方法のどこがダメだったのというのを簡単に話しておきたいと思います。
僕の観測している範囲だと、Webアプリでの帳票出力の実装には以下の2つの方法が採用されていることが多そうかなと思っています。
はじめはこの方法ですんなり行けると考えていました。
下記のような偉大なる先人の知恵があったので、慣れ親しんだHTML/CSSで帳票をデザインするだけだと。
しかし実際にやってみると、帳票の細かなデザインをHTML/CSSで再現するのがひたすらに面倒臭く、お客さんの要望を細かいところまで再現していった結果、非常に難読なHTML/CSSが出来上がりました😓
考えてみれば、帳票のデザインって多くの場合A4一枚にピッタリ収まることが大前提になっていて、Webにおけるページレイアウトのセオリーとはかけ離れているので、保守性を維持しながらこれを作るのは相当難しいです。
例えばテーブル(表)1つとっても、普段それほど使わないrowspancolspan を大量に使ってめちゃくちゃ複雑なレイアウトのテーブルを組み立てることとかを普通に要求されます。作るだけならまだしも、その後仕様変更でこのテーブルの中にセルを追加(しかも全体がちゃんとA4に収まるように)しないといけなくなったときのことを考えると、遠い目にならざるを得ません。
特に僕のように普段BootstrapなどのCSSフレームワークのレールに乗っかったHTMLしか書いていない人間にはとにかく苦行でしかありませんでした。(普段から複雑なHTMLを書いているデザイナーさんとかにとっては別にしんどくないのかもしれません)
HTML/CSSのメンテが大変すぎるということが分かったので、思い切ってExcelファイルをテンプレートにする方法を試してみました。
帳票を視覚的にデザインできますし、Excelなら(Windows版ならWordも)「縮小して全体を表示」というお馴染みの機能があるのでフォントの縮小についても何も考えなくてよさそうです。
調べてみると、LibreOffice のヘッドレスモードを使えばCLIでExcelファイルのPDFへの変換ができるというこを知り、これなら行けるのではと思いました。
ところがこの方法にも色々と難があり、特に
の3点が致命的でした。
Excel方眼紙は、セルの大きさがフォント1文字分ぐらいならまだギリ許せる(?)のですが、ピクセル単位に近い微妙なデザインを実現しようと思うと地獄のようにセルを小さくする必要が出てきて心が折れます。
LibreOfficeによるPDFへの変換が完璧でない点も、多くの案件において許容不可能でしょう。
というわけでたどり着いたのが、冒頭でご紹介した方法です。
上記2つの方法で満たせなかった
という要求を完璧に満たしてくれる のがこのSVGを使った方法です👍
以下、順を追って具体的なやり方を解説していきます。
まず、Adobe XD やFigma といったUI/UXデザインツールを使って帳票をデザインし、それをSVG形式でエクスポートします。
ファイル > 書き出し > すべてのアートボード でファイル保存のダイアログが出ます。

ここでフォーマット をSVG にして保存すればOKです。
帳票内で画像を使う場合は、下図のように画像を保存 の設定を埋め込み にする必要があります。

埋め込み にすると画像はbase64エンコードされてデータURLとして埋め込まれます。
フレーム単位で選択して、右カラム最下部のExport メニューでエクスポートします。

この際、
Include "id" Attribute にチェックを入れるOutline Text のチェックを外すの2点を忘れないようにしてください。
後述するJSによる調整の段階でid 属性を使いたいのと、そもそもテキストの置換を行うために文字列を<path> タグではなく<text> タグで出力してほしいのでこの設定が必要です。
デモアプリのソースコードの対応するコミットはこちら
SVG形式のテキストファイルができたので、まずはこのテキストをそのままHTMLに埋め込んで画面に出力します。
その際、
の2点を実現するために多少のCSSを書く必要があります。
具体的には以下のような内容でOKです。(これはSCSSで書いてあります)
@page{size: A4 portrait;margin:0;// ヘッダー・フッターが出力されないように}*{margin:0;padding:0;user-select: none;}body{width:210mm;color-adjust: exact;> svg{width:210mm;height:295.5mm;// 297mmだと2ページ目にはみ出してしまうので微調整page-break-after: always;}}// プレビュー用@media screen{body{background:#ccc;margin:0 auto;> svg{background:#fff;box-shadow:0.5mm2mmrgba(0,0,0,.3);margin-top:5mm;}}}このCSSの意味については今回は詳しい解説は割愛します🙏
以下の参考記事を読んでいただければ理解できると思います。
参考:
この時点で、下図のようにAdobe XDでデザインした帳票がそのままの見た目で印刷プレビューっぽく画面に表示でき、ブラウザの印刷機能を使えばそのままの見た目でPDF出力もできる という状態まで来ました👍

デモアプリのソースコードの対応するコミットはこちら
この時点の出力内容は、デザインの時点で埋め込んでおいた%顧客名% のようなプレースホルダー文字列になっているので、出力する前にこれを実際の値に置換する処理を書きます。
PHPの場合は、普通にstr_replace() で一つひとつ置換していけばOKです。画像を差し替える場合はxlink:href="data:image/png;base64,略" といった画像URL部分を置換します。
なお、<text> タグのfont-family 属性の値も置換する必要があることに注意しましょう。Adobe XDやFigmaでデザインしたときにテキストオブジェクトに設定していたフォントがfont-family 属性に書かれていますが、フォント自体が埋め込まれているわけではないので、別途ロードしたWebフォントに置き換えるか、明朝体とゴシック体の使い分けぐらいでいいならserifsans-serif に置き換えてユーザーの環境に任せてしまってもよいかと思います。
この時点の出力結果は以下のような感じです。内容は実際の値に置換されましたが、テキストが枠をはみ出していますし、金額を右寄せにしたりもしたい感じですね。

デモアプリのソースコードの対応するコミットはこちら
JavaScriptから<text> 要素を(id 属性で指定して)いじることで、文字の自動縮小や配置の調整が可能です👍
それぞれ具体的な方法を説明します。
まずは、Excelにおける縮小して全体を表示 相当の挙動をJavaScriptで実装します。
SVGの<text><tspan> 要素にはtextLength という属性があり、テキスト全体の幅を指定することができます。
textLength をコンテンツ幅よりも小さく設定すると、デフォルトの挙動では文字のサイズは変わらず字間が無理矢理詰められて文字と文字が重なってしまうのですが、lengthAdjust 属性にspacingAndGlyphs を設定することでこの挙動を変更することができます。
spacingAndGlyphs は、これ以上字間を詰められなくなると文字自体の幅を縮小してくれます。高さは変わらず幅だけが縮小されるので、狭い領域にめちゃくちゃ長いテキストを入れてしまうと異常に縦長な文字になってしまいますが、その状況では仮に縦横比を維持したまま縮小されたとしても字が小さすぎて読めないと思いますし、帳票印刷という文脈ではほぼ気にしなくていいかなと思います。
注意すべきは、textLength で指定した幅よりもコンテンツの幅のほうが小さい場合、逆に拡大されてしまうことです。これは、
if(elem.clientWidth> config.textLength){ elem.setAttribute('textLength', config.textLength)}といった具合にコンテンツ幅が指定の幅を超えているときのみtextLength を適用するようにすればよいでしょう。
一応こんな議論もあるようです。
lengthAdjust values just for shrinking · Issue #341 · w3c/svgwg
なお、Firefoxでは
があるため、追加でこのような対応 が必要になります。
次に中央寄せ・右寄せについてですが、これはtext-anchor 属性を使うことで実現可能です。
text-anchor 属性をmiddle にすれば、基準となるx座標にテキストの中心が来るようになり、end にすれば、基準となるx座標にテキストの末尾が来るようになります。特に指定しなければデフォルトでstart という値になり、基準となるx座標にテキストの先頭が来るようになります。
「基準となるx座標」とは、text 要素のtransform 属性(のtranslate(<x> [<y>]) 変換関数 )やtspan 要素のx 属性で指定されているx座標のことです。
なので、例えば右寄せを実現したい場合は、
text 要素のtransform 属性やtspan 要素のx 属性を操作して、右端となるx座標まで移動させるtext 要素にtext-anchor="end" を追加するという操作が必要になります。
具体的な実装例はデモアプリの実際のコード をご参照ください。
この時点の出力結果は以下のような感じです。(備考とコメント以外の)テキストが枠内に収まり、中央寄せ・右寄せが適切に施されて見た目がだいぶ整いました。

デモアプリのソースコードの対応するコミットはこちら
最後に、複数行テキストの自動折り返し・自動縮小に対応します。これは正直かなりの力技で対応する必要があります。
具体的には、
<text> 要素の中に行の数だけ<tspan> 要素を挿入して<tspan> 要素のy 属性を一行分ずつ大きくしていくという処理を実装します。
SVGの<text> 要素には改行の概念がないため、このような力技が必要になります😓
SVG 1.1ではこれしかやりようがないのですが、SVG Tiny 1.2 には
<textArea>という要素があり、テキストを自動で折り返してくれるようになっているようです。
また、HTMLの<br>に相当する<tbreak>という要素もあり、かなり簡単に複数行テキストを扱えそうです。
しかし残念ながらGoogle Chromeをはじめ主要なブラウザはSVG Tiny 1.2には対応していません。(要出典🙏)ブラウザがSVG Tiny 1.2に対応しているかどうかはこのページ で確認することができます。画面を開いてテキストが表示されれば対応しているということのようです。
実装方法は色々考えられますが、僕は
font-size の正方形」と見立ててテキストエリアに収まる縦横の文字数を割り出す(プロポーショナルフォントでは誤差が出るけど無視)font-size を少し小さく(0.95倍)して1に戻る、テキストエリアに収まっていれば4へ<tspan> 要素として書き出し、y 属性の値は各行font-size 分ずつ大きくなるようにする(厳密には、行間も考慮して1.2倍したり)という感じの処理を実装しました。実際にはもう少し細かい微調整もしていますが、詳細はデモアプリの実際のコード をご参照ください🙏
ここまでで、無事に完全な帳票が出力できるようになりました🙌

デモアプリ ではリロードする度に出力するテキストの量がランダムに変わるようになっているので、何度かリロードしてみて、どんな内容でも適切に折り返し・縮小されて枠に収まることを確認してみてください😉
デモアプリのソースコードの対応するコミットはこちら
ユーザーのブラウザの印刷機能に依存したくない場合は、 生成したHTMLのPDFへの変換まで含めてアプリ側でやってしまうとよいかと思います。
electron-pdf やGoogle Chromeのヘッドレスモード を使えば特に問題なく実現できるでしょう✋
ChromeのヘッドレスモードによるPDF出力は、Macなら
$ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome--headless --disable-gpu --print-to-pdf http://svg-paper-example.herokuapp.com/print/estimate/見積書(金額あり)って感じで簡単に試せます。
というわけで、僕の考えた最強の帳票印刷について解説しました。
解説は長くなりましたが、やっていること自体はそんなに複雑ではないですし、一度作ってしまえば他のプロジェクトにも同じ仕組みを流用できます。
今のところ自分の中でこれに勝る方法は見つけられていないので、もっといい方法あるよ!という方がいたらぜひご一報ください 💪
おまけというか単なるメモです。試行錯誤の中で分かったことがいくつかあったので書き残しておきます。
縮小して全体を表示 の実現が意外と厄介Excelにおける縮小して全体を表示 相当の挙動はCSSでは実現不可能なので、JSを使う必要があります。
このページなどを参考によさげなライブラリをいくつか(rikschennink/fitty、STRML/textFit 等)試してみましたが、どうもこの手のライブラリはフォントサイズをコンテンツ幅いっぱいにフィットさせる というコンセプトのものばかりで、テキストが多いときには期待どおり縮小されるのですが、逆にテキストが少ないときに枠いっぱいまで拡大されてしまう という挙動になってしまいました
やりたいのはもちろん縮小のみで拡大は一切されてほしくないのですが、標準の機能でそのような挙動を実現できるライブラリは見つけることができませんでした。
なので、複数行テキストのコンテンツにだけmaxSize 的なオプションを使って強引にフォントサイズを固定するようにする必要があります。
https://twitter.com/ttskch/status/1395242578191126529
colspan で結合されているテーブルで各列の幅を固定する方法https://twitter.com/ttskch/status/1394812266822864901
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
