3
""" Python Highlighter for PDF Version: 0.6
5
py2pdf.py [options] <file1> [<file2> ...]
8
-h or --help print help (this message)
9
- read from stdin (writes to stdout)
10
--stdout read from file, write to stdout
11
(restricted to first file only)
12
--title=<title> specify title
13
--config=<file> read configuration options from <file>
14
--input=<type> set input file type
15
'python': Python code (default)
16
'ascii': arbitrary ASCII files (b/w)
17
--mode=<mode> set output mode
18
'color': output in color (default)
20
--paperFormat= set paper format (ISO A, B, C series,
21
<format> US legal & letter; default: 'A4')
22
e.g. 'letter', 'A3', 'A4', 'B5', 'C6', ...
23
--paperSize= set paper size in points (size being a valid
24
<size> numeric 2-tuple (x,y) w/o any whitespace)
25
--landscape set landscape format (default: portrait)
26
--bgCol=<col> set page background-color in hex code like
27
'#FFA024' or '0xFFA024' for RGB components
28
(overwrites mono mode)
29
--<cat>Col=<col> set color of certain code categories, i.e.
30
<cat> can be the following:
37
--fontName=<name> set base font name (default: 'Courier')
38
like 'Helvetica', 'Times-Roman'
39
--fontSize=<size> set font size (default: 8)
40
--tabSize=<size> set tab size (default: 4)
41
--lineNum print line numbers
42
--multiPage generate one file per page (with filenames
43
tagged by 1, 2...), disables PDF outline
44
--noOutline don't generate PDF outline (default: unset)
45
-v or --verbose set verbose mode
47
Takes the input, assuming it is Python source code and formats
50
* Uses Just van Rossum's PyFontify version 0.4 to tag Python
51
scripts, now included in reportlab.lib.
53
* Uses the ReportLab library (version 0.92 or higher) to
54
generate PDF. You can get it without charge from ReportLab:
55
http://www.reportlab.com
57
* Parts of this code still borrow heavily from Marc-Andre
58
Lemburg's py2html who has kindly given permission to
59
include them in py2pdf. Thanks, M.-A.!
63
----------------------------------------------------------------------
64
(c) Copyright by Dinu C. Gherman, 2000 (gherman@europemail.com)
66
Permission to use, copy, modify, and distribute this
67
software and its documentation for any purpose and
68
without fee or royalty is hereby granted, provided
69
that the above copyright notice appear in all copies
70
and that both that copyright notice and this permission
71
notice appear in supporting documentation or portions
72
thereof, including modifications, that you make.
74
THE AUTHOR DINU C. GHERMAN DISCLAIMS ALL WARRANTIES
75
WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED
76
WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT
77
SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT
78
OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
79
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
80
IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
81
ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE
82
OR PERFORMANCE OF THIS SOFTWARE!
86
__author__ = 'Dinu C. Gherman'
87
__date__ = '2001-08-17'
88
__url__ = 'http://starship.python.net/crew/gherman/programs/py2pdf'
91
import sys, string, re, os, getopt
93
from reportlab.pdfgen import canvas
94
from reportlab.lib.colors import Color, HexColor
95
from reportlab.lib import fonts
96
from reportlab.lib.units import cm, inch
99
### Helpers functions.
101
def makeTuple(aString):
102
"""Evaluate a string securely into a tuple.
104
Match the string and return an evaluated object thereof if and
105
only if we think it is a tuple of two or more numeric decimal(!)
106
values, integers or floats. E-notation, hex or octal is not
107
supported, though! Shorthand notation (omitting leading or trail-
108
zeros, before or after the decimal point) like .25 or 25. is
112
c = string.count(aString, ',')
113
num = '(\d*?\.?\d+?)|(\d+?\.?\d*?)'
114
tup = string.join([num]*c, ',')
116
if re.match('\(' + tup + '\)', aString):
119
details = '%s cannot be parsed into a numeric tuple!' % aString
120
raise 'ValueError', details
123
def loadFontifier(options=None):
124
"Load a tagging module and return a corresponding function."
126
# We can't use 'if options' because of the modified
127
# attribute lookup it seems.
129
if type(options) != type(None) and options.marcs:
130
# Use mxTextTool's tagging engine.
132
from mxTextTools import tag
133
from mxTextTools.Examples.Python import python_script
134
tagFunc = lambda text, tag=tag, pytable=python_script: \
141
from reportlab.lib import PyFontify
143
if PyFontify.__version__ < '0.3':
146
tagFunc = PyFontify.fontify
150
Sorry, but this script needs the PyFontify.py module version 0.3
151
or higher; You can download it from Just's homepage at
153
URL: http://starship.python.net/crew/just
160
def makeColorFromString(aString):
161
"""Convert a string to a ReportLab RGB color object.
163
Supported formats are: '0xFFFFFF', '#FFFFFF' and '(R,G,B)'
164
where the latter is a tuple of three floats in the range
170
if s[0] == '#' or s[:2] in ('0x', '0X'):
171
if s[:2] in ('0x', '0X'):
172
return HexColor('#' + s[2:])
175
r, g, b = makeTuple(aString)
176
return Color(r, g, b)
181
# For py2pdf this is some kind of overkill, but it's fun, too.
183
"""Class to capture a paper format/size and its orientation.
185
This class represents an abstract paper format, including
186
the size and orientation of a sheet of paper in some formats
187
plus a few operations to change its size and orientation.
190
_A4W, _A4H = 21*cm, 29.7*cm
191
A0 = (4*_A4W, 4*_A4H)
193
_B4W, _B4H = 25*cm, 35.3*cm
194
B0 = (4*_B4W, 4*_B4H)
196
_C4W, _C4H = 22.9*cm, 32.4*cm
197
C0 = (4*_C4W, 4*_C4H)
199
letter = (8.5*inch, 11*inch)
200
legal = (8.5*inch, 14*inch)
203
def __init__(self, nameOrSize='A4', landscape=0):
209
self.setFormatName(nameOrSize, landscape)
210
elif t == type((1,)):
211
self.setSize(nameOrSize)
212
self.setLandscape(landscape)
216
"""Return a string representation of ourself.
218
The returned string can also be used to recreate
219
the same PaperFormat object again.
222
if self.name != 'custom':
227
format = "PaperFormat(nameOrSize=%s, landscape=%d)"
228
tuple = (nos, self.landscape)
230
return format % tuple
233
def setSize(self, size=None):
234
"Set explicit paper size."
237
x, y = self.size = size
238
self.landscape = x < y
241
def setLandscape(self, flag):
242
"Set paper orientation as desired."
244
# Swap paper orientation if needed.
246
self.landscape = flag
250
if (ls and x < y) or (not ls and x > y):
254
def setFormatName(self, name='A4', landscape=0):
255
"Set paper size derived from a format name."
258
# Assume ISO-A, -B, -C series.
259
# (Hmm, are B and C really ISO standards
260
# or just DIN? Well...)
261
c, f = name[0], int(name[1:])
262
self.size = getattr(self, c + '0')
266
elif name == 'letter':
267
self.size = self.letter
270
elif name == 'legal':
271
self.size = self.legal
274
self.setLandscape(landscape)
277
def makeHalfSize(self, times=1):
278
"Reduce paper size (surface) by 50% multiple times."
280
# Orientation remains unchanged.
282
# Iterates only for times >= 1.
283
for i in xrange(times):
285
self.size = s[1] / 2.0, s[0]
287
if self.name[0] in 'ABC':
288
self.name = self.name[0] + `int(self.name[1:]) + 1`
293
def makeDoubleSize(self, times=1):
294
"Increase paper size (surface) by 50% multiple times."
296
# Orientation remains unchanged.
298
# Iterates only for times >= 1.
299
for i in xrange(times):
301
self.size = s[1], s[0] * 2.0
303
if self.name[0] in 'ABC':
304
self.name = self.name[0] + `int(self.name[1:]) - 1`
310
"""Container class for options from command line and config files.
312
This class is a container for options as they are specified
313
when a program is called from a command line, but also from
314
the same kind of options that are saved in configuration
315
files. Both the short UNIX style (e.g. '-v') as well as the
316
extended GNU style (e.g. '--help') are supported.
318
An 'option' is a <name>/<value> pair with both parts being
319
strings at the beginning, but where <value> might be conver-
320
ted later into any other Python object.
322
Option values can be accessed by using their name as an attri-
323
bute of an Options object, returning None if no such option
324
name exists (that makes None values impossible, but so what?).
327
# Hmm, could also use UserDict... maybe later.
330
"Initialize with some default options name/values."
332
# Hmm, could also pass an initial optional dict...
334
# Create a dictionary containing the options
335
# and populate it with defaults.
340
def __getattr__(self, name):
341
"Turn attribute access into dictionary lookup."
343
return self.pool.get(name)
346
def setDefaults(self):
347
"Set default options."
349
### Maybe get these from a site config file...
350
self.pool.update({'fontName' : 'Courier',
352
'bgCol' : Color(1, 1, 1),
361
# Default colors (for color mode), mostly taken from py2html.
362
self.pool.update({'commCol' : HexColor('#1111CC'),
363
'kwCol' : HexColor('#3333CC'),
364
'identCol' : HexColor('#CC0000'),
365
'paramCol' : HexColor('#000066'),
366
'strngCol' : HexColor('#119911'),
367
'restCol' : HexColor('#000000')})
369
# Add a default 'real' paper format object.
370
pf = PaperFormat(self.paperFormat, self.landscape)
371
self.pool.update({'realPaperFormat' : pf})
372
self.pool.update({'files' : []})
376
"Display all current option names and values."
378
self.saveToFile(sys.stdout)
381
def saveToFile(self, path):
382
"Save options as a log file."
384
if type(path) == type(''):
387
# Assume a file-like object.
390
items = self.pool.items()
394
f.write("%-15s : %s\n" % (n, `v`))
397
def updateWithContentsOfFile(self, path):
398
"""Update options as specified in a config file.
400
The file is expected to contain one option name/value pair
401
(seperated by an equal sign) per line, but no other whitespace.
402
Option values may contain equal signs, but no whitespace.
403
Option names must be valid Python strings, preceeded by one
407
config = open(path).read()
408
config = string.split(config, '\n')
414
if cfg == '' or cfg[0] == '#':
418
if '=' in cfg and cfg[:2] == '--':
419
p = string.find(cfg, '=')
420
opt, arg = cfg[2:p], cfg[p+1:]
421
self.updateOption(opt, arg)
423
# Maybe treat single-letter options as well?
424
# elif ':' in cfg and cfg[0] == '-' and cfg[1] != '-':
428
self.updateOption(cfg[2:], None)
431
def updateWithContentsOfArgv(self, argv):
432
"Update options as specified in a (command line) argument vector."
434
# Specify accepted short option names (UNIX style).
437
# Specify accepted long option names (GNU style).
438
lo = 'tabSize= paperFormat= paperSize= landscape stdout title= fontName= fontSize='
439
lo = lo + ' bgCol= verbose lineNum marcs help multiPage noOutline config= input= mode='
440
lo = lo + ' commCol= identCol= kwCol= strngCol= paramCol= restCol='
441
longOpts = string.split(lo, ' ')
444
optList, args = getopt.getopt(argv, shortOpts, longOpts)
445
except getopt.error, msg:
446
sys.stderr.write("%s\nuse -h or --help for help\n" % str(msg))
449
self.updateOption('files', args) # Hmm, really needed?
452
# Remove leading dashes (max. two).
458
self.updateOption(o, v)
461
def updateOption(self, name, value):
462
"Update an option from a string value."
464
# Special treatment for coloring options...
465
if name[-3:] == 'Col':
466
if name[:-3] in string.split('bg comm ident kw strng param rest', ' '):
467
self.pool[name] = makeColorFromString(value)
469
elif name == 'paperSize':
470
tup = makeTuple(value)
471
self.pool['paperSize'] = tup
472
if not self.realPaperFormat:
473
pf = PaperFormat(self.paperFormat, self.landscape)
474
self.pool['realPaperFormat'] = pf
475
self.pool['realPaperFormat'].setSize(tup)
477
elif name == 'paperFormat':
478
self.pool['paperFormat'] = value
479
if not self.realPaperFormat:
480
pf = PaperFormat(self.paperFormat, self.landscape)
481
self.pool['realPaperFormat'] = pf
482
self.pool['realPaperFormat'].setFormatName(self.paperFormat, self.landscape)
484
elif name == 'landscape':
485
self.pool['landscape'] = 1
486
if self.realPaperFormat:
487
self.pool['realPaperFormat'].setLandscape(1)
489
elif name == 'fontSize':
490
self.pool['fontSize'] = int(value)
492
elif name == 'tabSize':
493
self.pool['tabSize'] = int(value)
496
self.pool['mode'] = value
498
cats = 'comm ident kw strng param rest'
499
for cat in string.split(cats, ' '):
500
self.pool[cat + 'Col'] = Color(0, 0, 0)
502
# Parse configuration file...
503
elif name == 'config':
504
self.updateWithContentsOfFile(value)
506
elif name == 'stdout':
507
self.pool['stdout'] = 1
509
elif name == 'files':
510
self.pool['files'] = value
513
# Set the value found or 1 for options without values.
514
self.pool[name] = value or 1
517
def update(self, **options):
520
# Not much tested and/or used, yet!!
522
for n, v in options.items():
526
### Layouting classes.
529
"""A class to layout a simple PDF document.
531
This is intended to help generate PDF documents where all pages
532
follow the same kind of 'template' which is supposed to be the
533
same adornments (header, footer, etc.) on each page plus a main
534
'frame' on each page. These frames are 'connected' such that one
535
can add individual text lines one by one with automatic line
536
wrapping, page breaks and text flow between frames.
539
def __init__(self, options):
542
self.options = options
544
self.multiLineStringStarted = 0
547
# Set a default color and font.
549
self.currColor = o.restCol
550
self.currFont = (o.fontName, o.fontSize)
555
def setMainFrame(self, frame=None):
556
"Define the main drawing frame of interest for each page."
561
# Maybe a long-term candidate for additional options...
562
width, height = self.options.realPaperFormat.size
563
self.frame = height - 3*cm, 3*cm, 2*cm, width - 2*cm
565
# self.frame is a 4-tuple:
566
# (topMargin, bottomMargin, leftMargin, rightMargin)
569
def setPDFMetaInfo(self):
570
"Set PDF meta information."
574
c.setAuthor('py2pdf %s' % __version__)
580
# For stdin use title option or empty...
581
if self.srcPath == sys.stdin:
584
# otherwise take the input file's name.
586
path = os.path.basename(self.srcPath)
587
filename = o.title or path
592
def setFillColorAndFont(self, color, font):
593
"Set new color/font (maintaining the current 'state')."
596
self.currColor = color
598
fontName, fontSize = font
599
self.text.setFont(fontName, fontSize)
600
self.text.setFillColor(color)
605
def begin(self, srcPath, numLines):
606
"Things to do before doing anything else."
610
self.numLines = numLines
611
self.srcPath = srcPath
613
# Set output filename (stdout if desired).
616
self.pdfPath = sys.stdout
618
if srcPath != sys.stdin:
619
self.pdfPath = os.path.splitext(srcPath)[0] + '.pdf'
621
self.pdfPath = sys.stdout
624
def beginDocument(self):
625
"""Things to do when a new document should be started.
627
The initial page counter is 0, meaning that beginPage()
628
will be called by beginLine()...
631
# Set initial page number and store file name.
637
size = o.realPaperFormat.size
638
self.canvas = canvas.Canvas(self.pdfPath, size, verbosity=0)
640
c.setPageCompression(1)
641
c.setFont(o.fontName, o.fontSize)
643
# Create document meta information.
644
self.setPDFMetaInfo()
649
# Determine the left text margin by adding the with
650
# of the line number to the left margin of the main frame.
651
format = "%%%dd " % len(`self.numLines`)
652
fn, fs = self.currFont
653
text = format % self.lineNum
654
tm, bm, lm, rm = self.frame
657
self.txm = self.txm + c.stringWidth(text, fn, fs)
661
"Things to do when a new page has to be added."
664
self.pageNum = self.pageNum + 1
667
tm, bm, lm, rm = self.frame
668
self.text = self.canvas.beginText(lm, tm - o.fontSize)
669
self.setFillColorAndFont(self.currColor, self.currFont)
671
# Fail if stdout desired (with multiPage).
673
raise "IOError", "Can't create multiple pages on stdout!"
675
# Create canvas with a modified path name.
676
base, ext = os.path.splitext(self.pdfPath)
677
newPath = "%s-%d%s" % (base, self.pageNum, ext)
678
size = o.realPaperFormat.size
679
self.canvas = canvas.Canvas(newPath, size, verbosity=0)
681
c.setPageCompression(1)
682
c.setFont(o.fontName, o.fontSize)
684
# Create document meta information.
685
self.setPDFMetaInfo()
690
tm, bm, lm, rm = self.frame
691
self.text = self.canvas.beginText(lm, tm - o.fontSize)
692
self.setFillColorAndFont(self.currColor, self.currFont)
694
self.putPageDecoration()
697
def beginLine(self, wrapped=0):
698
"Things to do when a new line has to be added."
700
# If there is no page yet, create the first one.
701
if self.pageNum == 0:
704
# If bottom of current page reached, do a page break.
705
# (This works only with one text object being used
706
# for the entire page. Otherwise we need to maintain
707
# the vertical position of the current line ourself.)
709
tm, bm, lm, rm = self.frame
714
# Print line number label, if needed.
717
#self.putLineNumLabel()
718
font = ('Courier', o.fontSize)
721
# Print a label containing the line number.
722
self.setFillColorAndFont(o.restCol, font)
723
format = "%%%dd " % len(`self.numLines`)
724
self.text.textOut(format % self.lineNum)
726
# Print an empty label (using bgCol). Hackish!
727
currCol = self.currColor
728
currFont = self.currFont
729
self.setFillColorAndFont(o.bgCol, font)
730
self.text.textOut(' '*(len(`self.numLines`) + 1))
731
self.setFillColorAndFont(currCol, currFont)
734
def endLine(self, wrapped=0):
735
"Things to do after a line is basically done."
737
# End the current line by adding an 'end of line'.
738
# (Actually done by the text object...)
739
self.text.textLine('')
742
self.lineNum = self.lineNum + 1
746
"Things to do after a page is basically done."
750
# Draw the current text object (later we might want
751
# to do that after each line...).
752
c.drawText(self.text)
755
if self.options.multiPage:
759
def endDocument(self):
760
"Things to do after the document is basically done."
764
# Display rest of last page and save it.
765
c.drawText(self.text)
771
"Things to do after everything has been done."
776
### The real meat: methods writing something to a canvas.
778
def putLineNumLabel(self, text, wrapped=0):
779
"Add a long text that can't be split into chunks."
782
font = ('Courier', o.fontSize)
785
# Print a label containing the line number.
786
self.setFillColorAndFont(o.restCol, font)
787
format = "%%%dd " % len(`self.numLines`)
788
self.text.textOut(format % self.lineNum)
790
# Print an empty label (using bgCol). Hackish!
791
currCol = self.currColor
792
currFont = self.currFont
793
self.setFillColorAndFont(o.bgCol, font)
794
self.text.textOut(' '*(len(`self.numLines`) + 1))
795
self.setFillColorAndFont(currCol, currFont)
798
# Tried this recursively before, in order to determine
799
# an appropriate string limit rapidly, but this wasted
800
# much space and showed very poor results...
801
# This linear method is very slow, but such lines should
804
def putSplitLongText(self, text):
805
"Add a long text that can't be split into chunks."
807
# Now, the splitting will be with 'no mercy',
808
# at the right margin of the main drawing area.
814
tm, bm, lm, rm = self.frame
816
width = self.canvas.stringWidth
817
fn, fs = self.currFont
818
tw = width(text, fn, fs)
826
tx = self.text # Can change after a page break.
827
tw = width(T, fn, fs)
831
self.endLine(wrapped=1)
832
self.beginLine(wrapped=1)
849
def putLongText(self, text):
850
"Add a long text by gracefully splitting it into chunks."
852
# Splitting is currently done only at blanks, but other
853
# characters such as '.' or braces are also good
854
# possibilities... later...
857
tm, bm, lm, rm = self.frame
859
width = self.canvas.stringWidth
860
fn, fs = self.currFont
861
tw = width(text, fn, fs)
863
arr = string.split(text, ' ')
865
for i in range(len(arr)):
867
t = self.text # Can change during the loop...
868
tw = width(a, fn, fs)
871
# If current item does not fit on current line, have it
872
# split and then put before/after a line (maybe also
875
self.putSplitLongText(a)
876
t = self.text # Can change after a page break...
878
# If it fits, just add it to the current text object.
882
# Add the character we used to split th original text.
887
def putText(self, text):
888
"Add some text to the current line."
893
fn, fs = o.fontName, o.fontSize
894
tw = self.canvas.stringWidth(text, fn, fs)
900
self.putLongText(text)
904
def putLine(self, text):
905
"Add a line to the current text."
911
def putPageDecoration(self):
912
"Draw some decoration on each page."
914
# Use some abbreviations.
917
tm, bm, lm, rm = self.frame
919
# Restore default font.
920
c.setFont(o.fontName, o.fontSize)
922
c.setLineWidth(0.5) # in pt.
925
c.setFillColor(o.bgCol)
926
pf = o.realPaperFormat.size
927
c.rect(0, 0, pf[0], pf[1], stroke=0, fill=1)
930
c.setFillColorRGB(0, 0, 0)
931
c.line(lm, tm + .5*cm, rm, tm + .5*cm)
932
c.setFont('Times-Italic', 12)
934
if self.pdfPath == sys.stdout:
935
filename = o.title or ' '
937
path = os.path.basename(self.srcPath)
938
filename = o.title or path
940
c.drawString(lm, tm + 0.75*cm + 2, filename)
943
c.line(lm, bm - .5*cm, rm, bm - .5*cm)
944
c.drawCentredString(0.5 * pf[0], 0.5*bm, "Page %d" % self.pageNum)
946
# Box around main frame.
947
# c.rect(lm, bm, rm - lm, tm - bm)
950
class PythonPDFLayouter (PDFLayouter):
951
"""A class to layout a simple multi-page PDF document.
954
### API for adding specific Python entities.
961
# Make base font bold.
962
fam, b, i = fonts.ps2tt(o.fontName)
963
ps = fonts.tt2ps(fam, 1, i)
964
font = (ps, o.fontSize)
966
self.setFillColorAndFont(o.kwCol, font)
969
if not o.noOutline and not o.multiPage:
970
if t in ('class', 'def'):
971
tm, bm, lm, rm = self.frame
972
pos = self.text.getX()
975
self.startPositions = []
979
if not hasattr(self, 'startPositions'):
980
self.startPositions = []
982
if pos not in self.startPositions:
983
self.startPositions.append(pos)
985
# Memorize certain keywords.
989
self.itemFound = None
995
def addIdent(self, t):
1000
# Make base font bold.
1001
fam, b, i = fonts.ps2tt(o.fontName)
1002
ps = fonts.tt2ps(fam, 1, i)
1003
font = (ps, o.fontSize)
1005
self.setFillColorAndFont(o.identCol, font)
1008
# Bookmark certain identifiers (class and function names).
1009
if not o.noOutline and not o.multiPage:
1010
item = self.itemFound
1012
# Add line height to current vert. position.
1013
pos = self.text.getY() + o.fontSize
1015
nameTag = "p%sy%s" % (self.pageNum, pos)
1017
i = self.startPositions.index(self.startPos)
1018
c.bookmarkHorizontalAbsolute(nameTag, pos)
1019
c.addOutlineEntry('%s %s' % (item, t), nameTag, i)
1022
def addParam(self, t):
1026
font = (o.fontName, o.fontSize)
1027
self.setFillColorAndFont(o.paramCol, font)
1028
self.text.putText(t)
1031
def addSimpleString(self, t):
1032
"Add a simple string."
1035
font = (o.fontName, o.fontSize)
1036
self.setFillColorAndFont(o.strngCol, font)
1040
def addTripleStringBegin(self):
1041
"Memorize begin of a multi-line string."
1043
# Memorise that we started a multi-line string.
1044
self.multiLineStringStarted = 1
1047
def addTripleStringEnd(self, t):
1048
"Add a multi-line string."
1052
# Forget about the multi-line string again.
1053
self.multiLineStringStarted = 0
1056
def addComm(self, t):
1061
# Make base font slanted.
1062
fam, b, i = fonts.ps2tt(o.fontName)
1063
ps = fonts.tt2ps(fam, b, 1)
1064
font = (ps, o.fontSize)
1066
self.setFillColorAndFont(o.commCol, font)
1070
def addRest(self, line, eol):
1071
"Add a regular thing."
1075
# Nothing else to be done, print line as-is...
1077
font = (o.fontName, o.fontSize)
1078
self.setFillColorAndFont(o.restCol, font)
1080
# ... except if the multi-line-string flag is set, then we
1081
# decide to change the current color to that of strings and
1083
if self.multiLineStringStarted:
1084
self.setFillColorAndFont(o.strngCol, font)
1088
# Print an empty line.
1096
class EmptyPythonPDFLayouter (PythonPDFLayouter):
1097
"""A PDF layout with no decoration and no margins.
1099
The main frame extends fully to all paper edges. This is
1100
useful for creating PDFs when writing one page per file,
1101
in order to provide pre-rendered, embellished Python
1102
source code to magazine publishers, who can include the
1103
individual files and only need to add their own captures.
1106
def setMainFrame(self, frame=None):
1107
"Make a frame extending to all paper edges."
1109
width, height = self.options.realPaperFormat.size
1110
self.frame = height, 0, 0, width
1113
def putPageDecoration(self):
1114
"Draw no decoration at all."
1119
### Pretty-printing classes.
1122
"""Generic PDF Printer class.
1124
Does not do much, but write a PDF file created from
1125
any ASCII input file.
1131
def __init__(self, options=None):
1134
self.data = None # Contains the input file.
1135
self.inPath = None # Path of input file.
1136
self.Layouter = PDFLayouter
1138
if type(options) != type(None):
1139
self.options = options
1141
self.options = Options()
1146
def readFile(self, path):
1147
"Read the content of a file."
1149
if path == sys.stdin:
1158
self.data = re.sub('\t', ' '*o.tabSize, data)
1162
def formatLine(self, line, eol=0):
1163
"Format one line of Python source code."
1165
font = ('Courier', 8)
1166
self.layouter.setFillColorAndFont(Color(0, 0, 0), font)
1167
self.layouter.putText(line)
1170
def writeData(self, srcCodeLines, inPath, outPath=None):
1171
"Convert Python source code lines into a PDF document."
1173
# Create a layouter object.
1174
self.layouter = self.Layouter(self.options)
1177
# Loop over all tagged source lines, dissect them into
1178
# Python entities ourself and let the layouter do the
1180
splitCodeLines = string.split(srcCodeLines, '\n')
1182
### Must also handle the case of outPath being sys.stdout!!
1183
l.begin(inPath, len(splitCodeLines))
1186
for line in splitCodeLines:
1188
self.formatLine(line)
1195
def writeFile(self, data, inPath=None, outPath=None):
1196
"Write some data into a file."
1198
if inPath == sys.stdin:
1199
self.outPath = sys.stdout
1202
path = os.path.splitext(self.inPath)[0]
1203
self.outPath = path + self.outFileExt
1205
self.writeData(data, inPath, outPath or self.outPath)
1208
def process(self, inPath, outPath=None):
1209
"The real 'action point' for working with Pretty-Printers."
1211
self.readFile(inPath)
1212
self.writeFile(self.data, inPath, outPath)
1215
class PythonPDFPrinter (PDFPrinter):
1216
"""A class to nicely format tagged Python source code.
1229
def __init__(self, options=None):
1230
"Initialisation, calling self._didInit() at the end."
1232
if type(options) != type(None):
1233
self.options = options
1235
self.options = Options()
1243
# Define regular expression patterns.
1247
s.commPat = comp('(.*?)<' + s.comm + '>(.*?)</' + s.comm + '>(.*)')
1248
s.kwPat = comp('(.*?)<' + s.kw + '>(.*?)</' + s.kw + '>(.*)')
1249
s.identPat = comp('(.*?)<' + s.ident + '>(.*?)</' + s.ident + '>(.*)')
1250
s.paramPat = comp('(.*?)<' + s.param + '>(.*?)</' + s.param + '>(.*)')
1251
s.stPat = comp('(.*?)<' + s.strng + '>(.*?)</' + s.strng + '>(.*)')
1252
s.strng1Pat = comp('(.*?)<' + s.strng + '>(.*)')
1253
s.strng2Pat = comp('(.*?)</' + s.strng + '>(.*)')
1254
s.allPat = comp('(.*)')
1256
cMatch = s.commPat.match
1257
kMatch = s.kwPat.match
1258
iMatch = s.identPat.match
1259
pMatch = s.paramPat.match
1260
sMatch = s.stPat.match
1261
s1Match = s.strng1Pat.match
1262
s2Match = s.strng2Pat.match
1263
aMatch = s.allPat.match
1265
self.matchList = ((cMatch, 'Comm'),
1269
(sMatch, 'SimpleString'),
1270
(s1Match, 'TripleStringBegin'),
1271
(s2Match, 'TripleStringEnd'),
1274
self.Layouter = PythonPDFLayouter
1277
self.tagFunc = loadFontifier(self.options)
1282
def formatLine(self, line, eol=0):
1283
"Format one line of Python source code."
1285
# Values for eol: -1:no-eol, 0:dunno-yet, 1:do-eol.
1287
for match, meth in self.matchList:
1291
groups = res.groups()
1292
method = getattr(self, '_format%s' % meth)
1297
def _formatIdent(self, groups, eol):
1298
"Format a Python identifier."
1300
before, id, after = groups
1301
self.formatLine(before, -1)
1302
self.layouter.addIdent(id)
1303
self.formatLine(after, eol)
1306
def _formatParam(self, groups, eol):
1307
"Format a Python parameter."
1309
before, param, after = groups
1310
self.formatLine(before, -1)
1311
self.layouter.addParam(before)
1312
self.formatLine(after, eol)
1315
def _formatSimpleString(self, groups, eol):
1316
"Format a Python one-line string."
1318
before, s, after = groups
1319
self.formatLine(before, -1)
1320
self.layouter.addSimpleString(s)
1321
self.formatLine(after, eol)
1324
def _formatTripleStringBegin(self, groups, eol):
1325
"Format a Python multi-line line string (1)."
1327
before, after = groups
1328
self.formatLine(before, -1)
1329
self.layouter.addTripleStringBegin()
1330
self.formatLine(after, 1)
1333
def _formatTripleStringEnd(self, groups, eol):
1334
"Format a Python multi-line line string (2)."
1336
before, after = groups
1337
self.layouter.addTripleStringEnd(before)
1338
self.formatLine(after, 1)
1341
def _formatKw(self, groups, eol):
1342
"Format a Python keyword."
1344
before, kw, after = groups
1345
self.formatLine(before, -1)
1346
self.layouter.addKw(kw)
1347
self.formatLine(after, eol)
1350
def _formatComm(self, groups, eol):
1351
"Format a Python comment."
1353
before, comment, after = groups
1354
self.formatLine(before, -1)
1355
self.layouter.addComm(comment)
1356
self.formatLine(after, 1)
1359
def _formatRest(self, groups, eol):
1360
"Format a piece of a Python line w/o anything special."
1363
self.layouter.addRest(line, eol)
1368
def writeData(self, srcCodeLines, inPath, outPath=None):
1369
"Convert Python source code lines into a PDF document."
1371
# Create a layouter object.
1372
self.layouter = self.Layouter(self.options)
1375
# Loop over all tagged source lines, dissect them into
1376
# Python entities ourself and let the layouter do the
1378
splitCodeLines = string.split(srcCodeLines, '\n')
1379
l.begin(inPath, len(splitCodeLines))
1382
for line in splitCodeLines:
1384
self.formatLine(line)
1391
def process(self, inPath, outPath=None):
1392
"The real 'action point' for working with Pretty-Printers."
1394
self.readFile(inPath)
1395
self.taggedData = self._fontify(self.data)
1396
self.writeFile(self.taggedData, inPath, outPath)
1401
def _fontify(self, pytext):
1406
'comment' : ('<%s>' % self.comm, '</%s>' % self.comm),
1407
'keyword' : ('<%s>' % self.kw, '</%s>' % self.kw),
1408
'parameter' : ('<%s>' % self.param, '</%s>' % self.param),
1409
'identifier' : ('<%s>' % self.ident, '</%s>' % self.ident),
1410
'string' : ('<%s>' % self.strng, '</%s>' % self.strng) }
1413
taglist = self.tagFunc(pytext)
1415
# Prepend special 'rest' tag.
1416
taglist[:0] = [('rest', 0, len(pytext), None)]
1418
# Prepare splitting.
1420
self._addSplits(splits, pytext, formats, taglist)
1422
# Do splitting & inserting.
1427
for ri, dummy, insert in splits:
1429
l.append(pytext[li:ri])
1434
if li < len(pytext):
1435
l.append(pytext[li:])
1437
return string.join(l, '')
1440
def _addSplits(self, splits, text, formats, taglist):
1443
# Helper for fontify().
1444
for id, left, right, sublist in taglist:
1447
pre, post = formats[id]
1449
# msg = 'Warning: no format '
1450
# msg = msg + 'for %s specified\n'%repr(id)
1451
# sys.stderr.write(msg)
1454
if type(pre) != type(''):
1455
pre = pre(text[left:right])
1457
if type(post) != type(''):
1458
post = post(text[left:right])
1460
# len(splits) is a dummy used to make sorting stable.
1461
splits.append((left, len(splits), pre))
1464
self._addSplits(splits, text, formats, sublist)
1466
splits.append((right, len(splits), post))
1472
"Process command line as if it were sys.argv"
1474
# Create default options and initialize with argv
1475
# from the command line.
1477
options.updateWithContentsOfArgv(cmdline[1:])
1479
# Print help message if desired, then exit.
1480
if options.h or options.help:
1484
# Apply modest consistency checks and exit if needed.
1485
cmdStr = string.join(cmdline, ' ')
1487
if find(cmdStr, 'paperSize') >= 0 and find(cmdStr, 'paperFormat') >= 0:
1488
details = "You can specify either paperSize or paperFormat, "
1489
details = detail + "but not both!"
1490
raise 'ValueError', details
1492
# Create PDF converter and pass options to it.
1494
input = string.lower(options.input)
1496
if input == 'python':
1497
P = PythonPDFPrinter
1498
elif input == 'ascii':
1501
details = "Input file type must be 'python' or 'ascii'."
1502
raise 'ValueError', details
1505
P = PythonPDFPrinter
1509
# Display options if needed.
1510
if options.v or options.verbose:
1511
pass # p.options.display()
1514
verbose = options.v or options.verbose
1517
if len(options.files) > 1 and verbose:
1518
print "Warning: will only convert first file on command line."
1519
f = options.files[0]
1520
p.process(f, sys.stdout)
1523
print 'py2pdf: working on:'
1525
for f in options.files:
1532
p.process(sys.stdin, sys.stdout)
1542
def process(*args, **kwargs):
1543
"Provides a way of using py2pdf from within other Python scripts."
1545
noValOpts = 'h v verbose landscape stdout lineNum marcs help multiPage noOutline'
1546
noValOpts = string.split(noValOpts, ' ')
1549
for k, v in kwargs.items():
1552
if k not in noValOpts:
1556
if k not in noValOpts:
1558
s = s + ' ' + string.join(args, ' ')
1559
s = string.split(s, ' ')
1566
if __name__=='__main__': #NORUNTESTS
b'\\ No newline at end of file'