Skip to content

Instantly share code, notes, and snippets.

@goerz
Created October 7, 2018 15:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save goerz/d0e1a28be8f322bf7bad3e9b8672bc67 to your computer and use it in GitHub Desktop.
Save goerz/d0e1a28be8f322bf7bad3e9b8672bc67 to your computer and use it in GitHub Desktop.
Script to generate a 3-page yearly calendar via luatex
#!/usr/bin/env python
"""Script to generate a yearly calendar via luatex.
Requires the `lualatex` executable to be in the PATH.
Requires the Rotis Semi Serif (55 Roman) and Rotis Semi Sans (55 Regular)
fonts, available at
https://www.linotype.com/49242/rotis-semi-serif-family.html
https://www.linotype.com/49198/rotis-semi-sans-family.html
You may edit the tex template in this script to use different fonts, or to only
use Rotis Sans.
The Jinja-templates for LaTeX are based on the ideas in
http://eosrei.net/articles/2015/11/latex-templates-python-and-jinja2-generate-pdfs
https://web.archive.org/web/20121024021221/http://e6h.de/post/11/
The templates output for letter-sized paper; again, you can modify the template
to change this.
"""
import sys
import os
import subprocess
import shutil
import tempfile
import calendar
import click
import jinja2
def weeknumber(d):
"""Week number for given day"""
return d.isocalendar()[1]
def get_num_weeks(year, month):
"""Get the number of weeks in the given month
This includes the week with the first of the month, but excludes the week
with the first of the next months. The result is either 4 or 5
"""
cal = calendar.Calendar()
weeks = list(cal.monthdatescalendar(int(year), int(month)))
num_weeks = len(weeks)
if weeks[-1][-1].month != month:
num_weeks += -1
return num_weeks
def get_row_week(year, month):
"""Return a string with tikz-tuples 'row/week'
In the result, row is the one-based weeknumber within the month (i.e. the
row at which it appears in the calendar), and week is the calendar week
number
"""
cal = calendar.Calendar()
weeks = list(cal.monthdatescalendar(int(year), int(month)))
out_tuples = []
for row, week in enumerate(weeks):
if week[-1].month == month:
out_tuples.append("%d/%d" % (row+1, weeknumber(week[0])))
return ", ".join(out_tuples)
def get_row_col_day(year, month):
"""Return a string with tikz-tuples 'row/col/day'
In the result, `row` is the one-based week number within the month (i.e.
the row at which it appears in the calendar), `col` is the one-based index
of the week day (i.e. the column at which it appears in the calendar), and
`day` is the day number within the month.
"""
cal = calendar.Calendar()
weeks = list(cal.monthdatescalendar(int(year), int(month)))
out_tuples = []
for row, week in enumerate(weeks):
if week[-1].month == month:
for col, day in enumerate(week):
out_tuples.append("%d/%d/%d" % (row+1, col+1, day.day))
return ", ".join(out_tuples)
LATEX_JINJA_ENV = jinja2.Environment(
block_start_string='\BLOCK{',
block_end_string='}',
variable_start_string='\VAR{',
variable_end_string='}',
comment_start_string='\#{',
comment_end_string='}',
line_statement_prefix='%%',
line_comment_prefix='%#',
trim_blocks=True,
autoescape=False,
)
MAIN_TEMPLATE = LATEX_JINJA_ENV.from_string(r'''
% compile with lualatex
\documentclass{article}
\usepackage{xcolor}
\usepackage[letterpaper, total={8.5in, 10.75in}]{geometry} % no margins
\usepackage[utf8]{inputenc}
\usepackage{fontspec}
\usepackage{tikz}
\usetikzlibrary{calc}
\pagestyle{empty}
\setlength{\parindent}{0in}
\definecolor{405U}{cmyk}{0.5,0.45,0.52,0.1} % PANTONE 405 U
\setmainfont[Color=405U]{RotisSemiSerifStd}
\setsansfont[Color=405U]{RotisSemiSansStd}
\renewcommand{\small}{\fontsize{6.0}{7.2}\selectfont}
\renewcommand{\normalsize}{\fontsize{8.0}{9.6}\selectfont}
\renewcommand{\large}{\fontsize{10.0}{12.0}\selectfont}
\renewcommand{\familydefault}{\sfdefault} % use sans-serif by default
\normalsize
\begin{document}
\tikzset{%
every picture/.style={line width=0.2pt, color=405U},
thick/.style={line width=0.5pt},
}
\pgfmathsetmacro{\Margin}{0.25}
\pgfmathsetmacro{\FullMonthHeight}{5.125}
\pgfmathsetmacro{\DayHeight}{0.8}
\pgfmathsetmacro{\DayWidth}{0.54}
\pgfmathsetmacro{\MondayExtraWidth}{0.653 - \DayWidth}
\pgfmathsetmacro{\FullMonthWidth}{\MondayExtraWidth + 7 * \DayWidth}
\begin{tikzpicture}[overlay, remember picture, x=1in, y=1in]
\begin{scope}[xshift=0.25in, yshift=-5.135in]
\VAR{month01}
\end{scope}
\begin{scope}[xshift=4.375in, yshift=-5.135in]
\VAR{month02}
\end{scope}
\begin{scope}[xshift=0.25in, yshift=-10.51in]
\VAR{month03}
\end{scope}
\begin{scope}[xshift=4.375in, yshift=-10.51in]
\VAR{month04}
\end{scope}
\end{tikzpicture}
\newpage
\begin{tikzpicture}[overlay, remember picture, x=1in, y=1in]
\begin{scope}[xshift=0.25in, yshift=-5.135in]
\VAR{month05}
\end{scope}
\begin{scope}[xshift=4.375in, yshift=-5.135in]
\VAR{month06}
\end{scope}
\begin{scope}[xshift=0.25in, yshift=-10.51in]
\VAR{month07}
\end{scope}
\begin{scope}[xshift=4.375in, yshift=-10.51in]
\VAR{month08}
\end{scope}
\end{tikzpicture}
\newpage
\begin{tikzpicture}[overlay, remember picture, x=1in, y=1in]
\begin{scope}[xshift=0.25in, yshift=-5.135in]
\VAR{month09}
\end{scope}
\begin{scope}[xshift=4.375in, yshift=-5.135in]
\VAR{month10}
\end{scope}
\begin{scope}[xshift=0.25in, yshift=-10.51in]
\VAR{month11}
\end{scope}
\begin{scope}[xshift=4.375in, yshift=-10.51in]
\VAR{month12}
\end{scope}
\end{tikzpicture}
\end{document}
''')
MONTH_TEMPLATE = LATEX_JINJA_ENV.from_string(r'''
\draw[thick, rounded corners=10pt]
(0,0) rectangle +(\FullMonthWidth, \FullMonthHeight);
% vertical grid lines
\foreach \x in {1,...,6}{%
\draw
(\MondayExtraWidth + \x * \DayWidth, \Margin + \BLOCK{ if num_weeks == 4 }1\BLOCK{ else }0\BLOCK{ endif } * \DayHeight)
-- +(0, \VAR{num_weeks} * \DayHeight);
}
% horizontal grid lines
\draw[thick] (0, \Margin + \BLOCK{ if num_weeks == 4 }1\BLOCK{ else }0\BLOCK{ endif } * \DayHeight) -- +(\FullMonthWidth, 0);
\foreach \y in {\BLOCK{ if num_weeks == 4 }2\BLOCK{ else } 1 \BLOCK{ endif },...,4}{%
\draw (0, \Margin + \y * \DayHeight) -- +(\FullMonthWidth, 0);
}
\foreach \row/\week in {%
\VAR{row_week_tuple}
} {%
\node[anchor=south west]
at (0, \Margin + 5 * \DayHeight - \row * \DayHeight) {\small w.\week};
}
\foreach \row/\col/\day in {%
\VAR{row_col_day_tuple}
}{%
\node[anchor=base] at ($
(\MondayExtraWidth + \col * \DayWidth - \DayWidth/2,
\Margin + 5 * \DayHeight - \row * \DayHeight) +
(0, 4pt) $){\day};
}
\draw[thick] (0, \Margin + 5 * 0.8) -- +(3.875, 0);
\node[anchor=base] at ($
(\MondayExtraWidth + 0*\DayWidth + \DayWidth/2,
\Margin + 5 * \DayHeight) + (0, 4pt) $){Monday};
\node[anchor=base] at ($
(\MondayExtraWidth + 1*\DayWidth + \DayWidth/2,
\Margin + 5 * \DayHeight) + (0, 4pt) $){Tuesday};
\node[anchor=base] at ($
(\MondayExtraWidth + 2*\DayWidth + \DayWidth/2,
\Margin + 5 * \DayHeight) + (0, 4pt) $){Wednesday};
\node[anchor=base] at ($
(\MondayExtraWidth + 3*\DayWidth + \DayWidth/2,
\Margin + 5 * \DayHeight) + (0, 4pt) $){Thursday};
\node[anchor=base] at ($
(\MondayExtraWidth + 4*\DayWidth + \DayWidth/2,
\Margin + 5 * \DayHeight) + (0, 4pt) $){Friday};
\node[anchor=base] at ($
(\MondayExtraWidth + 5*\DayWidth + \DayWidth/2,
\Margin + 5 * \DayHeight) + (0, 4pt) $){Saturday};
\node[anchor=base] at ($
(\MondayExtraWidth + 6*\DayWidth + \DayWidth/2,
\Margin + 5 * \DayHeight) + (0, 4pt) $){Sunday};
\node[right] at (\Margin, \FullMonthHeight-0.225)
{{\rmfamily \large \VAR{month|upper}}};
\node[left] at (\FullMonthWidth - \Margin, \FullMonthHeight-0.225)
{{\rmfamily \large \VAR{year}}};
''')
@click.command()
@click.help_option('--help', '-h')
@click.argument('year')
@click.argument('outfile')
def main(year, outfile):
"""Generate a 3-page yearly calendar.
The OUTFILE must either have a tex or a pdf extension. If tex, the
resulting file should be compiled with lualatex: `lualatex OUTFILE`. It
requires Rotis fonts to be installed on your system. If pdf, the
compilation with lualatex will be done in the background and the resulting
pdf will be stored in OUTFILE.
"""
months = {
"month%02d" % month: MONTH_TEMPLATE.render(dict(
month=calendar.month_name[month], year=year,
num_weeks=get_num_weeks(year, month),
row_week_tuple=get_row_week(year, month),
row_col_day_tuple=get_row_col_day(year, month)))
for month in range(1, 13)
}
if outfile.endswith('.tex'):
with open(outfile, "w") as out_fh:
out_fh.write(MAIN_TEMPLATE.render(**months))
elif outfile.endswith('.pdf'):
try:
dir = tempfile.mkdtemp()
with open(os.path.join(dir, 'calendar.tex'), "w") as out_fh:
out_fh.write(MAIN_TEMPLATE.render(**months))
cmd = ['lualatex', '--halt-on-error', '--interaction=nonstopmode',
'calendar.tex']
proc = subprocess.run(cmd, cwd=dir, stdout=subprocess.PIPE)
if proc.returncode != 0:
click.echo(proc.stdout)
proc.check_returncode() # raises SubprocessError
shutil.copy(os.path.join(dir, 'calendar.pdf'), outfile)
except (OSError, subprocess.SubprocessError) as exc_info:
click.echo(str(exc_info))
sys.exit(1)
finally:
shutil.rmtree(dir, ignore_errors=True)
else:
click.echo("OUTFILE must have either a .pdf or a .tex extension")
sys.exit(1)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment