Skip to content

Instantly share code, notes, and snippets.

@rfong
Last active May 7, 2023 09:26
Show Gist options
  • Save rfong/cece3a803cbed1c91e97c256734fd6e3 to your computer and use it in GitHub Desktop.
Save rfong/cece3a803cbed1c91e97c256734fd6e3 to your computer and use it in GitHub Desktop.
Use LaTeX to generate print layout for a mini-zine with double-sided pages, folded from a single sheet of paper

what is this

This is a pair of scripts to collate a small-page-size PDF into a final print layout for standard size paper, which can then be printed and assembled into a booklet.

The python script autogenerates intermediate LaTeX layout files to collate the print layout. The bash script runs the generator, and recompiles and schleps around the output files.

use case / why is that necessary

This year I've been getting into making mini-zines, which are folded from normal size sheets of printer paper. (See the attached image for a visual demonstration.)

I draft the mini-pages (either individually or typeset to PDF). The mini-pages must then be laid out into the final print format, so the printed sheet can be folded up into a booklet with minimal swearing.

To avoid mindless Illustrator/InDesign labor, I use multiple intermediate LaTeX files to compile them into the final print layout (with each intermediary simulating one paper-unfolding).

I got bored of tediously recompiling the multiple intermediate TeX files each time I revised my mini-page drafts, and forking them every time I created a new zine, so I made a quick script to automate it all in one go.

why zines?

I like zines because they're a nice way to infodump things out of my overstuffed brain, and because the low activation energy fosters a creative and diverse community that encourages marginalized and underprivileged demographics to publish. Stolen Sharpie Revolution is a nice resource on zine culture and events if you're curious.

I stash my zine work under the name Distractibility Cartographers.

#!/bin/bash
# Script to make a printable A4 or letter PDF from a PDF with a smaller page
# size which is meant to be folded from a double-sided sheet.
#
# It's easy to just edit and recompile the intermediate TeX files, but we
# all know I prefer macros to manual labor.
#
# Prereqs: pdflatex
#
# Example usage:
# `./mini-page-layout.sh zine-A7.pdf 3`
# `./mini-page-layout.sh -l zine-A7.pdf 3`
# Validation
if [[ -z "$1" ]]; then
echo "Argument 1 should be the path of an input PDF."
exit 1
fi
if [[ -z "$2" ]]; then
echo "Argument 2 should be the number of times an A4 sheet is folded."
exit 1
fi
while getopts "ltq" opt; do
case $opt in
l) # Input file is in landscape mode.
landscape=1
;;
t) # Test python file, but do not compile TeX
nocompile=1
;;
q) # Quiet output of TeX compilation
quiet=1
;;
#\?)
# echo "Invalid option: -$OPTARG" >&2
# exit 1
# ;;
esac
done
shift "$((OPTIND-1))" # Shift away opts
#echo "args: $@"
# Get file basename
basename=${1%.*}
# Remove old intermediate TeX files
exts=("tex" "aux" "log")
for ext in ${exts[@]}; do
[ -e "$basename-a*.$ext" ] && trash "$basename-a*.$ext"
done
# Generate new TeX files
if [ -z $landscape ]; then
python mini-page-layout.py $1 --folds=$2
else
python mini-page-layout.py $1 --folds=$2 --landscape
fi
if [ "$nocompile" ]; then
echo "Skip compilation"
exit
fi
# Compile TeX
for f in `ls $basename-*.tex | sort -r`; do
if [ "$quiet" ]; then
pdflatex $f >/dev/null
else
pdflatex $f
fi
done
open $basename-a4.pdf
#!/usr/bin/python -O
'''
Script to generate intermediate TeX to conglomerate a printable PDF from a
PDF with a smaller page-size, meant to be folded and cut from double-sided
A4 or letter paper.
(DISCLAIMER: I haven't actually unit tested this for anything besides
A7 & 1/8-letter yet)
Example usage: input PDF is A7 size in portrait mode
`python mini-page-layout.py input.pdf --folds=3`
Example usage: input PDF is 1/8-letter size in portrait mode
`python mini-page-layout.py input.pdf --folds=3 --letter`
Example usage: input PDF is A7 size in landscape mode
`python mini-page-layout.py input.pdf --folds=3 --landscape`
'''
from enum import Enum
import math
from optparse import OptionParser
import os
import pyPdf
MAX_FOLDS = 3
TEX_EXT = ".tex"
class PaperFormat(Enum):
A4 = 1
LETTER = 2
A4_FOLDS_TO_OUTPUT_FORMAT = {
# 1-indexed
1: "a4",
2: "a5",
3: "a6",
}
LETTER_FOLDS_TO_DIMENSIONS = {
# 1-indexed
# (width, height)
1: (11, 8.5),
2: (8.5, 5.5),
3: (5.5, 4.25),
}
def get_num_pdf_pages(fname):
reader = pyPdf.PdfFileReader(open(fname))
return reader.getNumPages()
def get_layout_tex(
from_fold_level, from_filename, num_pages, format=PaperFormat.A4, is_portrait=True):
'''
Return TeX text to conglomerate a smaller (folded) PDF into the next
size up, e.g. to assemble an A4 from an A5 PDF.
:param from_fold_level: number of A4 folds to reach input file paper size
:param from_filename: input filename
:num_pages: page signature of input file
:is_portrait: whether the INPUT file is in portrait mode
:returns: TeX layout to compile the input PDF to the next paper size up
'''
mode = "landscape," if not is_portrait else ""
if format == PaperFormat.A4:
docclass = A4_FOLDS_TO_OUTPUT_FORMAT[from_fold_level]+"paper,landscape"
return A4_LAYOUT_TEMPLATE % (docclass, mode, num_pages, from_filename)
else:
width, height = tuple([(str(x) + "in") for x in LETTER_FOLDS_TO_DIMENSIONS[from_fold_level]])
return LETTER_LAYOUT_TEMPLATE % (
width,
height,
mode,
num_pages,
from_filename)
# Parameterized TeX templates to assemble a PDF into the next paper size up
A4_LAYOUT_TEMPLATE="""\documentclass[%s]{article}
\usepackage[final]{pdfpages}
\usepackage[margin=0in,heightrounded]{geometry}
\\begin{document}
\includepdf[pages=-,nup=1x2,%ssignature=%d]{%s}
\end{document}
"""
LETTER_LAYOUT_TEMPLATE="""\documentclass[landscape]{article}
\usepackage[final]{pdfpages}
\usepackage[paperwidth=%s,paperheight=%s,margin=0in,heightrounded]{geometry}
\\begin{document}
\includepdf[pages=-,nup=1x2,%ssignature=%d]{%s}
\end{document}
"""
def main():
parser = OptionParser()
parser.add_option("--folds", dest="num_folds", default=1, type=int,
help="max folds: %d"%MAX_FOLDS)
parser.add_option("--landscape", dest="is_landscape",
default=False, action="store_true",
help="set if in landscape mode")
parser.add_option("--letter", dest="is_letter_format",
default=False, action="store_true",
help="set if using letter paper; default is A4")
(options,args) = parser.parse_args()
# Validation
if not args:
print "Expected filename of input PDF"
exit(0)
if not os.path.exists(args[0]):
print "Path %s does not exist" % args[0]
exit(0)
if options.num_folds not in range(1, MAX_FOLDS+1):
print "--folds should be an integer in the range [1,%d]" % MAX_FOLDS
exit(0)
options.paper_format = (
PaperFormat.LETTER if options.is_letter_format else PaperFormat.A4)
# For page signature, round up to nearest upward power of 2
num_pages = get_num_pdf_pages(args[0])
num_pages = int(math.pow(2, math.ceil(math.log(num_pages, 2))))
# Generate and write intermediate TeX files
path_base, path_ext = os.path.splitext(args[0])
out_path = args[0]
folds = options.num_folds
is_landscape = options.is_landscape
# Start from smallest fold and increase paper size
while folds > 0:
# Use most recent file as input to next larger paper size
in_path = os.path.splitext(out_path)[0] + ".pdf"
out_path = (
path_base + "-" + ("letter" if options.is_letter_format else "") +
A4_FOLDS_TO_OUTPUT_FORMAT[folds] + TEX_EXT
)
with open(out_path, "w") as f:
f.write(
get_layout_tex(
folds, in_path, num_pages, format=options.paper_format,
is_portrait=(not is_landscape)
))
# Set up for next loop
folds -= 1 # Decrement
num_pages /= 2
is_landscape = True # Always landscape for assemblies
if __name__=="__main__":
main()
#run_dir = os.getcwd()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment