紙のカードを使ったワークショップによる実験を行うにあたって、終了後の集計を効率化するため、QRコードをカードに仕込んでおく、ということをやってみました。
カードの印刷イメージ生成にはLaTeXなどの組版ソフトを使うのが一般的かと思いますが、もう少しお手軽に作りたいところです。
そこで、「カード印刷用のHTMLをスクリプトで生成する」という技を紹介します。スクリプト内のHTMLタグをいじることで、自由にカードデザインをカスタマイズできます。
スクリプト一式はGistに上げていますので、ご自由にご利用ください。
ポイント
- PythonスクリプトでTSV形式で用意したデータを読み込み、HTMLファイルを生成
- <hr>タグにCSSで "page-break-after: always;" を設定することで、印刷時に改ページさせる
- QRコードの生成はPython qrcodeモジュールを利用
- HTMLファイルからPDFファイルへの変換にはwkhtmltopdfを利用
- カードはA6サイズで作成し、A4に印刷して裁断機などで四つ切り
動作環境
- Python 2.7系列
- wkhtmltopdf
- qrcode 5.1
-
Pillow 2.7.0
- カード中に画像ファイルを埋め込みたい場合、画像ファイルサイズを取得するために利用
- Mac OS X環境で試していますが、たぶんWindows、UNIX環境でも動きます
環境のセットアップ
wkhtmltopdf
WebサイトのダウンロードページからStable版を入手してインストールします。
Pythonモジュール
pipで、qrcode, pillowをインストールしてください。
$ pip install qrcode
$ pip install pillow
サンプルスプリクトを動かしてみる
Gistから、以下のファイルをダウンロードしてください。
- make_html.py: スクリプト本体
- card_data.txt: サンプルデータ (Wikipedia日本語版の鉄道駅エントリと画像)
- Makefile: GNU makeによるコマンド自動実行
スクリプトの実行
HTMLファイルとQRコード画像を、サンプルデータから生成します。
$ python make_html.py --qrcode-dir ./qrcode card_data.txt card_printout.html
生成された card_printout.html をブラウザで開いて、カードデータが作成されていることを確認してみてください。
wkhtmltopdfコマンドで、HTMLファイルから印刷に適したPDFファイルを生成します。
wkhtmltopdf -s A6 card_printout.html card_printout.pdf
wkhtmltopdf のオプションリストはこちらにあります。
カードを印刷する
生成されたPDFファイルは、A4に2×2で印刷するとちょうどいいサイズになっています。
Acrobat Readerの印刷オプションで、「1枚あたりのページ数」を「4」に設定して印刷します。
サンプルスクリプトの解説
スクリプト make_html.py をカスタマイズされる方の参考のために、ちょっとした説明を書いておきます。
QRコードの生成と埋め込み
# QRコード生成 (page_urlをQRコード化)
qrcode.make(page_url).save('./qrcode/%08d.png' % (num))
page_url (サンプルデータではWikipediaページのURL) をQRコード化して、カード番号(num)に対応したファイル名の画像ファイル(PNG形式)を書き出しています。
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>
画像ファイルのサイズ調整
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上の画像ファイルを指定してページ中に埋め込むということをやっています。この際、画像サイズによってカードからはみ出ないよう、画像サイズの縦横比に応じて<img>タグの「width=」または「height=」属性でサイズ指定しています。
下の例では、制限サイズの横縦比(300÷180=1.66..)よりも画像ファイルが縦長(800÷536=1.49...)であるため、heightタグでサイズ指定しています。
<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>
カードの書き出し順のコントロール
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)分だけループを回して書き出しています。