Python Code To Generate Full Size Yearly Calendar in Inkscape

Originally Posted on with tags: python / inkscape
Last Update on

At-A-Glance DayMinder Monthly Planner 7” x 8-3/4” Version had been my calendar for a few years. Then I found the excellent PDF software PDFXChange-Viewer and I started downloading monthly PDF calendars from timeanddate.com. Each file was for one year, it had 13 pages. The first page was a yearly calendar and other 12 pages were monthly calendars. I marked events on the PDF directly with 10 point “Narrow Arial” fonts. This continued for a few years.

After I became familiar with Inkscape extension and Python programming, I wrote a python program which generates full size (36” x 24”) one page yearly calendar in Inkscape. I save both SVG and PDF files.

Now the one page PDF file is my daily calendar, and I mark events on the PDF. My event descriptions are usually very short like “9am meeting with Don”, “10:45 See Dr Jeff”, and “off work vacation in FL

Here is what the yearly calendar looks like.

calendar example

Here are the links for calendars from 2019 to 2025. I will add more links to this page in year 2025.

Here are the code in create_calendar.inx and create_calendar.py files.

<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
  <_name>Create Calendar</_name>
  <id>create_calendar.com.gotrafficsign</id>
  <dependency type="executable" location="extensions">inkex.py</dependency>
  <dependency type="executable" location="extensions">simplestyle.py</dependency>
  <param name="yearNumber" type="int" min="0" max="2100" gui-text="For Year: ">2019</param>

  <effect>
    <object-type>all</object-type>
    <effects-menu>
      <submenu _name="OpenCalendar">
      </submenu>
    </effects-menu>
  </effect>

  <script>
    <command reldir="extensions" interpreter="python">create_calendar.py</command>
  </script>

</inkscape-extension>
#! /usr/bin/python
# -*- coding: utf-8 -*-
"""
# create_calendar.py
Create post size calendar file in Inkscape

Copyright (C) December 08 2018 George Zhang

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
"""

import inkex
import simplestyle
import simpletransform
import sys
import math
import copy
import os
import re
import datetime
import calendar
import logging

class CreateCalendar(inkex.Effect):

    def __init__(self):
        inkex.Effect.__init__(self)
        self.OptionParser.add_option(
            "--yearNumber", action="store", type="int", dest="yearN", default="1")

        logging.basicConfig(level=logging.DEBUG, filename="logging.txt")

    def effect(self):
        PAGE_W = 36
        PAGE_H = 24

        w = PAGE_W
        h = PAGE_H

        self.change_SVG_size(w, h)

        year = self.options.yearN
        month = 1
        width = 9
        height = 8

        i = j = 0
        for month in range(1, 13):
            i = (month-1) % 4
            j = (month-1) / 4 # python 2 only
            x = i * 9
            y = j * 8
            self.draw_month(year, month, width, height, x, y)

    def change_SVG_size(self, width, height):
        ratio = 25.4
        svg_elem = self.document.getroot()

        page_width = width
        page_height = height

        svg_elem.set('width', str(page_width) + 'in')
        svg_elem.set('height', str(page_height) + 'in')
        svg_elem.set('viewBox', '0 0 ' + str(page_width * ratio) + ' '
                     + str(page_height * ratio))

    def draw_SVG_line(self, (x1, y1), (x2, y2), style, name, parent):
        '''style is a dict'''
        line_attribs = {'style': simplestyle.formatStyle(style),
                        inkex.addNS('label', 'inkscape'): name,
                        'd': 'M ' + str(x1) + ',' + str(y1) + ' L' +
                        str(x2) + ',' + str(y2)}
        elm = inkex.etree.SubElement(
            parent, inkex.addNS('path', 'svg'), line_attribs)
        return elm

    def draw_lines(self, layer, width, height):
        line_style = {'stroke': '#000000',
                      'stroke-width': str(self.unittouu('1px')),
                      'fill': 'none'}

        for i in range(1, 4):
            x1_str = str(9 * i) + 'in'
            y1_str = '0.25in'
            y2_str = str(height - 0.25) + 'in'
            x1, x2, y1, y2 = map(
                self.unittouu, [x1_str, x1_str, y1_str, y2_str])
            self.draw_SVG_line((x1, y1), (x2, y2), line_style, 'line', layer)

        for j in range(1, 3):
            x1_str = '0.25in'
            x2_str = str(width - 0.25) + 'in'
            y1_str = str(8 * j) + 'in'
            x1, x2, y1, y2 = map(
                self.unittouu, [x1_str, x2_str, y1_str, y1_str])
            self.draw_SVG_line((x1, y1), (x2, y2), line_style, 'line', layer)

    def draw_grid(self, layer, width, height, num_col, num_row, x, y):
        '''width and height, in in inches, same for x and y'''
        line_style = {'stroke': '#000000',
                      'stroke-width': str(self.unittouu('1px')),
                      'fill': 'none'}

        for i in range(num_col + 1):
            x1_str = str(x + (float(width)/num_col) * i) + 'in'
            x2_str = x1_str
            y1_str = str(y) + 'in'
            y2_str = str(y + height) + 'in'
            x1, x2, y1, y2 = map(
                self.unittouu, [x1_str, x2_str, y1_str, y2_str])
            self.draw_SVG_line((x1, y1), (x2, y2), line_style, 'line', layer)

        for i in range(num_row + 1):
            x1_str = str(x) + 'in'
            x2_str = str(x + width) + 'in'
            y1_str = str(y + (float(height)/num_row) * i) + 'in'
            y2_str = y1_str
            x1, x2, y1, y2 = map(
                self.unittouu, [x1_str, x2_str, y1_str, y2_str])
            logging.debug("%s %s %s %s" % (x1_str, x2_str, y1_str, y2_str))
            logging.debug("%s %s %s %s" % (x1, x2, y1, y2))
            self.draw_SVG_line((x1, y1), (x2, y2), line_style, 'line', layer)

    def draw_month(self, year, month, width, height, x, y):
        '''height and width in inches int
        x and y in inches from top left corner'''
        # num_col = 7
        # num_row = 5
        cal = calendar.Calendar(6) # set Sunday as first day

        width = width - 0.4
        height = height - 0.4
        x = x + 0.2
        y = y + 0.2

        # assert num_row in (4, 5, 6), "num of row should be 4 5 or 6"
        # width_col = float(width) / num_col
        # height_row = float(height) / num_row

        x_month = x + float(width) / 2
        y_month = y
        ratio = 25.4
        x_month_R, y_month_R = (x_month * ratio, y_month * ratio)

        y_month_R += 9.15 + 7 # 7 from observation
        text_layer = self.find_create_layer(self.document.getroot(),
            'text_layer')
        elem = self.draw_month_text(x_month_R, y_month_R, month, year)
        text_layer.append(elem)

        x_dayofweek = x + float(width) / 7.0 / 2
        y_dayofweek = y + 1.35
        x_dayofweek_R, y_dayofweek_R = (x_dayofweek * ratio,
            y_dayofweek * ratio)

        for i in range(7):
            elem = self.draw_weekday_text(x_dayofweek_R, y_dayofweek_R, i)
            text_layer.append(elem)
            x_dayofweek_R += float(width) / 7.0 * ratio

        bk_layer = self.find_create_layer(self.document.getroot(), 'bk_layer')

        num_col = 7
        num_row = len(cal.monthdayscalendar(year, month))

        self.draw_grid(bk_layer, width, height - 1.5, num_col, num_row,
            x, y+1.5)

        cal_matrix = cal.monthdayscalendar(year, month)

        for i in range(num_row):
            for j in range(num_col):
                day_num = cal_matrix[i][j]
                if day_num != 0:
                    self.draw_text(text_layer, width, height - 1.5,
                        num_col, num_row, x, y+1.5, str(day_num), j, i)

    def draw_text(self, layer, width, height, num_col, num_row, x, y,
        name, i, j):
        '''place day number in a monthly grid'''

        x_loc = x + (float(width)/num_col) * i
        y_loc = y + (float(height)/num_row) * j # top left corner

        ratio = 25.4

        x_loc_R = x_loc * ratio
        y_loc_R = y_loc * ratio

        y_loc_R += 6.097  # vertical adjustement, vertical align baseline

        x_loc_R += 2
        y_loc_R += 2  # second adjustment

        elem = self.create_text(x_loc_R, y_loc_R, name)
        layer.append(elem)

    def create_text(self, x, y, name):

        style_d = {'font-size': '6.34px',  # 18 pt
                   'font-family': 'Roboto',
                   #   'text-align': 'center',
                   'text-anchor': 'start',
                   'fill': '#000000',
                   'stroke': 'none',
                   }
        t = inkex.etree.Element('text')
        t.set(inkex.addNS('space', 'xml'), 'preserve')
        t.set('x', str(x))
        t.set('y', str(y))
        t.set('style', simplestyle.formatStyle(style_d))

        sp = inkex.etree.SubElement(t, 'tspan')
        sp.set(inkex.addNS('role', 'sodipodi'), 'line')
        sp.text = name
        return t

    def draw_month_text(self, x, y, month, year):
        month_name_str = ['0', 'January', 'February', 'March', 'April',
            'May', 'June', 'July', 'August',
            'September', 'October', 'November', 'December']

        style_d = {'font-size': '12.69px',  # 24 pt
                   'font-family': 'Roboto',
                   #'text-align': 'center',
                   'text-anchor': 'middle',
                   'fill': '#000000',
                   'stroke': 'none',
                   }
        t = inkex.etree.Element('text')
        t.set(inkex.addNS('space', 'xml'), 'preserve')
        t.set('x', str(x))
        t.set('y', str(y))
        t.set('style', simplestyle.formatStyle(style_d))

        sp = inkex.etree.SubElement(t, 'tspan')
        sp.set(inkex.addNS('role', 'sodipodi'), 'line')
        sp.text = month_name_str[month] + ' ' + str(year)
        return t

    def draw_weekday_text(self, x, y, dayofweek):
        dayofweek_str = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
            'Thursday', 'Friday', 'Saturday' ]

        style_d = {'font-size': '8.46px',  # 24 pt
                   'font-family': 'Roboto',
                   #'text-align': 'center',
                   'text-anchor': 'middle',
                   'fill': '#000000',
                   'stroke': 'none',
                   }
        t = inkex.etree.Element('text')
        t.set(inkex.addNS('space', 'xml'), 'preserve')
        t.set('x', str(x))
        t.set('y', str(y))
        t.set('style', simplestyle.formatStyle(style_d))

        sp = inkex.etree.SubElement(t, 'tspan')
        sp.set(inkex.addNS('role', 'sodipodi'), 'line')
        sp.text = dayofweek_str[dayofweek][0:3]
        return t

    def find_create_layer(self, parent, layer_name):
        # this should not be svg:g
        path = '//g[@inkscape:label="%s"]' % layer_name
        path += '|//svg:g[@inkscape:label="%s"]' % layer_name
        el_list = self.document.xpath(path, namespaces=inkex.NSS)
        # inkex.debug(el_list)
        if el_list:
            layer = el_list[0]
            #inkex.debug('this code never execute, why?')
        else:
            layer = inkex.etree.SubElement(parent, 'g')
            layer.set(inkex.addNS('label', 'inkscape'), layer_name)
            layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
        return layer

    def create_group(self, group_name):
        group = inkex.etree.Element('g')
        group.set(inkex.addNS('label', 'inkscape'), group_name)
        group.set('fill', 'none')
        return group

if __name__ == '__main__':
    e = CreateCalendar()
    e.affect()