【半日くらいでなんかつくる】ReportLabで年賀状をつくろう編

今回のきっかけ

あけましておめでとうございます。

人間から届いた年賀状がついに1枚きりとなってしまったわたしです。

そもそもこちらから1枚も出してないからね、そんなものよね……。

.

ひとりだけ送ってくれた友人がいたので、お返しを考えなければなあとぼんやり考えていて、

そういや新年初っ端からのお仕事で、

ReportLabというPython等でPDFファイルを作成・出力する仕組みをつくるためのツールを扱わなければなんだよなあ、

というのも思い出し、

これで年賀状を作れば練習にもなるし、お返しもできあがるしで一石二鳥……?

……お返しの作り方を一石二鳥の観点から選択してしまったことへの罪悪感をしっとりと感じながらも、取り掛かることにしました。

今回の制作物

実行するだけで年賀ハガキに印刷するためのpdfファイルを出力してくれるnenga2018.py。


環境構築

見よう見まねで整えた現環境はたぶん以下のようになってます、後述のトラブルにも繋がるところなのですが、anacondaがいまいちよくわからない……。

macOS High Sierra 10.13.6

anaconda 5.3.1 
Python(anacondaにin?) 3.7.1 
ReportLab(同上) 3.5.10

Virtual Studio Code 1.30.1

2019年の目標はこのPCをMojaveにアップデートすることです。

.

前にPythonの環境を構築した際、anacondaもVSCodeも入れてそのままだった(Pycharmの無料版延々と使ってた)ので、それを使えるようにしました。

【Mac】Visual Studio CodeでPythonでの開発環境を整える

上記等参考に進めました。

概ね順調でしたが、標準以外のライブラリであるReportLabをpip installしたにもかかわらず、

importしても反映がされない……といったところでしばらくハマりました。

完全に思考停止してpipを使っていたのですが、anaconda環境ではcondaというパッケージ管理ツールがあるため、

pipによってインストールしたものを取り扱おうとするとうまくいかないことがあるようです。

condaとpip:混ぜるな危険 - onoz000’s blog

uroshika's notes: VisualStudioCode で Python 実行時にモジュールが import できない場合の対処法

……いままで練習用にインストールしてたpip下のあれやこれやはどうして動いていたのだろう。

ひとまず今回はconda install -c anaconda reportlabにてReportLabを導入。


ReportLab

PythonからPDFを生成する際に用いられるライブラリ。

www.reportlab.com

公式ドキュメントは広大なpdfファイルになっております……。

ReportLab PDF Library

サブ的なライブラリ(pdfgen、pdfbase、platypus、lib)についてはこちら。

ReportLab API Reference


図形の描画

ReportLabでは、図形など描画するものの位置を、xy座標で指定するようになっています。

原点(0, 0)は左下です。

reportlab.pdfgen.canvasというモジュールでは、楕円と長方形を描画できます。

しかしながら、高さ・幅の指定に少し癖があり……。

長方形が起点の座標を指定→起点からの幅・高さを指定なのに対し、楕円はその楕円が入る枠を指定するというルールになっているようです。

……知能が良くなく5分くらい考えても上記のような説明しか書けなかったので、以下にちょっとした作例を挙げてみます。

from reportlab.pdfgen import canvas
from reportlab.lib.units import cm

pdfFile = canvas.Canvas('./python.pdf')
pdfFile.saveState()

pdfFile.setPageSize((10.0*cm, 10.0*cm))

# x:4cm、y:2cmの位置を起点として、幅2cm、高さ1.5cmの長方形(破線部)
pdfFile.setDash(array=[5, 5], phase=0)
pdfFile.rect(4*cm, 2*cm, 2*cm, 1.5*cm, stroke=1, fill=0)

# x1:4cm、y1:2cmの点とx2:6cm、y2:3.5cmの点を結ぶ線を対角線にもつ長方形にぴったり収まる楕円
# →長径2cm、短径1.5cmの楕円となる(実線部)
pdfFile.setDash(array=[], phase=0)
pdfFile.setFillColorRGB(float(121)/255, float(85)/255, float(72)/255)
pdfFile.ellipse(4*cm, 2*cm, 6*cm, 3.5*cm, stroke=1, fill=1)

pdfFile.restoreState()
pdfFile.save()

上記のコードではこんな感じのpdfファイルが出力されます。おわかりいただけただろうか……。

※赤緑青は追記した部分。

.

公式ドキュメントやVSCodeの機能(F12でクラスやメソッドの定義元確認できるのめっちゃ便利)を活用しつつ、

下記のような先人の作例を踏襲し、お絵描きを進めていきました。

PDF生成 (ReportLab) | Python-izm

PythonのPDFライブラリ「ReportLab」の使い方(直線、矩形、円、楕円、丸囲みの矩形の描画) - Symfoware

PythonのPDFライブラリ「ReportLab」の使い方(表描画、線・塗りつぶしの色、線の太さ、破線の指定) - Symfoware

PythonでPDFを生成したい そしてサイコロを作りたい

こんなコードになり、こんな画像ができました。顔上部と鼻です。

from reportlab.pdfgen import canvas
from reportlab.lib.units import cm

pdfFile = canvas.Canvas('./nenga2018.pdf')
pdfFile.saveState()

pdfFile.setAuthor('fujitami')
pdfFile.setTitle('nenga2018')
pdfFile.setSubject('2018')

# ハガキサイズ
pdfFile.setPageSize((10.0*cm, 14.8*cm))

# 顔
pdfFile.setFillColorRGB(float(141/255), float(110/255), float(99/255))
pdfFile.rect(2*cm, 3*cm, 6*cm, 6*cm, stroke=0, fill=1)

# 鼻
pdfFile.setFillColorRGB(float(121/255), float(85/255), float(72/255))
pdfFile.ellipse(4*cm, 4*cm, 6*cm, 5.5*cm, stroke=0, fill=1)

pdfFile.restoreState()
pdfFile.save()

ちなみに色はMaterial Design縛りで。適当に組み合わせても浮かないかなと……。

Material Design Colors, Material Colors, Color Palette | Material UI

PDF生成の基本的な機能を提供するライブラリ。

今回はCanvasモジュールを使用し、描画領域の生成からファイル名等の設定から図形の描画からファイルの保存といった何から何までを担当。

記述をやりやすくするための補助的な機能が入ったライブラリ。

上記では座標の指定をcm単位でできるようにするcmモジュールを使用。

あとのちに色指定をするためのcolorsモジュールというものを使用しますが、そちらもこちらのライブラリに。

pdfgenにもsetFillColorRGBといった色指定のメソッドがあるのですが、こちらの色指定は各RGB要素を0-1までの値に換算したfloat形式にしなければならず地味にめんどくさい……。


reportlab.graphics.shapes

ここまでは順調にお絵描きできたのですが、

pdfgen.canvasのモジュールでは描画できる図形に三角形が用意されていないようでありました。

三角形を描かなければ楽ではあるものの、顎や耳や牙のないイノシシはイノシシと呼べるのだろうか。

どうにかならないものかと手段を探していたところ、

グラフィックの描画に特化したreportlab.graphics.shapesというライブラリを発見。

pdfgenをあっさり捨て、以降はこちらで。

ReportLab Graphics Guide

三角形は、こちらのPolygonというモジュールで描画できるようでした。

reportlab.graphics.shapes.Polygon Example

from reportlab.lib.colors import * 
from reportlab.lib.units import cm
from reportlab.graphics.shapes import *
from reportlab.graphics import renderPDF

# ハガキサイズの描画領域を作成
d = Drawing(10.0*cm, 14.8*cm)

boar = Group()

# 顔
# 起点の座標xy、終点の座標xy
faceUpper = Rect(2.0*cm, 3.0*cm, 6.0*cm, 5.0*cm)
faceUpper.fillColor = HexColor('#8D6E63')
faceUpper.strokeColor = HexColor('#8D6E63')
d.add(faceUpper)

faceLower = Polygon([5.0*cm,2.0*cm, 2.07*cm,2.98*cm, 7.93*cm,2.98*cm])
faceLower.fillColor = HexColor('#8D6E63')
faceLower.strokeColor = HexColor('#8D6E63')
d.add(faceLower)

# 目
eyeLeft = Ellipse(3.25*cm, 5.5*cm, 0.5*cm, 0.25*cm)
eyeLeft.fillColor = HexColor('#7CB342')
eyeLeft.strokeColor = None
d.add(eyeLeft)

eyeRight = Ellipse(6.75*cm, 5.5*cm, 0.5*cm, 0.25*cm)
eyeRight.fillColor = HexColor('#7CB342')
eyeRight.strokeColor = None
d.add(eyeRight)

# 鼻
# 中心の座標xy、半径xy
nose = Ellipse(5.0*cm, 4.0*cm, 1.0*cm, 0.75*cm)
nose.fillColor = HexColor('#4E342E')
nose.strokeColor = None
d.add(nose)

# 鼻腔
holeLeft = Ellipse(4.6*cm, 4.0*cm, 0.2*cm, 0.3*cm)
holeLeft.fillColor = HexColor('#6D4C41')
holeLeft.strokeColor = None
d.add(holeLeft)

holeRight = Ellipse(5.4*cm, 4.0*cm, 0.2*cm, 0.3*cm)
holeRight.fillColor = HexColor('#6D4C41')
holeRight.strokeColor = None
d.add(holeRight)

# 牙
fangLeft = Polygon([3.1*cm,4.7*cm, 3.9*cm,3.5*cm, 3.0*cm,3.3*cm])
fangLeft.fillColor = HexColor('#B0BEC5')
fangLeft.strokeColor = None
d.add(fangLeft)

renderPDF.drawToFile(d, './nenga2018.pdf', 'nenga2018')

左の牙まで描いたところで、左右対称のパーツを作るのがめんどくさくなる(上記のように位置を計算しないといけないので……)。

ヒントを求めるうち、shapesライブラリの入っていたreportlab.graphicsのライブラリ中にwidgets.signsandsymbolsというライブラリを見つけました。

こんな図形がかけるらしい。

お……

君良い顔してるね……

SmileyFaceくんを召喚する方法はとっても簡単。

from reportlab.graphics.widgets.signsandsymbols import SmileyFace

smileyface = SmileyFace()
smileyface.x = 3*cm
smileyface.y = 10*cm
d.add(smileyface)

上記のSmileyFaceを描画するクラスの定義元を見に行ったところ、

左右対称パーツである目は以下のように描画していました。

class SmileyFace(_Symbol):

    def __init__(self):
        _Symbol.__init__(self)
        self.x = 0
        self.y = 0
        self.size = 100
        self.fillColor = colors.yellow
        self.strokeColor = colors.black

    def draw(self):
        # general widget bits
        s = float(self.size)  # abbreviate as we will use this a lot
        g = shapes.Group()

        # 中略

        for i in (1,2):
            g.add(shapes.Ellipse(self.x+(s/3)*i,self.y+(s/3)*2, s/30, s/10,
                    fillColor=self.strokeColor, strokeColor = self.strokeColor,
                    strokeWidth=max(s/38.,self.strokeWidth)))

        # 中略

        return g

for文になっているところで描画がされています。

forの対象としてタプルを作り、xの座標を調整(図形の横幅を3分割し、1/3のところに左目、2/3のところに右目を配置)している、楽!

……しかし上記のままイノシシの顔に転用したところ、

目は可愛くない牙はもう少し離したいと、物足りなさをことごとく感じることとなったため、 少しアレンジしました。

# 目
for i in (1, -1):
    boar.add(Ellipse((d.width/2)-((1.75*i)*cm), 5.5*cm, 0.5*cm, 0.25*cm,
            fillColor=HexColor('#00E5FF'), strokeColor = None))

イノシシのお顔の横幅の中心点から、指定の長さを引いたり足したりしたところに描画しています。


文字の描画

さすがに絵だけじゃ寂しいよね!ということで、

お決まりのフレーズ「Happy New Year」と、何かしら短いメッセージとを描画できるようにしました。

外部フォントの導入は、pdfmetricsライブラリが請け負うとのこと。

これに関しては、以下の記事の手順が詳しかったのでほぼこのままです。

TrueTypeフォントファイルは利用できますが、OpenTypeフォントは使うことができないのでご注意。

Pythonで日本語をPDFに出力する(ReportLabを利用) | ガンマソフト株式会社

今回使用したフォント。

Happy New Year→Yukarimobile

Yukarimobile Font Free by Vic Fieger » Font Squirrel

ひとことコメント→Oradano明朝

Oradano Mincho : public domain Retro style Japanese font

# 文字
Yukari_TTF = '/Users/fujitami/Library/fonts/yukarimobil.ttf'
Oradano_TTF = '/Users/fujitami/Library/fonts/OradanoGSRR.ttf'
pdfmetrics.registerFont(TTFont('Yukari', Yukari_TTF))
pdfmetrics.registerFont(TTFont('Oradano', Oradano_TTF))

# Happy New Year!!!!!
for i, s in enumerate(('Happy', 'New', 'Year')):
    d.add(String((0.0+(i+1)*0.8)*cm, ((15.2-((i+1)*2.5))*cm), s,
    fontName='Yukari', fontSize=30*(i+1), fillColor=HexColor('#263238')))

# ひとこと
message = input('20字弱くらいでひとことどうぞ\n')
d.add(String(d.width/2, 1.0*cm, message, 
    fontName='Oradano', fontSize=15, textAnchor='middle'))

ひとことはファイルの実行時に自由記述できるようにして、

いろんなバリエーションの年賀状を作れるようにしました。今回1枚しか出さないけど


完成

from reportlab.lib.colors import HexColor
from reportlab.lib.units import cm
from reportlab.graphics.shapes import Drawing, Ellipse, Group, Polygon, Rect, String
from reportlab.graphics import renderPDF
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

# ハガキサイズの描画領域を作成
d = Drawing(10.0*cm, 14.8*cm)
d.add(Rect(0, 0, 10.0*cm, 14.8*cm,
        fillColor=HexColor('#FFFF8D'),
        strokeColor=HexColor('#00E5FF'), strokeWidth=10))

boar = Group()

# 顔
# 起点の座標xy、終点の座標xy
boar.add(Rect(2.0*cm, 3.0*cm, 6.0*cm, 5.0*cm,
            fillColor=HexColor('#8D6E63'), strokeColor=HexColor('#8D6E63')))

boar.add(Polygon([5.0*cm,2.0*cm, 2.07*cm,2.98*cm, 7.93*cm,2.98*cm],
            fillColor=HexColor('#8D6E63'), strokeColor=HexColor('#8D6E63')))

# 耳
for i in (1, -1):
    boar.add(Polygon([((d.width/2)-((2.5*i)*cm)),9.5*cm,
            ((d.width/2)-((1.8*i)*cm)),7.8*cm,
            ((d.width/2)-((2.7*i)*cm)),7.8*cm],
            fillColor=HexColor('#8D6E63'), strokeColor = None))

# 目
# 各頂点の座標
for i in (1, -1):
    boar.add(Ellipse((d.width/2)-((1.75*i)*cm), 5.5*cm, 0.5*cm, 0.25*cm,
            fillColor=HexColor('#00E5FF'), strokeColor = None))

# 鼻
# 中心の座標xy、半径xy
boar.add(Ellipse(5.0*cm, 4.0*cm, 1.0*cm, 0.75*cm,
            fillColor = HexColor('#4E342E'), strokeColor = None))

# 鼻腔
for i in (1, -1):
    boar.add(Ellipse((d.width/2)-((0.4*i)*cm), 4.0*cm, 0.2*cm, 0.3*cm,
            fillColor=HexColor('#6D4C41'), strokeColor = None))

# 牙
for i in (1, -1):
    boar.add(Polygon([((d.width/2)-((1.9*i)*cm)),4.7*cm,
            ((d.width/2)-((1.1*i)*cm)),3.5*cm,
            ((d.width/2)-((2.0*i)*cm)),3.3*cm],
            fillColor=HexColor('#B0BEC5'), strokeColor = None))

# 文字
Yukari_TTF = '/Users/fujitami/Library/fonts/yukarimobil.ttf'
Oradano_TTF = '/Users/fujitami/Library/fonts/OradanoGSRR.ttf'
pdfmetrics.registerFont(TTFont('Yukari', Yukari_TTF))
pdfmetrics.registerFont(TTFont('Oradano', Oradano_TTF))

# Happy New Year!!!!!
for i, s in enumerate(('Happy', 'New', 'Year')):
    d.add(String((0.0+(i+1)*0.8)*cm, ((15.2-((i+1)*2.5))*cm), s,
    fontName='Yukari', fontSize=30*(i+1), fillColor=HexColor('#263238')))

# ひとこと
message = input('20字弱くらいでひとことどうぞ\n')
d.add(String(d.width/2, 1.0*cm, message, 
    fontName='Oradano', fontSize=15, textAnchor='middle'))

# 小さいイノシシ描画
boarSmall = boar.copy()
d.insert(6, boarSmall)
boarSmall.shift(5.6*cm, 1.6*cm)
boarSmall.scale(0.4, 0.4)

# 大きいイノシシ描画及び変形
d.insert(1, boar)
boar.shift(-2.0*cm, -2.0*cm)
boar.skew(13, 40)

# pdf書き出し
renderPDF.drawToFile(d, './nenga2018.pdf', 'nenga2018')

実行!

はい。


Groupクラスのメソッドが結構色々あったので遊んでみていました。

画像を斜めにするskewが引数で与えたパラメータをどう解釈して変形してるのかいまいち掴めてないのですが、

とりあえず値でかめに設定するとエグめの変形がなされます。

また、insertメソッドを使うと、引数に指定したインデックス番号の位置に要素が挿入されるため、

文字の下に絵を入れるといった、レイヤー操作みたいなこともやった……つもりです。


お正月休みも今日で終わり、もはや七草粥が目前に迫り来るほどの日数が経ってしまったので、投稿しなくても良いかなあ、と思っていたのですが、

下手すると明日から早速このReportLabを使うかもしれないのでは、と気が付いてしまったため、

新年初仕事で初泣きかまさないよう、明日の自分が参照できる程度にまでは記事を仕上げました、だいぶ最後駆け足ですが……。


ことしもがんばります。がんばりましょう。