使用Python生成可打印的日历
我想制作一个可以打印的PDF文件(美国信纸大小),每一页代表一个月,并且每一天都有一个大小相等的框。如果我想跳过周末,只显示工作日,该怎么做呢?
我需要用到哪些Python模块来完成以下任务?:
- 生成一个分辨率为美国信纸大小的图像
- 遍历每个月的每一天,并可以选择跳过特定的日子(比如,所有的周末)
- 将图像分割成每一天都有一个固定大小的框
- 对一年中的所有月份重复步骤2和3
- 将结果输出为一个PDF文件
3 个回答
我之前也遇到过类似的问题,那时候我用了一个很棒的工具叫pcal。虽然它不是用Python写的,但即使我是个Python的忠实粉丝,我还是发现用Python生成可靠的可打印PDF文件有很多限制——我的LaTeX水平不够好。
http://www.itmanagerscookbook.com/Workstation/power-user/calendar.html
对于那些从谷歌过来的朋友,有个叫比尔·米尔的人写了一个公共领域模块,用它生成日历变得非常简单,就像这个示例文本一样。
from pdf_calendar import createCalendar
#create a December, 2005 PDF
c = createCalendar(12, 2005, filename="blog_calendar.pdf")
#now add January, 2006 to the end
createCalendar(1, 2006, canvas=c)
c.save()
在我提供的链接中还有示例输出,虽然看起来简单朴素,但效果不错(类似于Scribus的“制作日历”脚本生成的效果),这可以作为未来改进的一个很好的起点。
完整代码:
#!/usr/bin/env python
"""Create a PDF calendar.
This script requires Python and Reportlab
( http://reportlab.org/rl_toolkit.html ). Tested only with Python 2.4 and
Reportlab 1.2.
See bottom of file for an example of usage. No command-line interface has been
added, but it would be trivial to do so. Furthermore, this script is pretty
hacky, and could use some refactoring, but it works for what it's intended
to do.
Created by Bill Mill on 11/16/05, this script is in the public domain. There
are no express warranties, so if you mess stuff up with this script, it's not
my fault.
If you have questions or comments or bugfixes or flames, please drop me a line
at bill.mill@gmail.com .
"""
from reportlab.lib import pagesizes
from reportlab.pdfgen.canvas import Canvas
import calendar, time, datetime
from math import floor
NOW = datetime.datetime.now()
SIZE = pagesizes.landscape(pagesizes.letter)
class NoCanvasError(Exception): pass
def nonzero(row):
return len([x for x in row if x!=0])
def createCalendar(month, year=NOW.year, canvas=None, filename=None, \
size=SIZE):
"""
Create a one-month pdf calendar, and return the canvas
month: can be an integer (1=Jan, 12=Dec) or a month abbreviation (Jan, Feb,
etc.
year: year in which month falls. Defaults to current year.
canvas: you may pass in a canvas to add a calendar page to the end.
filename: String containing the file to write the calendar to
size: size, in points of the canvas to write on
"""
if type(month) == type(''):
month = time.strptime(month, "%b")[1]
if canvas is None and filename is not None:
canvas = Canvas(filename, size)
elif canvas is None and filename is None:
raise NoCanvasError
monthname = time.strftime("%B", time.strptime(str(month), "%m"))
cal = calendar.monthcalendar(year, month)
width, height = size
#draw the month title
title = monthname + ' ' + str(year)
canvas.drawCentredString(width / 2, height - 27, title)
height = height - 40
#margins
wmar, hmar = width/50, height/50
#set up constants
width, height = width - (2*wmar), height - (2*hmar)
rows, cols = len(cal), 7
lastweek = nonzero(cal[-1])
firstweek = nonzero(cal[0])
weeks = len(cal)
rowheight = floor(height / rows)
boxwidth = floor(width/7)
#draw the bottom line
canvas.line(wmar, hmar, wmar+(boxwidth*lastweek), hmar)
#now, for all complete rows, draw the bottom line
for row in range(1, len(cal[1:-1]) + 1):
y = hmar + (row * rowheight)
canvas.line(wmar, y, wmar + (boxwidth * 7), y)
#now draw the top line of the first full row
y = hmar + ((rows-1) * rowheight)
canvas.line(wmar, y, wmar + (boxwidth * 7), y)
#and, then the top line of the first row
startx = wmar + (boxwidth * (7-firstweek))
endx = startx + (boxwidth * firstweek)
y = y + rowheight
canvas.line(startx, y, endx, y)
#now draw the vert lines
for col in range(8):
#1 = don't draw line to first or last; 0 = do draw
last, first = 1, 1
if col <= lastweek: last = 0
if col >= 7 - firstweek: first = 0
x = wmar + (col * boxwidth)
starty = hmar + (last * rowheight)
endy = hmar + (rows * rowheight) - (first * rowheight)
canvas.line(x, starty, x, endy)
#now fill in the day numbers and any data
x = wmar + 6
y = hmar + (rows * rowheight) - 15
for week in cal:
for day in week:
if day:
canvas.drawString(x, y, str(day))
x = x + boxwidth
y = y - rowheight
x = wmar + 6
#finish this page
canvas.showPage()
return canvas
if __name__ == "__main__":
#create a December, 2005 PDF
c = createCalendar(12, 2005, filename="blog_calendar.pdf")
#now add January, 2006 to the end
createCalendar(1, 2006, canvas=c)
c.save()
编辑于2017-11-25:我为了自己的使用对这个代码进行了重构,所以想在这里分享一下。最新版本会一直在这个GitHub Gist上,但下面我会包括在它依赖于PyEphem计算月相之前的最后一个修订版本:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Generate a printable calendar in PDF format, suitable for embedding
into another document.
Tested with Python 2.7 and 3.8.
Dependencies:
- Python
- Reportlab
Resources Used:
- https://stackoverflow.com/a/37443801/435253
(Originally present at http://billmill.org/calendar )
- https://www.reportlab.com/docs/reportlab-userguide.pdf
Originally created by Bill Mill on 11/16/05, this script is in the public
domain. There are no express warranties, so if you mess stuff up with this
script, it's not my fault.
Refactored and improved 2017-11-23 by Stephan Sokolow (http://ssokolow.com/).
TODO:
- Implement diagonal/overlapped cells for months which touch six weeks to avoid
wasting space on six rows.
"""
from __future__ import (absolute_import, division, print_function,
with_statement, unicode_literals)
__author__ = "Bill Mill; Stephan Sokolow (deitarion/SSokolow)"
__license__ = "CC0-1.0" # https://creativecommons.org/publicdomain/zero/1.0/
import calendar, collections, datetime
from contextlib import contextmanager
from reportlab.lib import pagesizes
from reportlab.pdfgen.canvas import Canvas
# Supporting languages like French should be as simple as editing this
ORDINALS = {
1: 'st', 2: 'nd', 3: 'rd',
21: 'st', 22: 'nd', 23: 'rd',
31: 'st',
None: 'th'}
# Something to help make code more readable
Font = collections.namedtuple('Font', ['name', 'size'])
Geom = collections.namedtuple('Geom', ['x', 'y', 'width', 'height'])
Size = collections.namedtuple('Size', ['width', 'height'])
@contextmanager
def save_state(canvas):
"""Simple context manager to tidy up saving and restoring canvas state"""
canvas.saveState()
yield
canvas.restoreState()
def add_calendar_page(canvas, rect, datetime_obj, cell_cb,
first_weekday=calendar.SUNDAY):
"""Create a one-month pdf calendar, and return the canvas
@param rect: A C{Geom} or 4-item iterable of floats defining the shape of
the calendar in points with any margins already applied.
@param datetime_obj: A Python C{datetime} object specifying the month
the calendar should represent.
@param cell_cb: A callback taking (canvas, day, rect, font) as arguments
which will be called to render each cell.
(C{day} will be 0 for empty cells.)
@type canvas: C{reportlab.pdfgen.canvas.Canvas}
@type rect: C{Geom}
@type cell_cb: C{function(Canvas, int, Geom, Font)}
"""
calendar.setfirstweekday(first_weekday)
cal = calendar.monthcalendar(datetime_obj.year, datetime_obj.month)
rect = Geom(*rect)
# set up constants
scale_factor = min(rect.width, rect.height)
line_width = scale_factor * 0.0025
font = Font('Helvetica', scale_factor * 0.028)
rows = len(cal)
# Leave room for the stroke width around the outermost cells
rect = Geom(rect.x + line_width,
rect.y + line_width,
rect.width - (line_width * 2),
rect.height - (line_width * 2))
cellsize = Size(rect.width / 7, rect.height / rows)
# now fill in the day numbers and any data
for row, week in enumerate(cal):
for col, day in enumerate(week):
# Give each call to cell_cb a known canvas state
with save_state(canvas):
# Set reasonable default drawing parameters
canvas.setFont(*font)
canvas.setLineWidth(line_width)
cell_cb(canvas, day, Geom(
x=rect.x + (cellsize.width * col),
y=rect.y + ((rows - row) * cellsize.height),
width=cellsize.width, height=cellsize.height),
font, scale_factor)
# finish this page
canvas.showPage()
return canvas
def draw_cell(canvas, day, rect, font, scale_factor):
"""Draw a calendar cell with the given characteristics
@param day: The date in the range 0 to 31.
@param rect: A Geom(x, y, width, height) tuple defining the shape of the
cell in points.
@param scale_factor: A number which can be used to calculate sizes which
will remain proportional to the size of the entire calendar.
(Currently the length of the shortest side of the full calendar)
@type rect: C{Geom}
@type font: C{Font}
@type scale_factor: C{float}
"""
# Skip drawing cells that don't correspond to a date in this month
if not day:
return
margin = Size(font.size * 0.5, font.size * 1.3)
# Draw the cell border
canvas.rect(rect.x, rect.y - rect.height, rect.width, rect.height)
day = str(day)
ordinal_str = ORDINALS.get(int(day), ORDINALS[None])
# Draw the number
text_x = rect.x + margin.width
text_y = rect.y - margin.height
canvas.drawString(text_x, text_y, day)
# Draw the lifted ordinal number suffix
number_width = canvas.stringWidth(day, font.name, font.size)
canvas.drawString(text_x + number_width,
text_y + (margin.height * 0.1),
ordinal_str)
def generate_pdf(datetime_obj, outfile, size, first_weekday=calendar.SUNDAY):
"""Helper to apply add_calendar_page to save a ready-to-print file to disk.
@param datetime_obj: A Python C{datetime} object specifying the month
the calendar should represent.
@param outfile: The path to which to write the PDF file.
@param size: A (width, height) tuple (specified in points) representing
the target page size.
"""
size = Size(*size)
canvas = Canvas(outfile, size)
# margins
wmar, hmar = size.width / 50, size.height / 50
size = Size(size.width - (2 * wmar), size.height - (2 * hmar))
add_calendar_page(canvas,
Geom(wmar, hmar, size.width, size.height),
datetime_obj, draw_cell, first_weekday).save()
if __name__ == "__main__":
generate_pdf(datetime.datetime.now(), 'calendar.pdf',
pagesizes.landscape(pagesizes.letter))
重构后的代码有以下优点:
- 日历绘制功能只绘制没有边距的单元格本身,因此可以方便地嵌入到更大的作品中。
- 绘制单个单元格的代码被提取到一个回调函数中,每次都会接收到一个新重置的画布状态。
- 现在文档注释写得很好。(老实说,是用ePydoc标记的,我还没有用ePyDoc处理过)
- 绘制数字上方对齐的序数后缀的代码。
- 符合PEP-8的代码风格和适当的元数据。
更新于2021-09-30:这是最后一个代码块生成的calendar.pdf
在Okular 1.9.3中以75%缩放查看时的样子:
(忽略不同的线宽。这只是Okular 1.9.3在某些缩放级别下的渲染错误。)
你可以用三个工具包来完成这个任务。第一个是'Reportlab',用来生成PDF文件;第二个是'calendar',用来获取月份的数据,格式是列表的列表;第三个是'Ghostscript'的Python绑定,用来把生成的PDF转换成PNG图片。
首先,你需要从calendar工具包中获取数据,然后用Reportlab来制作一页美国信纸大小的页面。你可以调整表格的样式,让每个单元格都是相同大小的方框,还可以改变文字的字体、大小和对齐方式。
如果你只想要一个PDF文件,可以就这样结束;但如果你想把这个PDF转换成图片,可以使用Ghostscript的Python绑定。或者你也可以直接用系统命令运行'Ghostscript',命令是'system('gs ...')'。不过要注意,Ghostscript必须先安装好,才能让Python的'Ghostscript'包正常工作。
如果你想过滤掉所有的周末,可以对calendar的数据进行一些传统的列表操作。
下面是一个生成PDF的例子。我只会做一个月的,不会处理那些零的情况。
from reportlab.lib.units import inch
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
from reportlab.graphics.shapes import Drawing
import calendar
doc = SimpleDocTemplate('calendar.pdf', pagesize=letter)
cal = [['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']]
cal.extend(calendar.monthcalendar(2011,9))
table = Table(cal, 7*[inch], len(cal) * [inch])
table.setStyle(TableStyle([
('FONT', (0, 0), (-1, -1), 'Helvetica'),
('FONT', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 8),
('INNERGRID', (0, 0), (-1, -1), 0.25, colors.black),
('BOX', (0, 0), (-1, -1), 0.25, colors.green),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
]))
#create the pdf with this
doc.build([table])
如果你想添加另一页,可以用PageBreak(),然后把下一个日历添加到传给doc.build()的列表中。PageBreak是reportlab.platypus的一部分。
要把PDF转换成PNG,可以使用
import ghostscript
args = ['gs', #it seems not to matter what is put in 1st position
'-dSAFER',
'-dBATCH',
'-dNOPAUSE',
'-sDEVICE=png16m',
'-r100',
'-sOutputFile=calendar.png',
'calendar.pdf']
ghostscript.Ghostscript(*args)
Reportlab和Ghostscript这两个工具包都可以通过pip安装。我是在一个'virtualenv'环境中创建的上述内容。
ReportLab http://www.reportlab.com/software/opensource/rl-toolkit/
Ghostscript Python绑定 https://bitbucket.org/htgoebel/python-ghostscript
calendar是Python标准库的一部分。