QRコード付きのカードを作成・印刷する

紙のカードを使ったワークショップによる実験を行うにあたって、終了後の集計を効率化するため、QRコードをカードに仕込んでおく、ということをやってみました。

カードの印刷イメージ生成にはLaTeXなどの組版ソフトを使うのが一般的かと思いますが、もう少しお手軽に作りたいところです。
そこで、「カード印刷用のHTMLをスクリプトで生成する」という技を紹介します。スクリプト内のHTMLタグをいじることで、自由にカードデザインをカスタマイズできます。

スクリプト一式は[Gist](https://gist.github.com/kiyota-yoji/2b01c2ab4150769e0ebd)に上げていますので、ご自由にご利用ください。

## ポイント

* PythonスクリプトでTSV形式で用意したデータを読み込み、HTMLファイルを生成
    * &lt;hr&gt;タグにCSSで "page-break-after: always;" を設定することで、印刷時に改ページさせる
* QRコードの生成は[Python qrcodeモジュール](https://pypi.python.org/pypi/qrcode)を利用
* HTMLファイルからPDFファイルへの変換には[wkhtmltopdf](http://wkhtmltopdf.org/)を利用
* カードはA6サイズで作成し、A4に印刷して裁断機などで四つ切り

## 動作環境

* Python 2.7系列
* [wkhtmltopdf](http://wkhtmltopdf.org/)
* [qrcode 5.1](https://pypi.python.org/pypi/qrcode)
* [Pillow 2.7.0](https://pypi.python.org/pypi/Pillow/2.7.0)
    * カード中に画像ファイルを埋め込みたい場合、画像ファイルサイズを取得するために利用
* Mac OS X環境で試していますが、たぶんWindows、UNIX環境でも動きます

## 環境のセットアップ

### wkhtmltopdf

Webサイトの[ダウンロードページ](http://wkhtmltopdf.org/downloads.html)からStable版を入手してインストールします。

### Pythonモジュール

pipで、qrcode, pillowをインストールしてください。

```bash
$ pip install qrcode
$ pip install pillow
```

## サンプルスプリクトを動かしてみる

[Gist](https://gist.github.com/kiyota-yoji/2b01c2ab4150769e0ebd)から、以下のファイルをダウンロードしてください。
* [make_html.py](https://gist.githubusercontent.com/kiyota-yoji/2b01c2ab4150769e0ebd/raw/2d54d3cb1d23a5d5b72f30bc61695add50e4242b/make_html.py): スクリプト本体
* [card_data.txt](https://gist.githubusercontent.com/kiyota-yoji/2b01c2ab4150769e0ebd/raw/e191a02f1e934a9d28691be57c13ff47c7f5d862/card_data.txt): サンプルデータ (Wikipedia日本語版の鉄道駅エントリと画像)
* [Makefile](https://gist.githubusercontent.com/kiyota-yoji/2b01c2ab4150769e0ebd/raw/dfbf9936c6c515cdbb81190e1834a588ec85552b/Makefile): GNU makeによるコマンド自動実行

### スクリプトの実行

HTMLファイルとQRコード画像を、サンプルデータから生成します。

```bash
$ python make_html.py --qrcode-dir ./qrcode card_data.txt card_printout.html
```

生成された card_printout.html をブラウザで開いて、カードデータが作成されていることを確認してみてください。

![QRコード付きカード印刷データ.png](https://qiita-image-store.s3.amazonaws.com/0/69546/b50d8f6d-08de-6b83-ab0c-6b9045f512e0.png)


wkhtmltopdfコマンドで、HTMLファイルから印刷に適したPDFファイルを生成します。

```bash
wkhtmltopdf -s A6 card_printout.html card_printout.pdf
```

wkhtmltopdf のオプションリストは[こちら](http://wkhtmltopdf.org/usage/wkhtmltopdf.txt)にあります。


## カードを印刷する

生成されたPDFファイルは、A4に2×2で印刷するとちょうどいいサイズになっています。

Acrobat Readerの印刷オプションで、「1枚あたりのページ数」を「4」に設定して印刷します。
![acroread_printout.png](https://qiita-image-store.s3.amazonaws.com/0/69546/37e4286a-ab43-9d33-0976-b3e51ffb4b81.png)

印刷されたカードを裁断機などで4ツ切りにすれば完成です。
![printed_cards.jpg](https://qiita-image-store.s3.amazonaws.com/0/69546/2528d054-1660-53f3-7723-f5da02fc0b07.jpeg)

## サンプルスクリプトの解説

スクリプト make_html.py をカスタマイズされる方の参考のために、ちょっとした説明を書いておきます。

### QRコードの生成と埋め込み

```py
        # QRコード生成 (page_urlをQRコード化)
        qrcode.make(page_url).save('./qrcode/%08d.png' % (num))
```
page_url (サンプルデータではWikipediaページのURL) をQRコード化して、カード番号(num)に対応したファイル名の画像ファイル(PNG形式)を書き出しています。

HTMLファイルの生成の際に、この画像ファイルを埋め込むためのHTMLタグを書き出しています。

```html
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>QRコード付きカード印刷データ</title>
<style type="text/css">

/* QRコードは右下に配置 */
div.qrcode {
  text-align: right;
}

</style>
</head>

<body>

...

<div class="qrcode"><img src="./qrcode/00000001.png" width="50" /></div>

...

</body>
</html>
```

### 画像ファイルのサイズ調整

```py

import urllib, cStringIO
import PIL.Image as Image

# 画像の大きさの制限値
IMAGE_WIDTH_LIMIT = 300
IMAGE_HEIGHT_LIMIT = 180

class CardHtmlMaker(object):

    def _get_image_size_tag(self, image_url):
        """
        Web上の画像サイズをHTTPで取得し、制限サイズを計算
        @param image_url 画像のURL
        @return imgタグのサイズ指定文字列
        """
        try:
            res = urllib.urlopen(image_url)
        except IOError, e:
            raise Exception(u'画像ファイルの取得中にエラーが発生しました: %s: %s' % (image_url, e))
        
        if res.code != 200:
            raise Exception(u'画像ファイルが取得できません: %s: status=%d' % (image_url, res.code))

        fp = cStringIO.StringIO(res.read())
        image = Image.open(fp)
        (width, height) = image.size

        if (float(width) / height) > (float(IMAGE_WIDTH_LIMIT) / IMAGE_HEIGHT_LIMIT):
            # 制限サイズより横長 -> 幅でサイズ指定
            return 'width="%d"' % IMAGE_WIDTH_LIMIT
        else:
            # 制限サイズより縦長 -> 高さでサイズ指定
            return 'height="%d"' % IMAGE_HEIGHT_LIMIT
```
サンプルデータでは、Web上の画像ファイルを指定してページ中に埋め込むということをやっています。この際、画像サイズによってカードからはみ出ないよう、画像サイズの縦横比に応じて&lt;img&gt;タグの「width=」または「height=」属性でサイズ指定しています。
下の例では、制限サイズの横縦比(300÷180=1.66..)よりも画像ファイルが縦長(800÷536=1.49...)であるため、heightタグでサイズ指定しています。

```html
<div class="image"><img src="http://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Tokyo_station_from_marunouchi_oazo.JPG/800px-Tokyo_station_from_marunouchi_oazo.JPG" height="180" /></div>
```

### カードの書き出し順のコントロール

```py
    def output(self, input_data, output_html):
        """
        TSVファイルで入力されたデータから、HTMLファイルを生成する
        @param input_data  入力ファイル(TSV)のファイルハンドル
        @param output_html 出力ファイル(HTML)のファイルハンドル
        """

        output_html.write(HTML_HEADER.encode('utf-8'))

        lines = input_data.readlines()
        input_data.close()

        num_cards = len(lines)
        num_pages = (num_cards - 1) / 4 + 1
        for page in range(num_pages):
            # 1枚目(左上)
            card_id = page
            output_html.write(self._make_card(card_id+1, lines[card_id]).encode('utf-8'))

            # 2枚目(左下)
            card_id = page + num_pages * 1
            if card_id > num_cards - 1:
                output_html.write('<hr>\n\n')
            else:
                output_html.write(self._make_card(card_id+1, lines[card_id]).encode('utf-8'))

            # 3枚目(右上)
            card_id = page + num_pages * 2
            if card_id > num_cards - 1:
                output_html.write('<hr>\n\n')
            else:
                output_html.write(self._make_card(card_id+1, lines[card_id]).encode('utf-8'))

            # 4枚目(右下)
            card_id = page + num_pages * 3
            if card_id > num_cards - 1:
                if page == num_pages - 1:
                    pass
                else:
                    output_html.write('<hr>\n\n')
            else:
                if page == num_pages - 1:
                    output_html.write(self._make_card(card_id+1, lines[card_id],
                                                      page_break=False).encode('utf-8'))
                else:
                    output_html.write(self._make_card(card_id+1, lines[card_id]).encode('utf-8'))

        output_html.write(HTML_FOOTER.encode('utf-8'))

        output_html.close()
```

サンプルデータのTSVファイルの1行目から順にHTMLタグを書き出してしまうと、4ツ切りで裁断したときに、手作業で並べ替えが必要となってしまいます。
そこで、裁断後に裁断された束を重ねるだけでデータファイルの順になるように、ページ数(page_num)分だけループを回して書き出しています。