使用 Python 生成可打印的日历

发布于 2024-12-02 16:32:38 字数 294 浏览 0 评论 0 原文

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

我将使用哪些 Python 模块来完成以下任务?

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

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?:

  1. Producing an image with the resolution of a US letter
  2. Iterating through each day of the month with the option to skip specific days (e.g., all weekends)
  3. Partitioning the image such that each day of the month is listed in a box of fixed size
  4. Repeating steps 2-3 for all months in a given year
  5. Producing a pdf as the output

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

蘸点软妹酱 2024-12-09 16:32:38

你可以用 3 个包来完成。 “Reportlab”用于生成 pdf,“calendar”用于获取月份列表,以及“Ghostscript”的 python 绑定用于将生成的 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)

reportlab 和 Ghostscript 软件包都可以通过使用 pip 获得。我在“virtualenv”环境中创建了上述内容。

报告实验室
http://www.reportlab.com/software/opensource/rl-toolkit/

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

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

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.

得不到的就毁灭 2024-12-09 16:32:38

对于任何离开 Google 的人,一位名叫 Bill Mill 的人编写了一个公共领域模块 这使得使用 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 的“制作日历”脚本中获得的结果),并且将为未来的增强功能提供一个极好的起点。

完整代码:

#!/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 [email protected] .
"""
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% 的分辨率查看时的样子:

example calendar

(忽略不同的线宽。这只是 Okular 1.9.3 渲染错误,会在某些缩放级别出现。)

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 [email protected] .
"""
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%:

example calendar

(Ignore the varying line widths. That's just an Okular 1.9.3 rendering bug that shows up at certain zoom levels.)

不语却知心 2024-12-09 16:32:38

不久前我遇到了类似的问题 - 我使用了出色的 pcal 实用程序。它不是 python,但即使作为一个 python 偏执者,我也发现从 python 获取可靠的可打印 PDF 存在严重限制 - 我的 LaTeX 不够好

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

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文