#!/usr/bin/env python """ Build a 4-up PDF of specific pages of a source PDF. PAGE RANGES are like Python slices. {page_range_help} EXAMPLES conv-sheet.py -o output.pdf convocation-prime-sheets.pdf 5 13 23 32 Build output.pdf containing a single 4-up page of pages 5, 13, 23, and 32 of convocation-prime-sheets.pdf DEPENDENCIES The script requires PyPDF2 (http://mstamy2.github.io/PyPDF2/) and the libraries it uses, all of which can be installed something like this (may need admin privileges): Linux: pip install PyPDF2 macOS: pip3 install PyPDF2 (note: on macos, launch this script with python3 instead of python) Windows: cd {your downloads}\pyPDF2 C:\python33\python.exe setup.py install """ # By Lester Ward. Alter or redistribute at will. # Current version: https://rpg.divnull.com/pub/convocation/conv-sheet.py # Built to support the role-playing game Convocation Prime # https://divnull.com/blog/products/convocation/ from __future__ import print_function from sys import stderr, stdout, exit import os import traceback import argparse from papersize import parse_papersize from collections import defaultdict from PyPDF2.pagerange import PAGE_RANGE_HELP from PyPDF2 import PdfFileReader, PdfFileWriter, parse_filename_page_ranges MARGINX=5 SPACING=3 pagenames = [ { "setname": "eidola", "startpage" : 5, "pages" : ( "individual","squad","swarm","acid", "air","earth","electricity","fire","ice","metal","water","wood", "beast","bug","construct","dragon","elemental","fae","fungus","light","magic","ooze","psychic","shadow","undead", "achiever","lesser","companion","corrupter","defender","flyer","healer","striker","uniter" ) }, { "setname": "wild", "startpage" : 40, "pages" : ( "behemoth","chimera","nullifier","ravager","scourge","trickster" ) }, { "setname": "pc", "startpage" : 47, "pages" : ( "larkspur","nihilund","pax", "anger","belonging","bloodlust","compassion","command","connoisseur","conviction","cunning","doubt","fear", "harmony","glory","love","optimism","self-loathing","succor","style","temperance","valor","vengeance","passion", "ambassador","bridge","chameleon","climber","cryophile","darksighted","debunker","filter","insomniac", "liedetector","medium","occultist","orienteer","pyrophile","seer","sneak","sniffer","trembler","tongue", "ultravore","waterbreather","woodworker","gift", "aggressive","artistic","attuned","empowering","generalist","inspiring","occult","privileged","reassuring", "specialized","thrifty","style" ) }, { "setname": "adversity", "startpage" : 107, "pages" : ( "peon","mook","average","tough","fast","sturdy","alpha","boss","assault","dreadnought","stats", "blighter","bolsterer","blaster","champion","entropic","mender","queen" ) }, ] class Builder(object): def __init__(self, writer, width, height, strict=True): self.writer = writer self.width = width self.height = height self.pos_on_page = 0 self.readers = dict() self.currentReader = None self.currentPage = None self.coords = None def handle_file(self, filename, page_range): if filename not in self.readers: self.readers[filename] = PdfFileReader(open(filename, "rb")) self.currentReader = self.readers[filename] pages = page_range.indices(self.currentReader.getNumPages()) for page in range(*pages): if (self.pos_on_page == 0): self.currentPage = self.writer.addBlankPage(width, height) src = self.currentReader.getPage(page - 1) # pages are zero indexed (tx,ty) = self.offset(src) self.currentPage.mergeScaledTranslatedPage(src, self.scale, tx, ty) self.advancePosition() def offset(self,src): if (self.coords == None): destHalfH = self.height/2 destHalfW = self.width/2 quarterPageW = destHalfW - MARGINX - SPACING srcBox = src.mediaBox srcH = srcBox.getHeight() srcW = srcBox.getWidth() self.scale = quarterPageW / srcW quarterPageH = srcH * self.scale bottomY = destHalfH - quarterPageH + SPACING topY = destHalfH - SPACING leftX = MARGINX rightX = SPACING + destHalfW self.coords = [(leftX,topY), (rightX,topY), (leftX,bottomY), (rightX,bottomY)] pos = self.pos_on_page return self.coords[pos] def advancePosition(self): pos = self.pos_on_page + 1 if (pos > 3): pos = 0 self.pos_on_page = pos def parse_args(): parser = argparse.ArgumentParser( description=__doc__.format(page_range_help=PAGE_RANGE_HELP), formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("-o", "--output", metavar="output_file") parser.add_argument("-p", "--paper", metavar="paper", default="letter", help="paper size") parser.add_argument("-v", "--verbose", action="store_true", help="show page ranges as they are being read") parser.add_argument("input", nargs=1, metavar="input [page range...]") # argparse chokes on page ranges like "-2:" unless caught like this: parser.add_argument("fn_pgrgs", nargs=argparse.REMAINDER, metavar="filenames and/or page ranges") args = parser.parse_args() # Build an mapping of page name to page number page_map = dict() for pageset in pagenames: pagenum = pageset["startpage"]; for name in pageset["pages"]: page_map[name] = pagenum; pagenum += 1 # add the name of the first input file to the front of the list # to be returned translated = list() translated.append(args.input[0]) # Translate any page names used in the arguments to numbers for arg in args.fn_pgrgs: try: pagenum = page_map[arg] translated.append(str(pagenum)) except: # arg not in the map, so just use it untranslated translated.append(arg) args.fn_pgrgs[0:] = translated #print(args, file=stderr) return args if __name__ == "__main__": args = parse_args() filename_page_ranges = parse_filename_page_ranges(args.fn_pgrgs) if args.output: output = open(args.output, "wb") else: stdout.flush() output = os.fdopen(stdout.fileno(), "wb") (width, height) = parse_papersize(args.paper) writer = PdfFileWriter() filename = None try: builder = Builder(writer, width, height) for (filename, page_range) in filename_page_ranges: if args.verbose: print(filename, page_range, file=stderr) builder.handle_file(filename, page_range) except: print(traceback.format_exc(), file=stderr) print("Error while reading " + filename, file=stderr) exit(1) if args.verbose: print("Writing to " + args.output, file=stderr) writer.write(output) # In 3.0, input files must stay open until output is written. # Not closing the in_fs because this script exits now.