画像にカレンダー出力
この前使った calendar モジュールとさらに前(ブログには未記述)に使った PIL をあわせて使ったら、絵にカレンダーが描けそうだったのでやってみた。
壁紙などの画像に直接カレンダーを描くのは考えが古い気もしたけれど、パソコン覚えたてのころに世話になっていた、壁紙にカレンダーを書き込むアプリ(名前忘れた)を再現して懐かしむということで。
以下の手順を基に作成したスクリプト
(2009-11-11更新)ImageCalendar のインスタンスを作成して、これの draw_month メソッドを呼ぶと、画像にカレンダーを描くことができる。使用例は test 関数にある。
(2009-11-12更新)カレンダーの背景色、背景透過度を指定できるようにした。
コマンドラインからも何とか使用できるようにした。ただし、望む出力に近づけていこうとすると、結局たくさんあるオプションをほぼすべて指定することとなる。正直、使いづらい。
# coding: utf-8 import calendar from PIL import Image, ImageDraw, ImageFont class ImageCalendar(calendar.Calendar): u"""PIL の画像にカレンダーを描画する calendar.Calendar のサブクラス""" month = ( u'January', u'February', u'March', u'April', u'May', u'June', u'July', u'August', u'September', u'October', u'November', u'December') weekday = (u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa', u'Su') def _create_month_header_image(self, theyear, themonth, font): s = u'%s %d' % (self.month[themonth - 1], theyear) im_size = font.getsize(s) im = Image.new('1', im_size) draw = ImageDraw.Draw(im) draw.text((0, 0), s, font=font, fill=1) return im def _get_box_size(self, font): _width = [] _height = [] text = [unicode(i) for i in range(1, 32)] text.extend(self.weekday) for w, h in (font.getsize(t) for t in text): _width.append(w) _height.append(h) width = max(_width) height = max(_height) return width, height def _get_month_list(self, theyear, themonth): weekdays = [ (self.weekday[weekday], weekday) for weekday in self.iterweekdays()] dates = [ [date for date in week] for week in self.monthdays2calendar(theyear, themonth)] month = [] month.append(weekdays) month.extend(dates) return month def _create_month_masks(self, theyear, themonth, font): box_size = self._get_box_size(font) month = self._get_month_list(theyear, themonth) month_masks = [] for week in month: week_masks = [] for date in week: day_mask = Image.new('1', box_size, color=0) if date[0]: date_text = unicode(date[0]) size = font.getsize(date_text) draw = ImageDraw.Draw(day_mask) draw.text( ( (box_size[0] - size[0]) // 2, (box_size[1] - size[1]) // 2), date_text, font=font, fill=1) week_masks.append((date, day_mask)) month_masks.append(week_masks) return month_masks def create_month_image( self, theyear, themonth, font, padding=(0, 0), color=(64, 64, 64), sat_color=None, sun_color=None, alpha=255, bgcolor=(0, 254, 0), bgalpha=0, holiday=()): u"""一月分のカレンダー画像を作成する 戻り値は theyear 年 themonth 月のカレンダーを描いた、 RGB 画像と alpah バンドである。 引数は以下の通り。 theyear と themonth は年と月。 font は フォントインスタンス。 padding は日付間のパディング。 color は通常の日付および年月を描く際に使用する色。 sat_color, sun_color は土曜、日祝日を描く際に使用する色。 alpah は透明度。 bgcolor, bgalpha はカレンダー部分の背景の色および透明度。 holiday は休日として扱う日付のリスト。 カレンダーをある画像 im の (0, 0) 位置に貼り付けるには 次のようにする。 im.paste(cal_rgb, (0, 0), mask=cal_alpha) なお、 RGB と alpha に分かれているのが不都合であれば 次のように処理することで RGBA 画像とすることができる。 cal_rgba = cal_rgb.convert('RGB') cal_rgba.putalpha(cal_alpha) """ if sat_color is None: sat_color = color if sun_color is None: sun_color = sat_color # 曜日、日付のマスクイメージのリスト month_masks = self._create_month_masks(theyear, themonth, font) # 行数 line_count = len(month_masks) # 曜日、日付部分のサイズ box_width, box_height = month_masks[0][0][1].size # 曜日、日付間のパディング padding_x, padding_y = padding # 月名のマスクイメージ mask_header = self._create_month_header_image( theyear, themonth, font) # カレンダーのサイズ im_width = max( box_width * 7 + padding_x * 6, mask_header.size[0]) im_height = ( box_height * line_count + padding_y * (line_count - 1) + padding_y + mask_header.size[1]) im_size = (im_width, im_height) # カレンダー画像およびカレンダーマスクの作成 cal = Image.new('RGB', im_size, bgcolor) cal_mask = Image.new('L', im_size, bgalpha) # 月名の描画 position = (0, 0) cal.paste( color, ( position[0] + (im_width - mask_header.size[0]) // 2, position[1]), mask_header) cal_mask.paste( alpha, ( position[0] + (im_width - mask_header.size[0]) // 2, position[1]), mask_header) # 曜日、日付の描画 month_position = ( position[0], position[1] + mask_header.size[1], + padding_y) for y, week_masks in enumerate(month_masks): for x, (date, day_mask) in enumerate(week_masks): day_color = color if date[1] == 5: day_color = sat_color elif date[1] == 6: day_color = sun_color if date[0] in holiday: day_color = sun_color day_position = ( ( month_position[0] + (box_width + padding_x) * x), ( month_position[1] + (box_height + padding_y) * y)) cal.paste( day_color, day_position, day_mask) cal_mask.paste( alpha, day_position, day_mask) return cal, cal_mask def create_month_image_rgba( self, theyear, themonth, font, padding=(0, 0), color=(64, 64, 64), sat_color=None, sun_color=None, alpha=255, bgcolor=(0, 254, 0), bgalpha=0, holiday=()): u"""一月分のカレンダー画像を作成する 戻り値は theyear 年 themonth 月のカレンダーを描いた、 RGBA 画像である。 引数は create_month_image メソッドと同じである。 カレンダーをある画像 im の (0, 0) 位置に貼り付けるには 次のようにする。 im.paste(cal_rgba, (0, 0), mask=cal_rgba) """ cal_im, cal_mask = self.create_month_image( theyear, themonth, font, padding=padding, color=color, sat_color=sat_color, sun_color=sun_color, alpha=alpha, bgcolor=bgcolor, bgalpha=bgalpha, holiday=holiday) cal_rgba = cal_rgb.convert('RGB') cal_rgba.putalpha(cal_alpha) return cal_rgba def draw_month( self, im, theyear, themonth, font, position=(0, 0),padding=(0, 0), color=(64, 64, 64), sat_color=None, sun_color=None, alpha=255, bgcolor=(0, 254, 0), bgalpha=0, holiday=()): u"""im に一月分のカレンダーを描きこむ 引数は以下の通り。 im は PIL の イメージインスタンス。 theyear と themonth は年と月。 font は フォントインスタンス。 position はカレンダーを描き込む位置。 padding は日付間のパディング。 color は通常の日付および年月を描く際に使用する色。 sat_color, sun_color は土曜、日祝日を描く際に使用する色。 alpah は透明度。 bgcolor, bgalpha はカレンダー部分の背景の色および透明度。 holiday は休日として扱う日付のリスト。 """ cal_im, cal_mask = self.create_month_image( theyear, themonth, font, padding=padding, color=color, sat_color=sat_color, sun_color=sun_color, alpha=alpha, bgcolor=bgcolor, bgalpha=bgalpha, holiday=holiday) im.paste(cal_im, position, mask=cal_mask) def test(): year = 2009 month = 11 holiday = (3, 23) position = (40, 30) padding = (8, 4) color = (32, 32, 32) sat_color = (0, 0, 192) sun_color = (160, 0, 0) alpha = 224 bgcolor = (192, 192, 192) bgalpha = 30 font_name = u'meiryo.ttc' font_size = 24 im = Image.new('RGB', (640, 480), color=(255, 255, 255)) font = ImageFont.truetype(font_name, font_size) cal = ImageCalendar(6) cal.draw_month( im, year, month, font, position=position, padding=padding, color=color, sat_color=sat_color, sun_color=sun_color, alpha=alpha, bgcolor=bgcolor, bgalpha=bgalpha, holiday=holiday) im.show() def main(): import os import datetime import optparse usage = 'usage: %prog src dst' version = '%prog 1.0' parser = optparse.OptionParser(usage=usage, version=version) parser.add_option('-f', '--force', action='store_true', dest='force', help=u'出力ファイルが存在しても上書きします') parser.add_option('--preview', action='store_true', dest='preview', help=u'生成結果をプレビュー表示します') parser.add_option('--year', action='store', type='int', dest='year', help=u'年を指定します') parser.add_option('--month', action='store', type='int', dest='month', help=u'月を指定します') parser.add_option('--font-name', action='store', type='string', dest='font_name', help=u'フォント名 (default: %default)') parser.add_option('--font-size', action='store', type='int', dest='font_size', help=u'フォントサイズ (default: %default)') parser.add_option('--position', action='store', nargs=2, type='int', dest='position', help=u'描き込む位置 (default: %default)') parser.add_option('--padding', action='store', nargs=2, type='int', dest='padding', help=u'日にち間のパディング (default: %default)') parser.add_option('--color', action='store', nargs=3, type='int', dest='color', help=u'通常色 (default: %default)') parser.add_option('--sat-color', action='store', nargs=3, type='int', dest='sat_color', help=u'土曜色 (default: 通常色と同じ)') parser.add_option('--sun-color', action='store', nargs=3, type='int', dest='sun_color', help=u'日曜色 (default: 土曜色と同じ)') parser.add_option('--alpha', action='store', type='int', dest='alpha', help=u'透過度 (default: %default)') parser.add_option('--bgcolor', action='store', nargs=3, type='int', dest='bgcolor', help=u'背景色 (default: %default)') parser.add_option('--bgalpha', action='store', type='int', dest='bgalpha', help=u'背景透過度 (default: %default)') parser.add_option('--ho', '--holiday', action='append', type='int', dest='holiday', help=u'休日扱いする日を指定します(複数回指定可)') parser.set_defaults( force=False, preview=False, year=None, month=None, font_name='meiryo.ttc', font_size=24, position=(0, 0), padding=(0, 0), color=(32, 32, 32), sat_color=None, sun_color=None, alpha=255, bgcolor=(128, 128, 128), bgalpha=0, holiday=[], ) options, args = parser.parse_args() len_args = len(args) if len_args < 2: parser.error(u'引数が足りません') elif len_args > 3: parser.error(u'引数が多すぎます') org_img_path = args[0] dst_img_path = args[1] if not os.path.isfile(org_img_path): parser.error(u'%s は存在しないファイルです' % org_img_path) if not options.force and os.path.isfile(dst_img_path): parser.error(u'%s は存在するファイルです' % dst_img_path) try: im = Image.open(org_img_path) except IOError, e: parser.error(e.args[0]) try: font = ImageFont.truetype(options.font_name, options.font_size) except IOError, e: parser.error(u'フォントを得ることができません') date = datetime.date.today() theyear = date.year if options.year is None else options.year themonth = date.month if options.month is None else options.month cal = ImageCalendar(6) cal.draw_month( im, theyear, themonth, font, position=options.position, padding=options.padding, color=options.color, sat_color=options.sat_color, sun_color=options.sun_color, alpha=options.alpha, bgcolor=options.bgcolor, bgalpha=options.bgalpha, holiday=options.holiday) im.save(dst_img_path) if options.preview: im.show() if __name__ == '__main__': main()
手順
まずは日付情報
まずは、描きたい月の 1 日が何曜日から始まって、何日まであるかなどの情報を得る。 datetime モジュールを使って調べてもいいけれど、 calendar モジュールにまかせれば簡単 。
まずは Calendar クラスのインスタンスをつくる。引数は週の始まりを何曜日にするか選ぶためにある。デフォルトは 0 になっている。 0 は月曜日を意味する。見慣れた日本のカレンダーは日曜スタートなので 6 にしておく。整数を直接書く代わりにcalendar.SUNDAY といったモジュール定数をつかうこともできる。
インスタンスができたら、 monthdayscalendar メソッドで 1 週間ずつのリストが得られる。 0 となっている部分は先月分、来月分。他の月の日付も 0 としてリストに含まれるため、週を表すリストの長さが 7 というのは保障されている。ちなみに週の数は可変。普通は 5 だけど、たとえば 2009年2月は 4 つ、 2009年5月は 6 つになる。曜日は日曜日が最初、土曜日が最後となる。
>>> from calendar import Calendar >>> cal = Calendar(6) >>> for week in cal.monthdayscalendar(2009, 11): ... print week ... [1, 2, 3, 4, 5, 6, 7] [8, 9, 10, 11, 12, 13, 14] [15, 16, 17, 18, 19, 20, 21] [22, 23, 24, 25, 26, 27, 28] [29, 30, 0, 0, 0, 0, 0] >>> for week in cal.monthdayscalendar(2009, 12): ... print week ... [0, 0, 1, 2, 3, 4, 5] [6, 7, 8, 9, 10, 11, 12] [13, 14, 15, 16, 17, 18, 19] [20, 21, 22, 23, 24, 25, 26] [27, 28, 29, 30, 31, 0, 0] >>> len(cal.monthdayscalendar(2009,2)) 4 >>> len(cal.monthdayscalendar(2009,5)) 6
曜日情報
iterweekdays メソッドで曜日を表す番号が得られる。
>>> list(cal.iterweekdays()) [6, 0, 1, 2, 3, 4, 5]
なにか文字列に直す時はこんな感じで。
>>> weekdays = (u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa', u'Su') >>> [weekdays[i] for i in cal.iterweekdays()] [u'Su', u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa']
カレンダー画像を用意
まずは PIL をインポートしてフォントを取得。 TrueType や OpenType のものは読めることを確認。たとえば IPA Pゴシック。
>>> from PIL import ImageFont >>> font = ImageFont.truetype(u'ipagp.otf', 24)
これで文字を書いたとき、どのくらいの大きさになるかを確認するには getsize メソッドが使える。
>>> font.getsize(u'spam') (96, 24)
カレンダーを書くのに最低限必要な要素は、 Su, や Mo のような曜日を意味する文字列と、 1, 2, 3, ... 31 の文字列。描画した際、どのくらいのスペースを要するかを、先ほどの getsize メソッドを使って確認する。
>>> weekdays = (u'Su', u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa') >>> days = tuple(map(unicode, range(1, 32))) >>> width = [] >>> height = [] >>> import itertools >>> for w, h in (font.getsize(t) for t in itertools.chain(weekdays, days)): ... width.append(w) ... height.append(h) ...
そして一番大きいもののサイズを確認する。
>>> width = max(width) >>> height = max(height) >>> width, height (48, 24)
このサイズの矩形を並べることでカレンダーとする。各矩形間に詰める dot 数はとりあえず 8, 4 ドットとした。
>>> padding_x = 8 >>> padding_y = 4 >>> >>> week [22, 23, 24, 25, 26, 27, 28] >>> padding_x = 8 >>> padding_y = 4 >>> weeks = len(cal.monthdayscalendar(2009, 11)) >>> weeks 5 calsize = (width * 7 + padding_x * 6, height * weeks + padding_y * (weeks - 1)) >>> calsize (384, 136)
これで必要とするカレンダー画像のサイズが 384, 136 と決まったので、このサイズで画像を作成する。まだ色は決める段階になく、文字の形が得られれば十分なので 2 値画像で。
>>> from PIL import Image >>> cal_im = Image.new('1', calsize)
この画像に文字列を書き込むため ImageDraw インスタンスを作成する。
>>> from PIL import ImageDraw >>> draw = ImageDraw.Draw(cal_im)
カレンダーをがしがし書き込んでいく。 width, height より小さいものはセンタリングするため、不足分 / 2 となる px, py をさらに位置に加える(曜日部分は取り合えず省略)。
>>> month = cal.monthdayscalendar(2009, 11) >>> for x, y in ((x, y) for x in range(7) for y in range(weeks)): ... i = month[y][x] ... t = unicode(i) ... fx, fy = font.getsize(t) ... px, py = (width - fx) // 2, (height - fy) // 2 ... if i: ... draw.text( ... (x * (width + padding_x) + px, y * (height + padding_y) + py), ... t, ... font=font, ... fill=1) ...
いったんプレビュー。
>>> cal_im.show()
黒地に白文字でカレンダーが描けていれば OK 。
画像にカレンダー画像を描き込む
先ほど作ったカレンダー画像を、ある画像 spam.jpg の 700, 100 位置に色 (255, 255, 0)、透明度 192、で描いてみる。
透明度 192 とするため、文字列部分の画素値 192 の 8bit画像 cal_im_L を作る。
>>> cal_im_L = Image.new('L', cal_im.size) >>> cal_im_L.paste(192, mask=cal_im)
spam.jpg を読み込み、 cal_im_L をマスクにして (255, 255, 0) 色をペースト。
>>> spam_im = Image.open(u'spam.jpg') >>> spam_im.paste((255, 255, 0), (700, 100), mask=cal_im_L)
完成。プレビューと保存は show と save メソッドで。
>>> spam_im.show()
>>> spam_im.save(u'spam_new.png')