ホーム

PDFをpythonで生成してみる

便利なモジュールがあるので、PDFを作ることがとても楽にできるんだということを示してみます。ラベルシート台紙なんかに印刷するものを想定。説明は、pythonがちょっとは分かっている人に向けて書いています。でも、分からなくても、簡単なんだろうなー、という印象は持てると思います。

Linuxなんかでもできることは確認済みだけど、ここではWindows上でやります。はじめるにあたって、pytnonの2系(2.6とか2.7)をマシン上にインストールして、PILっていう画像操作ライブラリと、reportlabっていうPDF生成ライブラリをそれぞれインストールしておきましょう。PILはすぐに必要にはなりませんが、画像をPDFに貼るときに必要になってきます。

上のそれぞれのサイトで、Windows用の、使っているpythonと同じバージョン用のインストーラを探してダウンロード、実行するだけで、基本的に準備は完了です。reportlabが提供するライブラリは商用版とオープンソース版がありますが、オープンソース版で足ります。

最初は、こんなスクリプトを作ります。エンコーディングはUTF-8で。(日本語はコメントにしか使ってないけど)

# coding:utf-8

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm

xmargin = 8.4*mm
ymargin = 8.8*mm
swidth = 48.3*mm
sheight = 25.4*mm

def draw_label(c, x, y, data):
  c.setLineWidth(0.5)
  c.rect(x, y, 48.3*mm, 25.4*mm, stroke=1, fill=0)
  c.drawString(x, y, str(data))

c = canvas.Canvas("test.pdf",pagesize=A4)
for i in xrange(44):
  #オフセット位置
  x = xmargin + swidth * (i%4)
  y = ymargin + sheight * (10-(i//4))
  #ラベル印刷
  draw_label(c, x, y, i)

# 一ページ分を確定して、PDFデータをファイルに格納
c.showPage()
c.save()

正常に実行できれば、test.pdf っていうPDFファイルができあがって、下みたいなページ内容のものになるでしょう。ラベルシートみたいなのを手差しするとその上に印字される想定です。このタイプの44枚切は、実際に市販品でたくさん見つかります。

スクリプトの最初のimport部は、reportlabモジュールから必要なものをロードしてきているだけです。

xmarginは用紙の左余白、ymarginは下余白、swidthはラベルシート一枚あたりの横幅、sheightは縦幅、のつもり。あとで調整が生じたときに直しやすいように、定義としてまとめました。

PDFの一般的なルールとして(そしてreportlabでも)、座標系は、左下が(0,0)です。Xが正なら右方向に、Yが正なら上方向です。

xrange(44)で、シートを44枚分、順に描画するためのループを形成します。オフセット位置の計算では、N番目のシートごとの「左下部分」がどの座標になるかを計算しています。

ラベル印刷のサブルーチンは、draw_labelとして別に準備しました。受け取る引数は、c(PDF生成オブジェクト)、x,y(オフセット)、data(シートごとに異なるデータ)くらいにしました。今回のdraw_label内では、線の太さを決めて(setLineWidth)、四角形を描いて(rect)、文字列を描画する(drawString)くらいです。それぞれの描画メソッドの引数の説明は、あんまり詳しくしません。ここでは「割とシンプルだね」という印象を持ってもらいたいだけです。でも、drawStringの描画位置をオフセットそのままにしたとき、文字列が枠の左下にこのようにくっついた形で出てくることは注意しておくとよいでしょう。最初の文字の左下(というか、ベースライン?)が指定の座標に合わせられます。表示している内容は、ループ中の数値そのものです。

枠線は、実際のラベルシートに印字するときは出す必要はないでしょうね。切れ目に重なるんだし。あくまで印字イメージを確かめている間だけ出すことになるでしょう。

さて、このスクリプトをちょっといじって、下のようにしました。

# coding:utf-8

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
import random

xmargin = 8.4*mm
ymargin = 8.8*mm
swidth = 48.3*mm
sheight = 25.4*mm

def get_random_code():
  s = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  return "".join([random.choice(s) for i in xrange(8)])

def draw_label(c, x, y, data):
  c.setLineWidth(0.5)
  c.rect(x, y, 48.3*mm, 25.4*mm, stroke=1, fill=0)
  c.setFont("Courier-Bold", 16)
  c.drawString(x + 14.6*mm, y + 13.4*mm, data)

c = canvas.Canvas("test.pdf",pagesize=A4)
for i in xrange(44):
  #オフセット位置
  x = xmargin + swidth * (i%4)
  y = ymargin + sheight * (10-(i//4))
  #ラベル固有のデータ
  code = get_random_code()
  #ラベル印刷
  draw_label(c, x, y, code)

# 一ページ分を確定して、PDFデータをファイルに格納
c.showPage()
c.save()

get_random_codeっていう関数をひとつ用意して、これを呼ぶと、デタラメな8桁の文字でできた「何かのコード」が得られるようにしました。(リスト内包っていう、『python自習テキスト』で触れたことのない、怪しいワザを使っています。スクリプトを短くするため。)デタラメなので別に何の役にもたたないんですが、ちょっとラベルっぽい雰囲気が出ます。下みたいに。

setFontっていうメソッドを使って、フォントや、直後に印字する文字の大きさをポイント数で指定しています。drawStringでは、さっきまでみたいな左下ピッタリでは格好悪いので、ちょっと数を足して表示位置を調整しています。(14.6*mm とかいう表記は、ここでは14.6ミリを表しています。mmってのが定数みたいになっていて、擬似的にこんな書き方でミリ数を表現するようになっています。)変えた部分は、せいぜいこの程度。乱数を使うので、スクリプト冒頭でインポート宣言するやつがちょっと増えてたりするのは、まあ別にいいよね。

実際に役に立つ情報を印字したければ、色々工夫することになるでしょうね。CSVなんかから順番に持ってくるなり、データベースから持ってくるなり。ここらへんはpythonのプログラミングスキル次第です。

で、次の発展が、バーコード印刷。reportlabはバーコード印字がとても楽にできるようになっていて、その手のニーズを持っている人にはとてもよい感じです。今のスクリプトをさらにいじります。

# coding:utf-8

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.graphics.barcode import code39
import random

xmargin = 8.4*mm
ymargin = 8.8*mm
swidth = 48.3*mm
sheight = 25.4*mm

def get_random_code():
  s = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  return "".join([random.choice(s) for i in xrange(8)])

def draw_label(c, x, y, data):
  c.setLineWidth(0.5)
  c.rect(x, y, 48.3*mm, 25.4*mm, stroke=1, fill=0)
  c.setFont("Courier-Bold", 16)
  c.drawString(x + 14.6*mm, y + 13.4*mm, data)
  barcode=code39.Standard39("*"+data+"*",barWidth=0.26*mm,barHeight=8.0*mm,checksum=False)
  barcode.drawOn(c, x, y + 3.4*mm)

c = canvas.Canvas("test.pdf",pagesize=A4)
for i in xrange(44):
  #オフセット位置
  x = xmargin + swidth * (i%4)
  y = ymargin + sheight * (10-(i//4))
  #ラベル固有のデータ
  code = get_random_code()
  #ラベル印刷
  draw_label(c, x, y, code)

# 一ページ分を確定して、PDFデータをファイルに格納
c.showPage()
c.save()

これの実行結果としてできあがるpdfは、下みたいな。

ここでは、英数字が扱えるCODE39っていう規格のバーコード(チェックデジットなし)を印字しました。reportlabはもうちょっと色々な規格にも対応しています。詳しく述べませんけど。

インポートするモジュールがちょっと増えたのは飛ばして、本質的に増えたのはbarcode云々っていう二行分だけ。あまり説明はしませんが、簡単そうでしょ? あいかわらずスクリプトはかなり短いまんまだし。

最後に、あらかじめ準備した画像ファイルを各ラベルに貼り込む例も示します。

こんな画像(logo.png)があったとして…

スクリプトを下のように改造すればいいのです。どこら辺が変わったか、見比べて探すとタメになるよ。たぶん。

# coding:utf-8

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.graphics.barcode import code39
from reportlab.lib.utils import ImageReader
import random

xmargin = 8.4*mm
ymargin = 8.8*mm
swidth = 48.3*mm
sheight = 25.4*mm

img1 = ImageReader('logo.png')

def get_random_code():
  s = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  return "".join([random.choice(s) for i in xrange(8)])

def draw_label(c, x, y, data):
  c.drawImage(img1, x + 2.6*mm, y + 13.4*mm, 10.0*mm, 10.0*mm)
  c.setLineWidth(0.5)
  c.rect(x, y, 48.3*mm, 25.4*mm, stroke=1, fill=0)
  c.setFont("Courier-Bold", 16)
  c.drawString(x + 14.6*mm, y + 13.4*mm, data)
  barcode=code39.Standard39("*"+data+"*",barWidth=0.26*mm,barHeight=8.0*mm,checksum=False)
  barcode.drawOn(c, x, y + 3.4*mm)

c = canvas.Canvas("test.pdf",pagesize=A4)
for i in xrange(44):
  #オフセット位置
  x = xmargin + swidth * (i%4)
  y = ymargin + sheight * (10-(i//4))
  #ラベル固有のデータ
  code = get_random_code()
  #ラベル印刷
  draw_label(c, x, y, code)

# 一ページ分を確定して、PDFデータをファイルに格納
c.showPage()
c.save()

あらかじめインポートしておいたImageReaderっていう関数(多分コンストラクタだけど)を呼び出してimg1っていう変数にあらかじめ入れておき、それをdrawImageっていうメソッドで、貼る位置とサイズを指定した、それだけです。イメージの描画をdraw_label関数の最初のほうに持ってきているのは、今まで描いた図形に画像の余白が上書きされたらイヤだな、と思ったので、下に重ねるためにそうしたまでです。

できあがりイメージは、下の通り。実際のPDFを見たかったら、ここから。

枠線、文字、バーコード、画像が好きに配置できるんだから、このノリで進めていけば、きっとなんでもラベルに載せて、そこらへんに貼りまくれますね。たのしいね。おしまい。

【日本語を文字列として書き入れるとき】

さっきまでのサンプルの文字列部分にそのまま日本語を入れてみても、まともな表示になりません。ラベルごとに変化しない内容なら、画像として貼り込んでしまえば問題ないんですけど、可変部分はそうはいかないですね。

日本語フォントは、PDF用の標準としては HeiseiKakuGo-W5 とか HeiseiMi-W3 として定義されていて、これをフォント指定で使えばいいらしいです。でもその前に、これを使うための手続きが少しあるので、下のような部分を書き足します。

# 追加インポートする部分
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont

# フォントを実際に使う前に、登録しておくための処理。明朝体とゴシック体
pdfmetrics.registerFont(UnicodeCIDFont("HeiseiKakuGo-W5"))
pdfmetrics.registerFont(UnicodeCIDFont("HeiseiMin-W3"))

...

  ...
  #c.setFont("Courier-Bold", 16)
  # さっき登録したフォントが使える
  c.setFont("HeiseiMin-W3", 16)
  #文字列は、ユニコード表記で指定する
  c.drawString(x + 14.6*mm, y + 13.4*mm, u"こんにちは日本語")
  ...

この処理は、フォントの埋め込みまでをするものではないですから、このPDFを開くマシンによっては文字が表示できない、という可能性が残ります。具体的にどのフォントが使われるかは表示する環境に依存しますから、表示イメージを厳密に決めたいときも問題かも。用途によっては充分だろうと思いますけどね。reportlabにはフォント埋め込みのための機能もあるらしいですが、筆者が今のところあまり興味ないです。

【ウェブアプリからPDFを出力するとき】

Webサーバー上にpythonとPILとreportlabを入れておけば、CGIなどのWebアプリケーションとして、ユーザーに動的に生成したPDFをダウンロードさせることも簡単です。

c.save() すると、ファイル上にPDFデータを格納するんでした。この代わりに、c.getpdfdata() とすると、格納する予定だった内容がファイルを経由せずに取得できますから、こいつをWebクライアントにMIME情報付きで返せばおしまいです。

#つまり、下の部分の変わりに…
#c.save()
#下のようにすればいいってわけ。
rawpdf = c.getpdfdata()
print "Content-Type: application/pdf"
print ""
print rawpdf

2012.11.15

クリエイティブ・コモンズ・ライセンス
この 作品 は クリエイティブ・コモンズ 表示 3.0 非移植 ライセンスの下に提供されています。