Help us understand the problem. What is going on with this article?

そろそろ真面目に、HTMLで帳票を描く話をしようか

More than 3 years have passed since last update.

帳票といえばPDFとして生成するのが一般的でしょうか? でも、2015年の今、あえてHTMLで描くのがホットです(個人的に)。ミリ単位で設定された高度な帳票も、CSSを駆使して簡単に作ることができます。業務システムでもモダンブラウザを選択することが増え、@pageなども積極的に使えるようになったこと、SPA(Single Page Application)の台頭、いろいろと条件が揃ってきました。

書いてたら結構長くなっちゃったので、さくっとコードだけ見たい方は、Paper CSSリポジトリをどうぞ。

Artboard 1.png

はじめに

HTML帳票のメリット

2015年現在、HTML帳票を選択する幾つかのメリットがあります。

  • ライブリロードで、リアルタイムなスタイル調整
  • バックエンドではなくフロントエンドで生成できる

前者は、gulpやGruntの普及で、CSSにしろHTMLにしろ、リアルタイムにプレビューできる環境が揃ったことが大きいです。Webサイトをインブラウザデザインするように、帳票のデザインができてしまうというのは、PDFだとありえなかった話。これによって、帳票作成の効率は相当上がりました。(個人的な体感としては、10倍速い感じ)

SPA時代の帳票生成

PDFを生成するのはバックエンドの仕事です。ですが、業務システムがSPAに移行し、フロントエンド中心の構成になるにつれて、帳票生成用に別プログラムを置くことが負担となっていきます。また、PDFの生成は非常に重い処理であり、転送の帯域も馬鹿になりません。その点、フロントであれば、通常のSPA同様にAPI経由でデータだけ送って、ブラウザ内で印刷用のHTMLを構成すれば良い話です。

対象ブラウザと帳票

モダンブラウザを対象に考えます。残念ながらSafariはPaged Media対応が限定的で、印刷時のマージンをコントロールできません。対象から外しています。

  • Chrome
  • Firefox
  • Edge

また、本稿は通常のWebページの印刷については書きません。あくまでも、最初から印刷を目的に生成されるHTMLを念頭に考えます。

HTML印刷の基本

改ページの制御

印刷時、要素の後に改ページを入れたい場合はpage-break-afterを使います。帳票の場合、ページ(シート)ごとに、<section>タグで囲ってしまうとシンプルです。

.sheet {
  page-break-after: always;
}
<section class="sheet">
  <!-- 印刷内容 1枚目 -->
</section>
<section class="sheet">
  <!-- 印刷内容 2枚目 -->
</section>

ちなみに、最後の<section>の後に改ページ指定があっても、空白ページは出力されません。その点はご心配なく。

always以外にも、いろいろ指定できますが、高度な組版をするわけではなくて、印刷したいのは帳票です。これだけ覚えておけばOK。同様に、page-break-beforepage-break-insideもあまり出番はないはずです。

背景は印刷されない

CSSで指定した背景(backgroundプロパティ)は出力されません。ブラウザの設定を変更して印刷することは可能ですが、ユーザに設定を強いるのは得策ではないので、背景なしで成立するデザインにするべきでしょう。

参考: 黒ベタに塗りたい

Full blockキャラクタ(█、U+2588)を使う方法と、borderを太くして使う方法があります。ただし、どちらの方法も前面に文字を重ねても白抜きにはできません。borderの方が指定が楽です。例:
1444017569-D4201161-C941-4F45-B9B3-90AAAF071489.png

<span class="green-label">緑のラベル</span>
.green-label {
  border: 1px solid green;
  border-left-width: 10px;
  padding: 1px 3px;
}

1px未満の線は印刷されない

背景(background)と異なり、borderを指定すれば印刷でも必ず出力されます。ただし、最小の細さは1pxです。

1pxは案外太く感じられるかもしれません。特に、Retinaディスプレイが当たり前になりつつある昨今、もう少し細くしたいこともあるのですが、現状では「印刷されない」と思っておいてください。

table {
  border: 1px solid black; /* 1pxが最小 */
}

ブラウザが出力する(余計な)ヘッダ/フッタ

Firefoxで印刷しようとすると、デフォルトで次のような項目が印刷されてしまいます。

  • タイトル
  • URL
  • ページ番号
  • 印刷日時

1444020994-682010EC-E9D8-4BE2-BFDF-A4DFC2F78745.png

帳票としては邪魔なので、ユーザに外してもらうしかありません。次のスクリーンショットは、Firefoxの場合の例です。

1444020946-19155BF1-DA69-477B-B72B-5EAA6A540D47.png

ただし、 Chromeに限っては回避方法があります@pagemarginをゼロに設定すればOKです。これで、ブラウザ機能のヘッダ/フッタ印刷は無効になります。@pageについての詳細は後述。

@page {
  size: A4;
  margin: 0;
}

ページ番号の出力

帳票が複数枚にわたる場合、プリンタの紙詰まりなどに備えて、ページ番号をつけたい場合があります。

  • HTMLに直接書く
  • JavaScriptで
  • CSSのカウンタで

など、方法はいくつかありますが、CSSで設定するのが簡単です。次は、各ページ右下に表示する場合の例です。

body {
  counter-reset: sheet; /* カウンタの初期化 */
}
.sheet::after {
  position: absolute;
  bottom: 0;
  right: 0;
  counter-increment: sheet;
  content: "ページ " counter(sheet);
}

複数ページと共通ヘッダ/フッタ

position: fixedを使うと、印刷時に全ページ印刷されるブラウザがあります。ただし、Chromeは非対応です。これを除くと、実のところ、あまりキレイな解決策がありません。つまり、コンテンツが1ページから溢れる場合は、JavaScriptで検知して次の<section>に続きを入れるなど、手動での対応になります。全ページ共通のヘッダ/フッタについても、各ページに重複して書き入れる必要があります。

なお、フロントエンドでHTMLを生成する場合は、DOM要素の高さを動的に取得可能です。これを元にページ送り位置を調整するのもひとつの手ですね。

印刷メディア設定はCSSで

@media

印刷時のみのCSS指定は、次のように書けます。

@media print {
  /* この中に印刷時のみの指定を書く */
}

ただ、これは一般のWebページの印刷時には便利なのですが、印刷のみを目的とする場合はいちいちこう書く必要もなく、むしろプレビュー時の表示のみを@media screen指定する方が、シンプルになります。

/* ここに印刷用スタイル指定を書く */

@media screen {
  /* この中に画面プレビュー用のスタイル指定を書く */
}

@page

意外と知られていないCSSの機能のひとつかもしれません。下記のように書くと、印刷時の紙サイズを指定できます。アプリケーション内で帳票サイズが混在している場合、印刷ダイアログでいちいち紙サイズを指定しなおさなくて良いので便利です。※ブラウザの設定によっては無視される場合もあり

@page { size: A5 }           /* A5タテ */
@page { size: A5 landscape } /* A5ヨコ */
@page { size: A4 }           /* A4タテ */
@page { size: A4 landscape } /* A4ヨコ */
@page { size: A3 }           /* A3タテ */
@page { size: A3 landscape } /* A3ヨコ */

数値でサイズを指定する場合は次のようになります。

@page { size: 210mm 297mm } /* A4タテと同じ */

マージン指定もできます。次は、10mmに設定する場合です。

@page {
  size: A4;
  margin: 10mm;
}

なお、前述のようにmarginをゼロにすると、Chromeでの印刷時にブラウザ機能のヘッダ/フッタ出力を抑制できます。

Chromeの縮尺バグを回避する

2013年から続くChromeのバグがあり、CSSで要素サイズを指定すると、実際に印刷されるサイズは1割ほど大きくなってしまいます。このバグを回避する最も簡単な方法は、BODY要素の幅を明示的に指定することです。

@page {
  size: A4;
  margin: 0;
}
@media print {
  body {
    width: 210mm; /* needed for Chrome */
  }
}

なお、マージンを指定する場合は、マージン分を減らした値にします。

@page {
  size: A4;
  margin: 10mm;
}
@media print {
  body {
    width: 190mm; /* needed for Chrome */
  }
}

プレビューとライブリロード

印刷ダイアログをいちいち開いていたのでは開発の効率が上がりません。画面上で表示した場合も、印刷時とほぼ同じになるようにします。灰色の背景に用紙1枚ごとにドロップシャドウ付きで表示すると「紙っぽく」できます。プレビュー用のスタイルは@media screenに指定すればOK。

プレビュー

例えば、次のようなCSSなら、画面上でも簡単にプレビューができますね。

.sheet {
  width: 210mm;
  height: 296mm; /* 1mm余裕をもたせる */
  page-break-after: always;
}
/* プレビュー用のスタイル */
@media screen {
  body {
    background: #eee;
  }
  .sheet {
    background: white; /* 背景を白く */
    box-shadow: 0 .5mm 2mm rgba(0,0,0,.3); /* ドロップシャドウ */
    margin: 5mm;
  }
}

1444022713-B97805AA-FAA8-4B7B-830A-5645D395966F.png

印刷ダイアログを開くと、ほとんど見た目が変わらないのが分かります。

1444022842-7B9AC55F-37A5-4BFB-AAE2-513DAFCE4184.png

ライブリロード

browsersyncを使うのが一般的です。ただし、印刷時に邪魔なので画面通知をオフにしておくとベターです。gulpを使った場合の簡単な例を次に示します。

var gulp        = require('gulp')
var browserSync = require('browser-sync')
var reload      = browserSync.reload

// watch files for changes and reload
gulp.task('watch', function() {
  browserSync({
    notify: false
  })

  gulp.watch(['*.html', '*.css'], reload)
})

あとは、作業前に次のコマンドを叩いておけば、HTML/CSSファイルの保存時、デザイン中の帳票画面が自動的にリロードされます。便利。

$ gulp watch

フロントエンドで帳票生成

Riot、Polymer...

あるいは、AngularやReactでも構いません。UIライブラリがそのまま帳票でも使えます。インタラクティブにする必要もないので、Jadeやmastacheなどの静的なテンプレートエンジンで生成する手もあります。Riotの場合の例を簡単に示します。

<print-receipt>
  <section class="sheet">
    <h1>領収証</h1>
    <h2>{ opts.title }様</h2>
    <p>下記、正に領収いたしました</p>
    <h3>金額 &yen; { opts.amount }-</h3>
    <h4>但・{ opts.desc }</h4>
    <time>{ opts.date } 発行</time>
  </section>

  <style scoped>
    /* 略 */
  </style>
</print-receipt>

こんな感じ(↑)のカスタムタグを準備して、次のようにマウントすればOK。

var opts = {
  title: '山田太郎',
  amount: 3000,
  desc: '懇親会参加費',
  date: '2015-10-04'
}
riot.mount('print-receipt', opts)

エントリーポイントを分ける

SPAとして、単一のエントリーポイント内でHTMLを生成することもできますが、次の点から分けた方が無難です。

  • 帳票生成は件数が多いとメモリを食う (通常UIよりフリーズの可能性高し)
  • 印刷後にタブ(ウィンドウ)を閉じれば確実にメモリが解放される
  • 複数の帳票サイズがある場合@pageルールの指定がしにくい

メインのSPAと、帳票サイズごとにエントリーポイントを分けておけば十分です。例:

  • index.html メインのエントリーポイント
  • print-a4.html A4帳票のエントリーポイント
  • print-a5.html A5帳票のエントリーポイント

自動的に印刷ダイアログを表示

帳票を表示する際は、メインのプログラムからwindow.open()で別タブを開かせるとよいです。

var url = 'print-a4.html'
window.open(url)

ユーザにいちいち印刷メニューを選択させるというのは面倒です。HTML帳票を開いた時点で、印刷するに決まっているのですから、DOMがロードされたら自動的にダイアログ表示させましょう。print()関数を呼び出すだけです。

window.print()
window.close()

ここでは、続けてclose()も呼び出して、帳票用に開かれたタブを自動的に閉じるようにしました。print()は同期的(sync)に実行されるため、こう書くだけで「印刷後(あるいはキャンセル後)、画面を閉じる」の意になります。

先ほどのRiotの例に付け足すなら、次のようになるでしょうか。mountのタイミングだと、まだ描画が完了していないためsetTimeoutで少し余裕をみています。

<print-receipt>
  <section class="sheet">
    <!-- 略 -->
  </section>

  <script>
    this.on('mount', function() {
      setTimeout(function(){
        window.print()
        window.close()
      }, 100)
    })
  </script>

  <style scoped>
    /* 略 */
  </style>
</print-receipt>

トラブルシューティング

罫線が表示されない (一般)

border設定を1px未満にしていませんか? 残念ながら、ヘアライン(印刷可能な一番細い線)にする方法はありません。1px罫線で我慢しましょう。※ちなみに、transform: scale()を使った場合も、1px以下に判定されると印刷されません...。

罫線が表示されない (TABLEタグ)

1px以上であっても、Chromeはときどき<table>の罫線をサボります。そんなときは、下記のようにHTML側の属性指定で乗り切りましょう。(そこ、のけぞらないで!)

<table border="1">
  <!-- 略 -->
</table>

あとは、外したい部分だけCSSから罫線を解除していけばなんとかなります。きっと。

環境によって行間がずれる

フォントによって行間の取り方が異なります。業務システムであれば、共通にインストールされたフォントを利用しましょう。また、行の高さline-heightの指定をmmでしておくと、帳票の崩れを最小限に抑えられます。

あるいは、WebフォントをCSSで指定する手もあります。日本語が使えるものとしては、GoogleのNotoが便利です。詳しくは、Google Web Fonts Early Accessを参照のこと。

@import url(http://fonts.googleapis.com/earlyaccess/notosansjapanese.css);
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away