odoo/bin/reportlab/tools/py2pdf/py2pdf.py

1567 lines
45 KiB
Python

#!/usr/bin/env python
""" Python Highlighter for PDF Version: 0.6
py2pdf.py [options] <file1> [<file2> ...]
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=<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)