Automating Letter Creation
tex python osx writing

Generating a Letter in TeX

Last week I needed to send about thirty letters through the United States Postal Service. I had a small selection of stationary in the house, but it was either too casual or too bland for my purposes. I remembered that TeX had a letter document class, so I started tinkering with the formatting to see if I could produce a passable letter style that satisfied my needs.

The basic letter class style is passable, but I decided to build a more aesthetically pleasing template that I could use in the future. The letter style I wanted to use was an asymmetrical two column layout with the recipients address on the left and the body of the letter on the right. I’ve seen letters structured like this in the past and I really like the look of the vertical demarcation that the column divider provides.

Creating a letter with two asymmetrical columns turned out to be a bit tricker than I had imagined. Originally, I thought that the multicolumn package would probably have an option to define individual column widths. Unable to find any way to make asymmetrical columns with multicolumn, I next turned to the minipage environment. I was able to produce a passable letter style with minipage, but it felt extremely hacky and I was fairly certain that there was a more elegant way to generate asymmetrical columns with TeX.

After consulting the TeX Stack Exchange, the surprising canonical solution to generate asymmetrical columns was to use the lowly table environment. Obviously, the table environment is not intended to handle prose particularly well. One issue I had was how to issue line breaks. The conventional line break command (\\) inserts a new table row rather than a line break inside the table environment (see here for details). The best solution I found was to switch the cell layout to paragraphs within the table, which then allowed me to use the \newline command. The last tricky bit was top aligning the recipient’s address with the open body of the letter with this command:

newcommand{breakcell}[2][l]{begin{tabular}[t]{@{}#1@{}}#2\end{tabular}}

The resulting letter looked like this:

After I was satisfied with the letter format, I soon realized that it wasn’t practical to create thirty letters by manually changing each address, compiling each PDF, and printing each letter. To automate this process I broke the task into three stages and I created a single Python script as the glue to coordinate the process. The three stages of the process were:

  1. Create a letter template
  2. Generate PDFs of each letter for each individual recipient
  3. Combine letter PDFs into one document for easy printing

Letter Template

I first created a letter template using Jinja2. Jinja2 is an awesome template engine that I use all the time for various TeX and HTML projects. It was fairly straight forward to use Jinja2 to generate a letter template. The only tricky bit is changing Jinja2’s default delimiters so they don’t clash with TeX metacharacters (see code below). A Jinja2 template can be any text file with some specialized syntax. My letter template creates a specific salutation and adds each recipient’s address for each letter. The basic Jinja2 syntax looks like this inside the letter template:

\today & Dear %{{ address.Name %}}, \\
& \\
\breakcell{%{{ address.Name %}} \\ %{{ address.A1 %}} \\ %{{ address.A2 %}}} &

PDF Generation

I use the XeLaTeX typesetting compiler with TeX primarily so I can use system fonts. For most projects, I use the Fontin font from the exljbris Foundry, but for this letter, I chose Calluna. To automate PDF creation for each letter, I used Python to call the XeLaTeX engine from within my script to automatically compile each letter into a PDF.

PDF Concatenation For Printing

After the PDF files of each letter were created I wanted to automatically merge all the PDFs together so that all the letters could be printed at one time by only issuing a single print command. OS X 10.4 onward ships with a Python script that merges PDFs. The script is used by Automator and is called join.py. From the command line, join.py can be used to merge all the PDFs in a directory into a single PDF (merged.pdf) using this syntax:

$ /System/Library/Automator/Combine\ PDF\ Pages.action/Contents/Resources/join.py -o merged.pdf *.pdf

For my purposes, I called join.py from within my Python script. The final Python script is shown below. It reads in a text file called addresses.txt where each person’s address is a colon-delimited line such as:

Mr.Smith:13 Elm St.:Seattle, WA 98109

The entire letter creation process was fairly straight-forward and saved me a ton of time. Here my complete script for creating individual TeX letters, compiling them to PDF, and then merging the PDFs together:

#!/usr/bin/env python
#encoding=utf-8
"""
Seth Brown
Python 3.2
"""
import os, sys, csv
from glob import iglob
from subprocess import Popen, PIPE
import jinja2

def wrapper(*args):
    """CLI wrapper"""
    opts = [i for i in args]
    cmd = [] + opts
    process = Popen(cmd, stdout=PIPE)
    process.communicate()[0]
    return process

if __name__ == '__main__':
    # change the default delimiters used by Jinja
    # (prevent JinJa from interferring with LaTeX macros)
    letter_renderer = jinja2.Environment(
      block_start_string = '%{',
      block_end_string = '%}',
      variable_start_string = '%{{',
      variable_end_string = '%}}',
      loader = jinja2.FileSystemLoader(os.path.abspath('.')))

    template = letter_renderer.get_template('letter_template.tex')

    with open('addresses.txt') as infile:
        reader = csv.DictReader(infile, delimiter=":")
        os.mkdir("_letter_output")
        for n, address in enumerate(reader):
            output_dir = "_letter_output/"
            outfile =''.join((sys.argv[1], "-", str(n) + ".tex"))
            path = os.path.join(output_dir, outfile)
            with open(path, mode="w") as letter:
                letter.write(template.render(address = address))

    os.chdir(output_dir)
    [wrapper('/usr/texbin/xelatex', f) for f in iglob("*.tex")]
    pdf_util = '/System/Library/Automator/Combine PDF Pages.action/Contents/Resources/join.py'
    pdf_files = [p for p in iglob("*.pdf")]
    wrapper(pdf_util, '-o', 'merged_letters.pdf', *pdf_files)