使用Python生成可打印的日历

时间:2011-09-01 20:18:52

标签: python image pdf calendar

如何生成可打印的PDF文件(美国字母大小),以便每个页面代表一个月并进行分区,以便每月的每一天获得相同大小的方框?如果我想跳过周末并只显示工作日怎么办?

我将使用哪些Python模块来完成以下操作?

  1. 制作分辨率为美国字母的图像
  2. 在每月的每一天进行迭代,并选择跳过特定日期(例如,所有周末)
  3. 对图像进行分区,使得每月的每一天都列在固定大小的框中
  4. 对给定年份中的所有月份重复步骤2-3
  5. 制作pdf作为输出

3 个答案:

答案 0 :(得分:7)

你可以用3个包来做。 'Reportlab'用于生成pdf,'calendar'用于将月份作为列表列表,'python绑定用于'Ghostscript'以将生成的pdf转换为png。

您首先要从日历包中获取数据,然后使用Reportlab生成一个美国字母大小的页面。该表可以被操作以具有网格,每个单元格具有相同大小的框并且改变文本字体,大小和对齐。

如果你只想要一个pdf,你可以把它留在那,或者你可以使用Ghostscript python绑定将这个pdf转换为图像。或者如果你喜欢,你可以使用system('gs ...')运行'Ghostscript'。还必须安装Ghostscript才能使python'Ghostscript'程序包正常工作。

如果您想过滤掉所有周末,那么您可以在日历数据上使用旧的老式列表操作。

以下是如何制作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)

可以通过使用pip获得reportlab和ghostscript包。我在'virtualenv'环境中创建了上述内容。

ReportLab的 http://www.reportlab.com/software/opensource/rl-toolkit/

Ghostscript python绑定 https://bitbucket.org/htgoebel/python-ghostscript

日历是标准python库的一部分。

答案 1 :(得分:2)

对于那些在Google上游荡的人来说,名叫Bill Mill的人写了一个public domain module,使用reportlab生成日历就像这个示例文本一样简单。

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的“make calendar”脚本中得到的东西)并且会成为一个很好的起点未来的改进。

完整代码:

#!/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:我不得不重构这个以供我自己使用,所以我想我会在这里分享。最新版本将始终为in this GitHub Gist但是,在下面,我将包含最后一个修订版,然后才获得对PyEphem的依赖,以计算月相的事情:

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""Generate a printable calendar in PDF format, suitable for embedding
into another document.

Tested with Python 2.7.

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的代码风格和适当的元数据。

答案 2 :(得分:1)

我曾经有类似的roblem - 我使用了优秀的pcal实用程序。它不是python,但即使作为python bigot,我发现从python获得可靠的可打印PDF的严重限制 - 我的LaTeX不够好

http://www.itmanagerscookbook.com/Workstation/power-user/calendar.html