#!/usr/bin/env python """ Python Highlighter for PDF Version: 0.6 py2pdf.py [options] [ ...] options: -h or --help print help (this message) - read from stdin (writes to stdout) --stdout read from file, write to stdout (restricted to first file only) --title= specify title --config=<file> read configuration options from <file> --input=<type> set input file type 'python': Python code (default) 'ascii': arbitrary ASCII files (b/w) --mode=<mode> set output mode 'color': output in color (default) 'mono': output in b/w --paperFormat= set paper format (ISO A, B, C series, <format> US legal & letter; default: 'A4') e.g. 'letter', 'A3', 'A4', 'B5', 'C6', ... --paperSize= set paper size in points (size being a valid <size> numeric 2-tuple (x,y) w/o any whitespace) --landscape set landscape format (default: portrait) --bgCol=<col> set page background-color in hex code like '#FFA024' or '0xFFA024' for RGB components (overwrites mono mode) --<cat>Col=<col> set color of certain code categories, i.e. <cat> can be the following: 'comm': comments 'ident': identifiers 'kw': keywords 'strng': strings 'param': parameters 'rest': all the rest --fontName=<name> set base font name (default: 'Courier') like 'Helvetica', 'Times-Roman' --fontSize=<size> set font size (default: 8) --tabSize=<size> set tab size (default: 4) --lineNum print line numbers --multiPage generate one file per page (with filenames tagged by 1, 2...), disables PDF outline --noOutline don't generate PDF outline (default: unset) -v or --verbose set verbose mode Takes the input, assuming it is Python source code and formats it into PDF. * Uses Just van Rossum's PyFontify version 0.4 to tag Python scripts, now included in reportlab.lib. * Uses the ReportLab library (version 0.92 or higher) to generate PDF. You can get it without charge from ReportLab: http://www.reportlab.com * Parts of this code still borrow heavily from Marc-Andre Lemburg's py2html who has kindly given permission to include them in py2pdf. Thanks, M.-A.! """ __copyright__ = """ ---------------------------------------------------------------------- (c) Copyright by Dinu C. Gherman, 2000 (gherman@europemail.com) Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee or royalty is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation or portions thereof, including modifications, that you make. THE AUTHOR DINU C. GHERMAN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE! """ __version__ = '0.6' __author__ = 'Dinu C. Gherman' __date__ = '2001-08-17' __url__ = 'http://starship.python.net/crew/gherman/programs/py2pdf' import sys, string, re, os, getopt from reportlab.pdfgen import canvas from reportlab.lib.colors import Color, HexColor from reportlab.lib import fonts from reportlab.lib.units import cm, inch ### Helpers functions. def makeTuple(aString): """Evaluate a string securely into a tuple. Match the string and return an evaluated object thereof if and only if we think it is a tuple of two or more numeric decimal(!) values, integers or floats. E-notation, hex or octal is not supported, though! Shorthand notation (omitting leading or trail- zeros, before or after the decimal point) like .25 or 25. is supported. """ c = string.count(aString, ',') num = '(\d*?\.?\d+?)|(\d+?\.?\d*?)' tup = string.join([num]*c, ',') if re.match('\(' + tup + '\)', aString): return eval(aString) else: details = '%s cannot be parsed into a numeric tuple!' % aString raise 'ValueError', details def loadFontifier(options=None): "Load a tagging module and return a corresponding function." # We can't use 'if options' because of the modified # attribute lookup it seems. if type(options) != type(None) and options.marcs: # Use mxTextTool's tagging engine. from mxTextTools import tag from mxTextTools.Examples.Python import python_script tagFunc = lambda text, tag=tag, pytable=python_script: \ tag(text,pytable)[1] else: # Load Just's. try: from reportlab.lib import PyFontify if PyFontify.__version__ < '0.3': raise ValueError tagFunc = PyFontify.fontify except: print """ Sorry, but this script needs the PyFontify.py module version 0.3 or higher; You can download it from Just's homepage at URL: http://starship.python.net/crew/just """ sys.exit() return tagFunc def makeColorFromString(aString): """Convert a string to a ReportLab RGB color object. Supported formats are: '0xFFFFFF', '#FFFFFF' and '(R,G,B)' where the latter is a tuple of three floats in the range [0.0, 1.0]. """ s = aString if s[0] == '#' or s[:2] in ('0x', '0X'): if s[:2] in ('0x', '0X'): return HexColor('#' + s[2:]) elif s[0] == '(': r, g, b = makeTuple(aString) return Color(r, g, b) ### Utility classes. # For py2pdf this is some kind of overkill, but it's fun, too. class PaperFormat: """Class to capture a paper format/size and its orientation. This class represents an abstract paper format, including the size and orientation of a sheet of paper in some formats plus a few operations to change its size and orientation. """ _A4W, _A4H = 21*cm, 29.7*cm A0 = (4*_A4W, 4*_A4H) _B4W, _B4H = 25*cm, 35.3*cm B0 = (4*_B4W, 4*_B4H) _C4W, _C4H = 22.9*cm, 32.4*cm C0 = (4*_C4W, 4*_C4H) letter = (8.5*inch, 11*inch) legal = (8.5*inch, 14*inch) def __init__(self, nameOrSize='A4', landscape=0): "Initialisation." t = type(nameOrSize) if t == type(''): self.setFormatName(nameOrSize, landscape) elif t == type((1,)): self.setSize(nameOrSize) self.setLandscape(landscape) def __repr__(self): """Return a string representation of ourself. The returned string can also be used to recreate the same PaperFormat object again. """ if self.name != 'custom': nos = `self.name` else: nos = `self.size` format = "PaperFormat(nameOrSize=%s, landscape=%d)" tuple = (nos, self.landscape) return format % tuple def setSize(self, size=None): "Set explicit paper size." self.name = 'custom' x, y = self.size = size self.landscape = x < y def setLandscape(self, flag): "Set paper orientation as desired." # Swap paper orientation if needed. self.landscape = flag x, y = self.size ls = self.landscape if (ls and x < y) or (not ls and x > y): self.size = y, x def setFormatName(self, name='A4', landscape=0): "Set paper size derived from a format name." if name[0] in 'ABC': # Assume ISO-A, -B, -C series. # (Hmm, are B and C really ISO standards # or just DIN? Well...) c, f = name[0], int(name[1:]) self.size = getattr(self, c + '0') self.name = c + '0' self.makeHalfSize(f) elif name == 'letter': self.size = self.letter self.name = 'letter' elif name == 'legal': self.size = self.legal self.name = 'legal' self.setLandscape(landscape) def makeHalfSize(self, times=1): "Reduce paper size (surface) by 50% multiple times." # Orientation remains unchanged. # Iterates only for times >= 1. for i in xrange(times): s = self.size self.size = s[1] / 2.0, s[0] if self.name[0] in 'ABC': self.name = self.name[0] + `int(self.name[1:]) + 1` else: self.name = 'custom' def makeDoubleSize(self, times=1): "Increase paper size (surface) by 50% multiple times." # Orientation remains unchanged. # Iterates only for times >= 1. for i in xrange(times): s = self.size self.size = s[1], s[0] * 2.0 if self.name[0] in 'ABC': self.name = self.name[0] + `int(self.name[1:]) - 1` else: self.name = 'custom' class Options: """Container class for options from command line and config files. This class is a container for options as they are specified when a program is called from a command line, but also from the same kind of options that are saved in configuration files. Both the short UNIX style (e.g. '-v') as well as the extended GNU style (e.g. '--help') are supported. An 'option' is a <name>/<value> pair with both parts being strings at the beginning, but where <value> might be conver- ted later into any other Python object. Option values can be accessed by using their name as an attri- bute of an Options object, returning None if no such option name exists (that makes None values impossible, but so what?). """ # Hmm, could also use UserDict... maybe later. def __init__(self): "Initialize with some default options name/values." # Hmm, could also pass an initial optional dict... # Create a dictionary containing the options # and populate it with defaults. self.pool = {} self.setDefaults() def __getattr__(self, name): "Turn attribute access into dictionary lookup." return self.pool.get(name) def setDefaults(self): "Set default options." ### Maybe get these from a site config file... self.pool.update({'fontName' : 'Courier', 'fontSize' : 8, 'bgCol' : Color(1, 1, 1), 'mode' : 'color', 'lineNum' : 0, 'tabSize' : 4, 'paperFormat': 'A4', 'landscape' : 0, 'title' : None, 'multiPage' : 0}) # Default colors (for color mode), mostly taken from py2html. self.pool.update({'commCol' : HexColor('#1111CC'), 'kwCol' : HexColor('#3333CC'), 'identCol' : HexColor('#CC0000'), 'paramCol' : HexColor('#000066'), 'strngCol' : HexColor('#119911'), 'restCol' : HexColor('#000000')}) # Add a default 'real' paper format object. pf = PaperFormat(self.paperFormat, self.landscape) self.pool.update({'realPaperFormat' : pf}) self.pool.update({'files' : []}) def display(self): "Display all current option names and values." self.saveToFile(sys.stdout) def saveToFile(self, path): "Save options as a log file." if type(path) == type(''): f = open(path, 'w') else: # Assume a file-like object. f = path items = self.pool.items() items.sort() for n, v in items: f.write("%-15s : %s\n" % (n, `v`)) def updateWithContentsOfFile(self, path): """Update options as specified in a config file. The file is expected to contain one option name/value pair (seperated by an equal sign) per line, but no other whitespace. Option values may contain equal signs, but no whitespace. Option names must be valid Python strings, preceeded by one or two dashes. """ config = open(path).read() config = string.split(config, '\n') for cfg in config: if cfg == None: break if cfg == '' or cfg[0] == '#': continue # GNU long options if '=' in cfg and cfg[:2] == '--': p = string.find(cfg, '=') opt, arg = cfg[2:p], cfg[p+1:] self.updateOption(opt, arg) # Maybe treat single-letter options as well? # elif ':' in cfg and cfg[0] == '-' and cfg[1] != '-': # pass else: self.updateOption(cfg[2:], None) def updateWithContentsOfArgv(self, argv): "Update options as specified in a (command line) argument vector." # Specify accepted short option names (UNIX style). shortOpts = 'hv' # Specify accepted long option names (GNU style). lo = 'tabSize= paperFormat= paperSize= landscape stdout title= fontName= fontSize=' lo = lo + ' bgCol= verbose lineNum marcs help multiPage noOutline config= input= mode=' lo = lo + ' commCol= identCol= kwCol= strngCol= paramCol= restCol=' longOpts = string.split(lo, ' ') try: optList, args = getopt.getopt(argv, shortOpts, longOpts) except getopt.error, msg: sys.stderr.write("%s\nuse -h or --help for help\n" % str(msg)) sys.exit(2) self.updateOption('files', args) # Hmm, really needed? for o, v in optList: # Remove leading dashes (max. two). if o[0] == '-': o = o[1:] if o[0] == '-': o = o[1:] self.updateOption(o, v) def updateOption(self, name, value): "Update an option from a string value." # Special treatment for coloring options... if name[-3:] == 'Col': if name[:-3] in string.split('bg comm ident kw strng param rest', ' '): self.pool[name] = makeColorFromString(value) elif name == 'paperSize': tup = makeTuple(value) self.pool['paperSize'] = tup if not self.realPaperFormat: pf = PaperFormat(self.paperFormat, self.landscape) self.pool['realPaperFormat'] = pf self.pool['realPaperFormat'].setSize(tup) elif name == 'paperFormat': self.pool['paperFormat'] = value if not self.realPaperFormat: pf = PaperFormat(self.paperFormat, self.landscape) self.pool['realPaperFormat'] = pf self.pool['realPaperFormat'].setFormatName(self.paperFormat, self.landscape) elif name == 'landscape': self.pool['landscape'] = 1 if self.realPaperFormat: self.pool['realPaperFormat'].setLandscape(1) elif name == 'fontSize': self.pool['fontSize'] = int(value) elif name == 'tabSize': self.pool['tabSize'] = int(value) elif name == 'mode': self.pool['mode'] = value if value == 'mono': cats = 'comm ident kw strng param rest' for cat in string.split(cats, ' '): self.pool[cat + 'Col'] = Color(0, 0, 0) # Parse configuration file... elif name == 'config': self.updateWithContentsOfFile(value) elif name == 'stdout': self.pool['stdout'] = 1 elif name == 'files': self.pool['files'] = value else: # Set the value found or 1 for options without values. self.pool[name] = value or 1 def update(self, **options): "Update options." # Not much tested and/or used, yet!! for n, v in options.items(): self.pool[n] = v ### Layouting classes. class PDFLayouter: """A class to layout a simple PDF document. This is intended to help generate PDF documents where all pages follow the same kind of 'template' which is supposed to be the same adornments (header, footer, etc.) on each page plus a main 'frame' on each page. These frames are 'connected' such that one can add individual text lines one by one with automatic line wrapping, page breaks and text flow between frames. """ def __init__(self, options): "Initialisation." self.options = options self.canvas = None self.multiLineStringStarted = 0 self.lineNum = 0 # Set a default color and font. o = self.options self.currColor = o.restCol self.currFont = (o.fontName, o.fontSize) ### Helper methods. def setMainFrame(self, frame=None): "Define the main drawing frame of interest for each page." if frame: self.frame = frame else: # Maybe a long-term candidate for additional options... width, height = self.options.realPaperFormat.size self.frame = height - 3*cm, 3*cm, 2*cm, width - 2*cm # self.frame is a 4-tuple: # (topMargin, bottomMargin, leftMargin, rightMargin) def setPDFMetaInfo(self): "Set PDF meta information." o = self.options c = self.canvas c.setAuthor('py2pdf %s' % __version__) c.setSubject('') # Set filename. filename = '' # For stdin use title option or empty... if self.srcPath == sys.stdin: if o.title: filename = o.title # otherwise take the input file's name. else: path = os.path.basename(self.srcPath) filename = o.title or path c.setTitle(filename) def setFillColorAndFont(self, color, font): "Set new color/font (maintaining the current 'state')." self.currFont = font self.currColor = color fontName, fontSize = font self.text.setFont(fontName, fontSize) self.text.setFillColor(color) ### API def begin(self, srcPath, numLines): "Things to do before doing anything else." self.lineNum = 0 self.pageNum = 0 self.numLines = numLines self.srcPath = srcPath # Set output filename (stdout if desired). o = self.options if o.stdout: self.pdfPath = sys.stdout else: if srcPath != sys.stdin: self.pdfPath = os.path.splitext(srcPath)[0] + '.pdf' else: self.pdfPath = sys.stdout def beginDocument(self): """Things to do when a new document should be started. The initial page counter is 0, meaning that beginPage() will be called by beginLine()... """ # Set initial page number and store file name. self.pageNum = 0 o = self.options if not o.multiPage: # Create canvas. size = o.realPaperFormat.size self.canvas = canvas.Canvas(self.pdfPath, size, verbosity=0) c = self.canvas c.setPageCompression(1) c.setFont(o.fontName, o.fontSize) # Create document meta information. self.setPDFMetaInfo() # Set drawing frame. self.setMainFrame() # Determine the left text margin by adding the with # of the line number to the left margin of the main frame. format = "%%%dd " % len(`self.numLines`) fn, fs = self.currFont text = format % self.lineNum tm, bm, lm, rm = self.frame self.txm = lm if o.lineNum: self.txm = self.txm + c.stringWidth(text, fn, fs) def beginPage(self): "Things to do when a new page has to be added." o = self.options self.pageNum = self.pageNum + 1 if not o.multiPage: tm, bm, lm, rm = self.frame self.text = self.canvas.beginText(lm, tm - o.fontSize) self.setFillColorAndFont(self.currColor, self.currFont) else: # Fail if stdout desired (with multiPage). if o.stdout: raise "IOError", "Can't create multiple pages on stdout!" # Create canvas with a modified path name. base, ext = os.path.splitext(self.pdfPath) newPath = "%s-%d%s" % (base, self.pageNum, ext) size = o.realPaperFormat.size self.canvas = canvas.Canvas(newPath, size, verbosity=0) c = self.canvas c.setPageCompression(1) c.setFont(o.fontName, o.fontSize) # Create document meta information. self.setPDFMetaInfo() # Set drawing frame. self.setMainFrame() tm, bm, lm, rm = self.frame self.text = self.canvas.beginText(lm, tm - o.fontSize) self.setFillColorAndFont(self.currColor, self.currFont) self.putPageDecoration() def beginLine(self, wrapped=0): "Things to do when a new line has to be added." # If there is no page yet, create the first one. if self.pageNum == 0: self.beginPage() # If bottom of current page reached, do a page break. # (This works only with one text object being used # for the entire page. Otherwise we need to maintain # the vertical position of the current line ourself.) y = self.text.getY() tm, bm, lm, rm = self.frame if y < bm: self.endPage() self.beginPage() # Print line number label, if needed. o = self.options if o.lineNum: #self.putLineNumLabel() font = ('Courier', o.fontSize) if not wrapped: # Print a label containing the line number. self.setFillColorAndFont(o.restCol, font) format = "%%%dd " % len(`self.numLines`) self.text.textOut(format % self.lineNum) else: # Print an empty label (using bgCol). Hackish! currCol = self.currColor currFont = self.currFont self.setFillColorAndFont(o.bgCol, font) self.text.textOut(' '*(len(`self.numLines`) + 1)) self.setFillColorAndFont(currCol, currFont) def endLine(self, wrapped=0): "Things to do after a line is basically done." # End the current line by adding an 'end of line'. # (Actually done by the text object...) self.text.textLine('') if not wrapped: self.lineNum = self.lineNum + 1 def endPage(self): "Things to do after a page is basically done." c = self.canvas # Draw the current text object (later we might want # to do that after each line...). c.drawText(self.text) c.showPage() if self.options.multiPage: c.save() def endDocument(self): "Things to do after the document is basically done." c = self.canvas # Display rest of last page and save it. c.drawText(self.text) c.showPage() c.save() def end(self): "Things to do after everything has been done." pass ### The real meat: methods writing something to a canvas. def putLineNumLabel(self, text, wrapped=0): "Add a long text that can't be split into chunks." o = self.options font = ('Courier', o.fontSize) if not wrapped: # Print a label containing the line number. self.setFillColorAndFont(o.restCol, font) format = "%%%dd " % len(`self.numLines`) self.text.textOut(format % self.lineNum) else: # Print an empty label (using bgCol). Hackish! currCol = self.currColor currFont = self.currFont self.setFillColorAndFont(o.bgCol, font) self.text.textOut(' '*(len(`self.numLines`) + 1)) self.setFillColorAndFont(currCol, currFont) # Tried this recursively before, in order to determine # an appropriate string limit rapidly, but this wasted # much space and showed very poor results... # This linear method is very slow, but such lines should # be very rare, too! def putSplitLongText(self, text): "Add a long text that can't be split into chunks." # Now, the splitting will be with 'no mercy', # at the right margin of the main drawing area. M = len(text) t = self.text x = t.getX() o = self.options tm, bm, lm, rm = self.frame width = self.canvas.stringWidth fn, fs = self.currFont tw = width(text, fn, fs) tx = self.text if tw > rm - lm - x: i = 1 T = '' while text: T = text[:i] tx = self.text # Can change after a page break. tw = width(T, fn, fs) if x + tw > rm: tx.textOut(T[:-1]) self.endLine(wrapped=1) self.beginLine(wrapped=1) x = tx.getX() text = text[i-1:] M = len(text) i = 0 i = i + 1 if i > M: break tx.textOut(T) else: t.textOut(text) def putLongText(self, text): "Add a long text by gracefully splitting it into chunks." # Splitting is currently done only at blanks, but other # characters such as '.' or braces are also good # possibilities... later... o = self.options tm, bm, lm, rm = self.frame width = self.canvas.stringWidth fn, fs = self.currFont tw = width(text, fn, fs) arr = string.split(text, ' ') for i in range(len(arr)): a = arr[i] t = self.text # Can change during the loop... tw = width(a, fn, fs) x = t.getX() # If current item does not fit on current line, have it # split and then put before/after a line (maybe also # page) break. if x + tw > rm: self.putSplitLongText(a) t = self.text # Can change after a page break... # If it fits, just add it to the current text object. else: t.textOut(a) # Add the character we used to split th original text. if i < len(arr) - 1: t.textOut(' ') def putText(self, text): "Add some text to the current line." t = self.text x = t.getX() o = self.options fn, fs = o.fontName, o.fontSize tw = self.canvas.stringWidth(text, fn, fs) rm = self.frame[3] if x + tw < rm: t.textOut(text) else: self.putLongText(text) # Not yet tested. def putLine(self, text): "Add a line to the current text." self.putText(text) self.endLine() def putPageDecoration(self): "Draw some decoration on each page." # Use some abbreviations. o = self.options c = self.canvas tm, bm, lm, rm = self.frame # Restore default font. c.setFont(o.fontName, o.fontSize) c.setLineWidth(0.5) # in pt. # Background color. c.setFillColor(o.bgCol) pf = o.realPaperFormat.size c.rect(0, 0, pf[0], pf[1], stroke=0, fill=1) # Header. c.setFillColorRGB(0, 0, 0) c.line(lm, tm + .5*cm, rm, tm + .5*cm) c.setFont('Times-Italic', 12) if self.pdfPath == sys.stdout: filename = o.title or ' ' else: path = os.path.basename(self.srcPath) filename = o.title or path c.drawString(lm, tm + 0.75*cm + 2, filename) # Footer. c.line(lm, bm - .5*cm, rm, bm - .5*cm) c.drawCentredString(0.5 * pf[0], 0.5*bm, "Page %d" % self.pageNum) # Box around main frame. # c.rect(lm, bm, rm - lm, tm - bm) class PythonPDFLayouter (PDFLayouter): """A class to layout a simple multi-page PDF document. """ ### API for adding specific Python entities. def addKw(self, t): "Add a keyword." o = self.options # Make base font bold. fam, b, i = fonts.ps2tt(o.fontName) ps = fonts.tt2ps(fam, 1, i) font = (ps, o.fontSize) self.setFillColorAndFont(o.kwCol, font) # Do bookmarking... if not o.noOutline and not o.multiPage: if t in ('class', 'def'): tm, bm, lm, rm = self.frame pos = self.text.getX() if pos == self.txm: self.startPositions = [] self.startPos = pos if not hasattr(self, 'startPositions'): self.startPositions = [] if pos not in self.startPositions: self.startPositions.append(pos) # Memorize certain keywords. self.itemFound = t else: self.itemFound = None self.startPos = None self.putText(t) def addIdent(self, t): "Add an identifier." o = self.options # Make base font bold. fam, b, i = fonts.ps2tt(o.fontName) ps = fonts.tt2ps(fam, 1, i) font = (ps, o.fontSize) self.setFillColorAndFont(o.identCol, font) self.putText(t) # Bookmark certain identifiers (class and function names). if not o.noOutline and not o.multiPage: item = self.itemFound if item: # Add line height to current vert. position. pos = self.text.getY() + o.fontSize nameTag = "p%sy%s" % (self.pageNum, pos) c = self.canvas i = self.startPositions.index(self.startPos) c.bookmarkHorizontalAbsolute(nameTag, pos) c.addOutlineEntry('%s %s' % (item, t), nameTag, i) def addParam(self, t): "Add a parameter." o = self.options font = (o.fontName, o.fontSize) self.setFillColorAndFont(o.paramCol, font) self.text.putText(t) def addSimpleString(self, t): "Add a simple string." o = self.options font = (o.fontName, o.fontSize) self.setFillColorAndFont(o.strngCol, font) self.putText(t) def addTripleStringBegin(self): "Memorize begin of a multi-line string." # Memorise that we started a multi-line string. self.multiLineStringStarted = 1 def addTripleStringEnd(self, t): "Add a multi-line string." self.putText(t) # Forget about the multi-line string again. self.multiLineStringStarted = 0 def addComm(self, t): "Add a comment." o = self.options # Make base font slanted. fam, b, i = fonts.ps2tt(o.fontName) ps = fonts.tt2ps(fam, b, 1) font = (ps, o.fontSize) self.setFillColorAndFont(o.commCol, font) self.putText(t) def addRest(self, line, eol): "Add a regular thing." o = self.options # Nothing else to be done, print line as-is... if line: font = (o.fontName, o.fontSize) self.setFillColorAndFont(o.restCol, font) # ... except if the multi-line-string flag is set, then we # decide to change the current color to that of strings and # just go on. if self.multiLineStringStarted: self.setFillColorAndFont(o.strngCol, font) self.putText(line) # Print an empty line. else: if eol != -1: self.putText('') ### End of API. class EmptyPythonPDFLayouter (PythonPDFLayouter): """A PDF layout with no decoration and no margins. The main frame extends fully to all paper edges. This is useful for creating PDFs when writing one page per file, in order to provide pre-rendered, embellished Python source code to magazine publishers, who can include the individual files and only need to add their own captures. """ def setMainFrame(self, frame=None): "Make a frame extending to all paper edges." width, height = self.options.realPaperFormat.size self.frame = height, 0, 0, width def putPageDecoration(self): "Draw no decoration at all." pass ### Pretty-printing classes. class PDFPrinter: """Generic PDF Printer class. Does not do much, but write a PDF file created from any ASCII input file. """ outFileExt = '.pdf' def __init__(self, options=None): "Initialisation." self.data = None # Contains the input file. self.inPath = None # Path of input file. self.Layouter = PDFLayouter if type(options) != type(None): self.options = options else: self.options = Options() ### I/O. def readFile(self, path): "Read the content of a file." if path == sys.stdin: f = path else: f = open(path) self.inPath = path data = f.read() o = self.options self.data = re.sub('\t', ' '*o.tabSize, data) f.close() def formatLine(self, line, eol=0): "Format one line of Python source code." font = ('Courier', 8) self.layouter.setFillColorAndFont(Color(0, 0, 0), font) self.layouter.putText(line) def writeData(self, srcCodeLines, inPath, outPath=None): "Convert Python source code lines into a PDF document." # Create a layouter object. self.layouter = self.Layouter(self.options) l = self.layouter # Loop over all tagged source lines, dissect them into # Python entities ourself and let the layouter do the # rendering. splitCodeLines = string.split(srcCodeLines, '\n') ### Must also handle the case of outPath being sys.stdout!! l.begin(inPath, len(splitCodeLines)) l.beginDocument() for line in splitCodeLines: l.beginLine() self.formatLine(line) l.endLine() l.endDocument() l.end() def writeFile(self, data, inPath=None, outPath=None): "Write some data into a file." if inPath == sys.stdin: self.outPath = sys.stdout else: if not outPath: path = os.path.splitext(self.inPath)[0] self.outPath = path + self.outFileExt self.writeData(data, inPath, outPath or self.outPath) def process(self, inPath, outPath=None): "The real 'action point' for working with Pretty-Printers." self.readFile(inPath) self.writeFile(self.data, inPath, outPath) class PythonPDFPrinter (PDFPrinter): """A class to nicely format tagged Python source code. """ comm = 'COMMENT' kw = 'KEYWORD' strng = 'STRING' ident = 'IDENT' param = 'PARAMETER' outFileExt = '.pdf' def __init__(self, options=None): "Initialisation, calling self._didInit() at the end." if type(options) != type(None): self.options = options else: self.options = Options() self._didInit() def _didInit(self): "Post-Initialising" # Define regular expression patterns. s = self comp = re.compile s.commPat = comp('(.*?)<' + s.comm + '>(.*?)</' + s.comm + '>(.*)') s.kwPat = comp('(.*?)<' + s.kw + '>(.*?)</' + s.kw + '>(.*)') s.identPat = comp('(.*?)<' + s.ident + '>(.*?)</' + s.ident + '>(.*)') s.paramPat = comp('(.*?)<' + s.param + '>(.*?)</' + s.param + '>(.*)') s.stPat = comp('(.*?)<' + s.strng + '>(.*?)</' + s.strng + '>(.*)') s.strng1Pat = comp('(.*?)<' + s.strng + '>(.*)') s.strng2Pat = comp('(.*?)</' + s.strng + '>(.*)') s.allPat = comp('(.*)') cMatch = s.commPat.match kMatch = s.kwPat.match iMatch = s.identPat.match pMatch = s.paramPat.match sMatch = s.stPat.match s1Match = s.strng1Pat.match s2Match = s.strng2Pat.match aMatch = s.allPat.match self.matchList = ((cMatch, 'Comm'), (kMatch, 'Kw'), (iMatch, 'Ident'), (pMatch, 'Param'), (sMatch, 'SimpleString'), (s1Match, 'TripleStringBegin'), (s2Match, 'TripleStringEnd'), (aMatch, 'Rest')) self.Layouter = PythonPDFLayouter # Load fontifier. self.tagFunc = loadFontifier(self.options) ### def formatLine(self, line, eol=0): "Format one line of Python source code." # Values for eol: -1:no-eol, 0:dunno-yet, 1:do-eol. for match, meth in self.matchList: res = match(line) if res: groups = res.groups() method = getattr(self, '_format%s' % meth) method(groups, eol) break def _formatIdent(self, groups, eol): "Format a Python identifier." before, id, after = groups self.formatLine(before, -1) self.layouter.addIdent(id) self.formatLine(after, eol) def _formatParam(self, groups, eol): "Format a Python parameter." before, param, after = groups self.formatLine(before, -1) self.layouter.addParam(before) self.formatLine(after, eol) def _formatSimpleString(self, groups, eol): "Format a Python one-line string." before, s, after = groups self.formatLine(before, -1) self.layouter.addSimpleString(s) self.formatLine(after, eol) def _formatTripleStringBegin(self, groups, eol): "Format a Python multi-line line string (1)." before, after = groups self.formatLine(before, -1) self.layouter.addTripleStringBegin() self.formatLine(after, 1) def _formatTripleStringEnd(self, groups, eol): "Format a Python multi-line line string (2)." before, after = groups self.layouter.addTripleStringEnd(before) self.formatLine(after, 1) def _formatKw(self, groups, eol): "Format a Python keyword." before, kw, after = groups self.formatLine(before, -1) self.layouter.addKw(kw) self.formatLine(after, eol) def _formatComm(self, groups, eol): "Format a Python comment." before, comment, after = groups self.formatLine(before, -1) self.layouter.addComm(comment) self.formatLine(after, 1) def _formatRest(self, groups, eol): "Format a piece of a Python line w/o anything special." line = groups[0] self.layouter.addRest(line, eol) ### def writeData(self, srcCodeLines, inPath, outPath=None): "Convert Python source code lines into a PDF document." # Create a layouter object. self.layouter = self.Layouter(self.options) l = self.layouter # Loop over all tagged source lines, dissect them into # Python entities ourself and let the layouter do the # rendering. splitCodeLines = string.split(srcCodeLines, '\n') l.begin(inPath, len(splitCodeLines)) l.beginDocument() for line in splitCodeLines: l.beginLine() self.formatLine(line) l.endLine() l.endDocument() l.end() def process(self, inPath, outPath=None): "The real 'action point' for working with Pretty-Printers." self.readFile(inPath) self.taggedData = self._fontify(self.data) self.writeFile(self.taggedData, inPath, outPath) ### Fontifying. def _fontify(self, pytext): "" formats = { 'rest' : ('', ''), 'comment' : ('<%s>' % self.comm, '</%s>' % self.comm), 'keyword' : ('<%s>' % self.kw, '</%s>' % self.kw), 'parameter' : ('<%s>' % self.param, '</%s>' % self.param), 'identifier' : ('<%s>' % self.ident, '</%s>' % self.ident), 'string' : ('<%s>' % self.strng, '</%s>' % self.strng) } # Parse. taglist = self.tagFunc(pytext) # Prepend special 'rest' tag. taglist[:0] = [('rest', 0, len(pytext), None)] # Prepare splitting. splits = [] self._addSplits(splits, pytext, formats, taglist) # Do splitting & inserting. splits.sort() l = [] li = 0 for ri, dummy, insert in splits: if ri > li: l.append(pytext[li:ri]) l.append(insert) li = ri if li < len(pytext): l.append(pytext[li:]) return string.join(l, '') def _addSplits(self, splits, text, formats, taglist): "" # Helper for fontify(). for id, left, right, sublist in taglist: try: pre, post = formats[id] except KeyError: # msg = 'Warning: no format ' # msg = msg + 'for %s specified\n'%repr(id) # sys.stderr.write(msg) pre, post = '', '' if type(pre) != type(''): pre = pre(text[left:right]) if type(post) != type(''): post = post(text[left:right]) # len(splits) is a dummy used to make sorting stable. splits.append((left, len(splits), pre)) if sublist: self._addSplits(splits, text, formats, sublist) splits.append((right, len(splits), post)) ### Main def main(cmdline): "Process command line as if it were sys.argv" # Create default options and initialize with argv # from the command line. options = Options() options.updateWithContentsOfArgv(cmdline[1:]) # Print help message if desired, then exit. if options.h or options.help: print __doc__ sys.exit() # Apply modest consistency checks and exit if needed. cmdStr = string.join(cmdline, ' ') find = string.find if find(cmdStr, 'paperSize') >= 0 and find(cmdStr, 'paperFormat') >= 0: details = "You can specify either paperSize or paperFormat, " details = detail + "but not both!" raise 'ValueError', details # Create PDF converter and pass options to it. if options.input: input = string.lower(options.input) if input == 'python': P = PythonPDFPrinter elif input == 'ascii': P = PDFPrinter else: details = "Input file type must be 'python' or 'ascii'." raise 'ValueError', details else: P = PythonPDFPrinter p = P(options) # Display options if needed. if options.v or options.verbose: pass # p.options.display() # Start working. verbose = options.v or options.verbose if options.stdout: if len(options.files) > 1 and verbose: print "Warning: will only convert first file on command line." f = options.files[0] p.process(f, sys.stdout) else: if verbose: print 'py2pdf: working on:' for f in options.files: try: if verbose: print ' %s' % f if f != '-': p.process(f) else: p.process(sys.stdin, sys.stdout) except IOError: if verbose: print '(IOError!)', if verbose: print print 'Done.' def process(*args, **kwargs): "Provides a way of using py2pdf from within other Python scripts." noValOpts = 'h v verbose landscape stdout lineNum marcs help multiPage noOutline' noValOpts = string.split(noValOpts, ' ') s = 'py2pdf.py' for k, v in kwargs.items(): if len(k) == 1: s = s + ' -%s' % k if k not in noValOpts: s = s + ' %s' % v elif len(k) > 1: s = s + ' --%s' % k if k not in noValOpts: s = s + '=%s' % v s = s + ' ' + string.join(args, ' ') s = string.split(s, ' ') main(s) ### if __name__=='__main__': #NORUNTESTS main(sys.argv)