Producing a printable calendar with Python
How can I produce a printable PDF file (US letter sized) such that each page represents a month and is partitioned such that each day of the month gets a box of equal size? What if I want to skip weekends and just display weekdays?
What Python modules would I use to accomplish the following?:
- Producing an image with the resolution of a US letter
- Iterating through each day of the month with the option to skip specific days (e.g., all weekends)
- Partitioning the image such that each day of the month is listed in a box of fixed size
- Repeating steps 2-开发者_开发百科3 for all months in a given year
- Producing a pdf as the output
You could do it with 3 packages. 'Reportlab' for producing the pdf, 'calendar' for getting the month as lists of lists, and python binding for 'Ghostscript' to transform the pdf produced into a png.
You would start by getting the data from the calendar package, using Reportlab to produce a page of US letter size. The table can be manipulated to have a grid, have each cell a box of the same size and alter the text font, size, and alignment.
You could leave it at that if you just want a pdf, or you can convert this pdf into a image using Ghostscript python bindings. Or if you like you can just run 'Ghostscript' using system('gs ...'). Also Ghostscript must be installed for the python 'Ghostscript' package to work.
If you want to filter out all weekends then you can use good old fashioned list manipulation on the calendar data for that.
Here is a example of how you could produce the pdf. I'm not going to do a whole year just a single month, and I'm not going to bother filtering out the zeros.
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])
If you want another page add PageBreak() followed by the next calendar to the list passed to doc.build(). PageBreak is part of reportlab.platypus.
And to convert the pdf to 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)
Both reportlab and ghostscript packages are available through the use of pip. I created the above in a 'virtualenv' environment.
ReportLab http://www.reportlab.com/software/opensource/rl-toolkit/
Ghostscript python bindings https://bitbucket.org/htgoebel/python-ghostscript
calendar is part of the standard python library.
For anyone who wanders in off Google, a fellow named Bill Mill wrote a public domain module that makes generating a calendar using reportlab as simple as this example text.
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()
There's also sample output at the link I provided and, while it's simple and spartan, it looks decent (similar to what you get out of things like the "make calendar" script for Scribus) and would make an excellent starting point for future enhancements.
Full code:
#!/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()
EDIT 2017-11-25: I had to refactor this for my own use, so I thought I'd share it here. The newest version will always be in this GitHub Gist but, below, I'm including the last revision before it gained a dependency on PyEphem for calculating things like moon phases:
#!/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))
The refactored code has the following advantages:
- The calendar drawing function doesn't draw anything other than the marginless cells themselves, so it's useful for embedding the output into larger creations.
- The code to draw individual cells has been factored out into a callback which receives a freshly-reset canvas state each time.
- It's all nicely documented now. (Admittedly, in ePydoc markup I haven't run through ePyDoc yet)
- Code to draw top-aligned ordinal suffixes on numbers
- PEP-8 compliant code style and proper metadata.
UPDATE 2021-09-30: Here's what the calendar.pdf
generated by that last code block looks like when viewed in Okular 1.9.3 at 75%:
(Ignore the varying line widths. That's just an Okular 1.9.3 rendering bug that shows up at certain zoom levels.)
I had a similar roblem a while back - I used the excellent pcal utility. It's not python but even as a python bigot I found severe limitations getting reliable printable PDFs from python - my LaTeX was not good enough
http://www.itmanagerscookbook.com/Workstation/power-user/calendar.html
精彩评论