~toolpart/openobject-server/toolpart

« back to all changes in this revision

Viewing changes to bin/reportlab/tools/py2pdf/py2pdf.py

  • Committer: pinky
  • Date: 2006-12-07 13:41:40 UTC
  • Revision ID: pinky-3f10ee12cea3c4c75cef44ab04ad33ef47432907
New trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python2.3
 
2
 
 
3
""" Python Highlighter for PDF                     Version: 0.6
 
4
 
 
5
    py2pdf.py [options] <file1> [<file2> ...]
 
6
 
 
7
    options:
 
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)
 
19
                         'mono':  output in b/w
 
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:
 
31
                         'comm':  comments
 
32
                         'ident': identifiers
 
33
                         'kw':    keywords
 
34
                         'strng': strings
 
35
                         'param': parameters
 
36
                         'rest':  all the rest
 
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
 
46
 
 
47
    Takes the input, assuming it is Python source code and formats
 
48
    it into PDF.
 
49
 
 
50
    * Uses Just van Rossum's PyFontify version 0.4 to tag Python
 
51
      scripts, now included in reportlab.lib.
 
52
 
 
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
 
56
 
 
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.!
 
60
"""
 
61
 
 
62
__copyright__ = """
 
63
----------------------------------------------------------------------
 
64
(c) Copyright by Dinu C. Gherman, 2000  (gherman@europemail.com)
 
65
 
 
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.
 
73
 
 
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!
 
83
"""
 
84
 
 
85
__version__ = '0.6'
 
86
__author__  = 'Dinu C. Gherman'
 
87
__date__    = '2001-08-17'
 
88
__url__     = 'http://starship.python.net/crew/gherman/programs/py2pdf'
 
89
 
 
90
 
 
91
import sys, string, re, os, getopt
 
92
 
 
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
 
97
 
 
98
 
 
99
### Helpers functions.
 
100
 
 
101
def makeTuple(aString):
 
102
    """Evaluate a string securely into a tuple.
 
103
 
 
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
 
109
    supported.
 
110
    """
 
111
 
 
112
    c = string.count(aString, ',')
 
113
    num = '(\d*?\.?\d+?)|(\d+?\.?\d*?)'
 
114
    tup = string.join([num]*c, ',')
 
115
 
 
116
    if re.match('\(' + tup + '\)', aString):
 
117
        return eval(aString)
 
118
    else:
 
119
        details = '%s cannot be parsed into a numeric tuple!' % aString
 
120
        raise 'ValueError', details
 
121
 
 
122
 
 
123
def loadFontifier(options=None):
 
124
    "Load a tagging module and return a corresponding function."
 
125
 
 
126
    # We can't use 'if options' because of the modified
 
127
    # attribute lookup it seems.
 
128
 
 
129
    if type(options) != type(None) and options.marcs:
 
130
        # Use mxTextTool's tagging engine.
 
131
 
 
132
        from mxTextTools import tag
 
133
        from mxTextTools.Examples.Python import python_script
 
134
        tagFunc = lambda text, tag=tag, pytable=python_script: \
 
135
                  tag(text,pytable)[1]
 
136
 
 
137
    else:
 
138
        # Load Just's.
 
139
 
 
140
        try:
 
141
            from reportlab.lib import PyFontify
 
142
 
 
143
            if PyFontify.__version__ < '0.3':
 
144
                raise ValueError
 
145
 
 
146
            tagFunc = PyFontify.fontify
 
147
 
 
148
        except:
 
149
            print """
 
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
 
152
 
 
153
       URL: http://starship.python.net/crew/just
 
154
"""
 
155
            sys.exit()
 
156
 
 
157
    return tagFunc
 
158
 
 
159
 
 
160
def makeColorFromString(aString):
 
161
    """Convert a string to a ReportLab RGB color object.
 
162
 
 
163
    Supported formats are: '0xFFFFFF', '#FFFFFF' and '(R,G,B)'
 
164
    where the latter is a tuple of three floats in the range
 
165
    [0.0, 1.0].
 
166
    """
 
167
 
 
168
    s = aString
 
169
 
 
170
    if s[0] == '#' or s[:2] in ('0x', '0X'):
 
171
        if s[:2] in ('0x', '0X'):
 
172
            return HexColor('#' + s[2:])
 
173
 
 
174
    elif s[0] == '(':
 
175
        r, g, b = makeTuple(aString)
 
176
        return Color(r, g, b)
 
177
 
 
178
 
 
179
### Utility classes.
 
180
 
 
181
# For py2pdf this is some kind of overkill, but it's fun, too.
 
182
class PaperFormat:
 
183
    """Class to capture a paper format/size and its orientation.
 
184
 
 
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.
 
188
    """
 
189
 
 
190
    _A4W, _A4H = 21*cm, 29.7*cm
 
191
    A0 = (4*_A4W, 4*_A4H)
 
192
 
 
193
    _B4W, _B4H = 25*cm, 35.3*cm
 
194
    B0 = (4*_B4W, 4*_B4H)
 
195
 
 
196
    _C4W, _C4H = 22.9*cm, 32.4*cm
 
197
    C0 = (4*_C4W, 4*_C4H)
 
198
 
 
199
    letter = (8.5*inch, 11*inch)
 
200
    legal = (8.5*inch, 14*inch)
 
201
 
 
202
 
 
203
    def __init__(self, nameOrSize='A4', landscape=0):
 
204
        "Initialisation."
 
205
 
 
206
        t = type(nameOrSize)
 
207
 
 
208
        if t == type(''):
 
209
            self.setFormatName(nameOrSize, landscape)
 
210
        elif t == type((1,)):
 
211
            self.setSize(nameOrSize)
 
212
            self.setLandscape(landscape)
 
213
 
 
214
 
 
215
    def __repr__(self):
 
216
        """Return a string representation of ourself.
 
217
 
 
218
        The returned string can also be used to recreate
 
219
        the same PaperFormat object again.
 
220
        """
 
221
 
 
222
        if self.name != 'custom':
 
223
            nos = `self.name`
 
224
        else:
 
225
            nos = `self.size`
 
226
 
 
227
        format = "PaperFormat(nameOrSize=%s, landscape=%d)"
 
228
        tuple = (nos, self.landscape)
 
229
 
 
230
        return format % tuple
 
231
 
 
232
 
 
233
    def setSize(self, size=None):
 
234
        "Set explicit paper size."
 
235
 
 
236
        self.name = 'custom'
 
237
        x, y = self.size = size
 
238
        self.landscape = x < y
 
239
 
 
240
 
 
241
    def setLandscape(self, flag):
 
242
        "Set paper orientation as desired."
 
243
 
 
244
        # Swap paper orientation if needed.
 
245
 
 
246
        self.landscape = flag
 
247
        x, y = self.size
 
248
 
 
249
        ls = self.landscape
 
250
        if (ls and x < y) or (not ls and x > y):
 
251
            self.size = y, x
 
252
 
 
253
 
 
254
    def setFormatName(self, name='A4', landscape=0):
 
255
        "Set paper size derived from a format name."
 
256
 
 
257
        if name[0] in 'ABC':
 
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')
 
263
            self.name = c + '0'
 
264
            self.makeHalfSize(f)
 
265
 
 
266
        elif name == 'letter':
 
267
            self.size = self.letter
 
268
            self.name = 'letter'
 
269
 
 
270
        elif name == 'legal':
 
271
            self.size = self.legal
 
272
            self.name = 'legal'
 
273
 
 
274
        self.setLandscape(landscape)
 
275
 
 
276
 
 
277
    def makeHalfSize(self, times=1):
 
278
        "Reduce paper size (surface) by 50% multiple times."
 
279
 
 
280
        # Orientation remains unchanged.
 
281
 
 
282
        # Iterates only for times >= 1.
 
283
        for i in xrange(times):
 
284
            s = self.size
 
285
            self.size = s[1] / 2.0, s[0]
 
286
 
 
287
            if self.name[0] in 'ABC':
 
288
                self.name = self.name[0] + `int(self.name[1:]) + 1`
 
289
            else:
 
290
                self.name = 'custom'
 
291
 
 
292
 
 
293
    def makeDoubleSize(self, times=1):
 
294
        "Increase paper size (surface) by 50% multiple times."
 
295
 
 
296
        # Orientation remains unchanged.
 
297
 
 
298
        # Iterates only for times >= 1.
 
299
        for i in xrange(times):
 
300
            s = self.size
 
301
            self.size = s[1], s[0] * 2.0
 
302
 
 
303
            if self.name[0] in 'ABC':
 
304
                self.name = self.name[0] + `int(self.name[1:]) - 1`
 
305
            else:
 
306
                self.name = 'custom'
 
307
 
 
308
 
 
309
class Options:
 
310
    """Container class for options from command line and config files.
 
311
 
 
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.
 
317
 
 
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.
 
321
 
 
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?).
 
325
    """
 
326
 
 
327
    # Hmm, could also use UserDict... maybe later.
 
328
 
 
329
    def __init__(self):
 
330
        "Initialize with some default options name/values."
 
331
 
 
332
        # Hmm, could also pass an initial optional dict...
 
333
 
 
334
        # Create a dictionary containing the options
 
335
        # and populate it with defaults.
 
336
        self.pool = {}
 
337
        self.setDefaults()
 
338
 
 
339
 
 
340
    def __getattr__(self, name):
 
341
        "Turn attribute access into dictionary lookup."
 
342
 
 
343
        return self.pool.get(name)
 
344
 
 
345
 
 
346
    def setDefaults(self):
 
347
        "Set default options."
 
348
 
 
349
        ### Maybe get these from a site config file...
 
350
        self.pool.update({'fontName' : 'Courier',
 
351
            'fontSize'   : 8,
 
352
            'bgCol'      : Color(1, 1, 1),
 
353
            'mode'       : 'color',
 
354
            'lineNum'    : 0,
 
355
            'tabSize'    : 4,
 
356
            'paperFormat': 'A4',
 
357
            'landscape'  : 0,
 
358
            'title'      : None,
 
359
            'multiPage'  : 0})
 
360
 
 
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')})
 
368
 
 
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' : []})
 
373
 
 
374
 
 
375
    def display(self):
 
376
        "Display all current option names and values."
 
377
 
 
378
        self.saveToFile(sys.stdout)
 
379
 
 
380
 
 
381
    def saveToFile(self, path):
 
382
        "Save options as a log file."
 
383
 
 
384
        if type(path) == type(''):
 
385
            f = open(path, 'w')
 
386
        else:
 
387
            # Assume a file-like object.
 
388
            f = path
 
389
 
 
390
        items = self.pool.items()
 
391
        items.sort()
 
392
 
 
393
        for n, v in items:
 
394
            f.write("%-15s : %s\n" % (n, `v`))
 
395
 
 
396
 
 
397
    def updateWithContentsOfFile(self, path):
 
398
        """Update options as specified in a config file.
 
399
 
 
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
 
404
        or two dashes.
 
405
        """
 
406
 
 
407
        config = open(path).read()
 
408
        config = string.split(config, '\n')
 
409
 
 
410
        for cfg in config:
 
411
            if cfg == None:
 
412
                break
 
413
 
 
414
            if cfg == '' or cfg[0] == '#':
 
415
                continue
 
416
 
 
417
            # GNU long options
 
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)
 
422
 
 
423
            # Maybe treat single-letter options as well?
 
424
            # elif ':' in cfg and cfg[0] == '-' and cfg[1] != '-':
 
425
            #    pass
 
426
 
 
427
            else:
 
428
                self.updateOption(cfg[2:], None)
 
429
 
 
430
 
 
431
    def updateWithContentsOfArgv(self, argv):
 
432
        "Update options as specified in a (command line) argument vector."
 
433
 
 
434
        # Specify accepted short option names (UNIX style).
 
435
        shortOpts = 'hv'
 
436
 
 
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, ' ')
 
442
 
 
443
        try:
 
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))
 
447
            sys.exit(2)
 
448
 
 
449
        self.updateOption('files', args) # Hmm, really needed?
 
450
 
 
451
        for o, v in optList:
 
452
            # Remove leading dashes (max. two).
 
453
            if o[0] == '-':
 
454
                o = o[1:]
 
455
            if o[0] == '-':
 
456
                o = o[1:]
 
457
 
 
458
            self.updateOption(o, v)
 
459
 
 
460
 
 
461
    def updateOption(self, name, value):
 
462
        "Update an option from a string value."
 
463
 
 
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)
 
468
 
 
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)
 
476
 
 
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)
 
483
 
 
484
        elif name == 'landscape':
 
485
            self.pool['landscape'] = 1
 
486
            if self.realPaperFormat:
 
487
                self.pool['realPaperFormat'].setLandscape(1)
 
488
 
 
489
        elif name == 'fontSize':
 
490
            self.pool['fontSize'] = int(value)
 
491
 
 
492
        elif name == 'tabSize':
 
493
            self.pool['tabSize'] = int(value)
 
494
 
 
495
        elif name == 'mode':
 
496
            self.pool['mode'] = value
 
497
            if value == 'mono':
 
498
                cats = 'comm ident kw strng param rest'
 
499
                for cat in string.split(cats, ' '):
 
500
                    self.pool[cat + 'Col'] = Color(0, 0, 0)
 
501
 
 
502
        # Parse configuration file...
 
503
        elif name == 'config':
 
504
            self.updateWithContentsOfFile(value)
 
505
 
 
506
        elif name == 'stdout':
 
507
            self.pool['stdout'] = 1
 
508
 
 
509
        elif name == 'files':
 
510
            self.pool['files'] = value
 
511
 
 
512
        else:
 
513
            # Set the value found or 1 for options without values.
 
514
            self.pool[name] = value or 1
 
515
 
 
516
 
 
517
    def update(self, **options):
 
518
        "Update options."
 
519
 
 
520
        # Not much tested and/or used, yet!!
 
521
 
 
522
        for n, v in options.items():
 
523
            self.pool[n] = v
 
524
 
 
525
 
 
526
### Layouting classes.
 
527
 
 
528
class PDFLayouter:
 
529
    """A class to layout a simple PDF document.
 
530
 
 
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.
 
537
    """
 
538
 
 
539
    def __init__(self, options):
 
540
        "Initialisation."
 
541
 
 
542
        self.options = options
 
543
        self.canvas = None
 
544
        self.multiLineStringStarted = 0
 
545
        self.lineNum = 0
 
546
 
 
547
        # Set a default color and font.
 
548
        o = self.options
 
549
        self.currColor = o.restCol
 
550
        self.currFont = (o.fontName, o.fontSize)
 
551
 
 
552
 
 
553
    ### Helper methods.
 
554
 
 
555
    def setMainFrame(self, frame=None):
 
556
        "Define the main drawing frame of interest for each page."
 
557
 
 
558
        if frame:
 
559
            self.frame = frame
 
560
        else:
 
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
 
564
 
 
565
            # self.frame is a 4-tuple:
 
566
            # (topMargin, bottomMargin, leftMargin, rightMargin)
 
567
 
 
568
 
 
569
    def setPDFMetaInfo(self):
 
570
        "Set PDF meta information."
 
571
 
 
572
        o = self.options
 
573
        c = self.canvas
 
574
        c.setAuthor('py2pdf %s' % __version__)
 
575
        c.setSubject('')
 
576
 
 
577
        # Set filename.
 
578
        filename = ''
 
579
 
 
580
        # For stdin use title option or empty...
 
581
        if self.srcPath == sys.stdin:
 
582
            if o.title:
 
583
                filename = o.title
 
584
        # otherwise take the input file's name.
 
585
        else:
 
586
            path = os.path.basename(self.srcPath)
 
587
            filename = o.title or path
 
588
 
 
589
        c.setTitle(filename)
 
590
 
 
591
 
 
592
    def setFillColorAndFont(self, color, font):
 
593
        "Set new color/font (maintaining the current 'state')."
 
594
 
 
595
        self.currFont = font
 
596
        self.currColor = color
 
597
 
 
598
        fontName, fontSize = font
 
599
        self.text.setFont(fontName, fontSize)
 
600
        self.text.setFillColor(color)
 
601
 
 
602
 
 
603
    ### API
 
604
 
 
605
    def begin(self, srcPath, numLines):
 
606
        "Things to do before doing anything else."
 
607
 
 
608
        self.lineNum = 0
 
609
        self.pageNum = 0
 
610
        self.numLines = numLines
 
611
        self.srcPath = srcPath
 
612
 
 
613
        # Set output filename (stdout if desired).
 
614
        o = self.options
 
615
        if o.stdout:
 
616
            self.pdfPath = sys.stdout
 
617
        else:
 
618
            if srcPath != sys.stdin:
 
619
                self.pdfPath = os.path.splitext(srcPath)[0] + '.pdf'
 
620
            else:
 
621
                self.pdfPath = sys.stdout
 
622
 
 
623
 
 
624
    def beginDocument(self):
 
625
        """Things to do when a new document should be started.
 
626
 
 
627
        The initial page counter is 0, meaning that beginPage()
 
628
        will be called by beginLine()...
 
629
        """
 
630
 
 
631
        # Set initial page number and store file name.
 
632
        self.pageNum = 0
 
633
        o = self.options
 
634
 
 
635
        if not o.multiPage:
 
636
            # Create canvas.
 
637
            size = o.realPaperFormat.size
 
638
            self.canvas = canvas.Canvas(self.pdfPath, size, verbosity=0)
 
639
            c = self.canvas
 
640
            c.setPageCompression(1)
 
641
            c.setFont(o.fontName, o.fontSize)
 
642
 
 
643
            # Create document meta information.
 
644
            self.setPDFMetaInfo()
 
645
 
 
646
            # Set drawing frame.
 
647
            self.setMainFrame()
 
648
 
 
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
 
655
            self.txm = lm
 
656
            if o.lineNum:
 
657
                self.txm = self.txm + c.stringWidth(text, fn, fs)
 
658
 
 
659
 
 
660
    def beginPage(self):
 
661
        "Things to do when a new page has to be added."
 
662
 
 
663
        o = self.options
 
664
        self.pageNum = self.pageNum + 1
 
665
 
 
666
        if not o.multiPage:
 
667
            tm, bm, lm, rm = self.frame
 
668
            self.text = self.canvas.beginText(lm, tm - o.fontSize)
 
669
            self.setFillColorAndFont(self.currColor, self.currFont)
 
670
        else:
 
671
            # Fail if stdout desired (with multiPage).
 
672
            if o.stdout:
 
673
                raise "IOError", "Can't create multiple pages on stdout!"
 
674
 
 
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)
 
680
            c = self.canvas
 
681
            c.setPageCompression(1)
 
682
            c.setFont(o.fontName, o.fontSize)
 
683
 
 
684
            # Create document meta information.
 
685
            self.setPDFMetaInfo()
 
686
 
 
687
            # Set drawing frame.
 
688
            self.setMainFrame()
 
689
 
 
690
            tm, bm, lm, rm = self.frame
 
691
            self.text = self.canvas.beginText(lm, tm - o.fontSize)
 
692
            self.setFillColorAndFont(self.currColor, self.currFont)
 
693
 
 
694
        self.putPageDecoration()
 
695
 
 
696
 
 
697
    def beginLine(self, wrapped=0):
 
698
        "Things to do when a new line has to be added."
 
699
 
 
700
        # If there is no page yet, create the first one.
 
701
        if self.pageNum == 0:
 
702
            self.beginPage()
 
703
 
 
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.)
 
708
        y = self.text.getY()
 
709
        tm, bm, lm, rm = self.frame
 
710
        if y < bm:
 
711
            self.endPage()
 
712
            self.beginPage()
 
713
 
 
714
        # Print line number label, if needed.
 
715
        o = self.options
 
716
        if o.lineNum:
 
717
            #self.putLineNumLabel()
 
718
            font = ('Courier', o.fontSize)
 
719
 
 
720
            if not wrapped:
 
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)
 
725
            else:
 
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)
 
732
 
 
733
 
 
734
    def endLine(self, wrapped=0):
 
735
        "Things to do after a line is basically done."
 
736
 
 
737
        # End the current line by adding an 'end of line'.
 
738
        # (Actually done by the text object...)
 
739
        self.text.textLine('')
 
740
 
 
741
        if not wrapped:
 
742
            self.lineNum = self.lineNum + 1
 
743
 
 
744
 
 
745
    def endPage(self):
 
746
        "Things to do after a page is basically done."
 
747
 
 
748
        c = self.canvas
 
749
 
 
750
        # Draw the current text object (later we might want
 
751
        # to do that after each line...).
 
752
        c.drawText(self.text)
 
753
        c.showPage()
 
754
 
 
755
        if self.options.multiPage:
 
756
            c.save()
 
757
 
 
758
 
 
759
    def endDocument(self):
 
760
        "Things to do after the document is basically done."
 
761
 
 
762
        c = self.canvas
 
763
 
 
764
        # Display rest of last page and save it.
 
765
        c.drawText(self.text)
 
766
        c.showPage()
 
767
        c.save()
 
768
 
 
769
 
 
770
    def end(self):
 
771
        "Things to do after everything has been done."
 
772
 
 
773
        pass
 
774
 
 
775
 
 
776
    ### The real meat: methods writing something to a canvas.
 
777
 
 
778
    def putLineNumLabel(self, text, wrapped=0):
 
779
        "Add a long text that can't be split into chunks."
 
780
 
 
781
        o = self.options
 
782
        font = ('Courier', o.fontSize)
 
783
 
 
784
        if not wrapped:
 
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)
 
789
        else:
 
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)
 
796
 
 
797
 
 
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
 
802
    # be very rare, too!
 
803
 
 
804
    def putSplitLongText(self, text):
 
805
        "Add a long text that can't be split into chunks."
 
806
 
 
807
        # Now, the splitting will be with 'no mercy',
 
808
        # at the right margin of the main drawing area.
 
809
 
 
810
        M = len(text)
 
811
        t = self.text
 
812
        x = t.getX()
 
813
        o = self.options
 
814
        tm, bm, lm, rm = self.frame
 
815
 
 
816
        width = self.canvas.stringWidth
 
817
        fn, fs = self.currFont
 
818
        tw = width(text, fn, fs)
 
819
 
 
820
        tx = self.text
 
821
        if tw > rm - lm - x:
 
822
            i = 1
 
823
            T = ''
 
824
            while text:
 
825
                T = text[:i]
 
826
                tx = self.text # Can change after a page break.
 
827
                tw = width(T, fn, fs)
 
828
 
 
829
                if x + tw > rm:
 
830
                    tx.textOut(T[:-1])
 
831
                    self.endLine(wrapped=1)
 
832
                    self.beginLine(wrapped=1)
 
833
                    x = tx.getX()
 
834
                    text = text[i-1:]
 
835
                    M = len(text)
 
836
                    i = 0
 
837
 
 
838
                i = i + 1
 
839
 
 
840
                if i > M:
 
841
                    break
 
842
 
 
843
            tx.textOut(T)
 
844
 
 
845
        else:
 
846
            t.textOut(text)
 
847
 
 
848
 
 
849
    def putLongText(self, text):
 
850
        "Add a long text by gracefully splitting it into chunks."
 
851
 
 
852
        # Splitting is currently done only at blanks, but other
 
853
        # characters such as '.' or braces are also good
 
854
        # possibilities... later...
 
855
 
 
856
        o = self.options
 
857
        tm, bm, lm, rm = self.frame
 
858
 
 
859
        width = self.canvas.stringWidth
 
860
        fn, fs = self.currFont
 
861
        tw = width(text, fn, fs)
 
862
 
 
863
        arr = string.split(text, ' ')
 
864
 
 
865
        for i in range(len(arr)):
 
866
            a = arr[i]
 
867
            t = self.text # Can change during the loop...
 
868
            tw = width(a, fn, fs)
 
869
            x = t.getX()
 
870
 
 
871
            # If current item does not fit on current line, have it
 
872
            # split and then put before/after a line (maybe also
 
873
            # page) break.
 
874
            if x + tw > rm:
 
875
                self.putSplitLongText(a)
 
876
                t = self.text # Can change after a page break...
 
877
 
 
878
            # If it fits, just add it to the current text object.
 
879
            else:
 
880
                t.textOut(a)
 
881
 
 
882
            # Add the character we used to split th original text.
 
883
            if i < len(arr) - 1:
 
884
                t.textOut(' ')
 
885
 
 
886
 
 
887
    def putText(self, text):
 
888
        "Add some text to the current line."
 
889
 
 
890
        t = self.text
 
891
        x = t.getX()
 
892
        o = self.options
 
893
        fn, fs = o.fontName, o.fontSize
 
894
        tw = self.canvas.stringWidth(text, fn, fs)
 
895
        rm = self.frame[3]
 
896
 
 
897
        if x + tw < rm:
 
898
            t.textOut(text)
 
899
        else:
 
900
            self.putLongText(text)
 
901
 
 
902
 
 
903
    # Not yet tested.
 
904
    def putLine(self, text):
 
905
        "Add a line to the current text."
 
906
 
 
907
        self.putText(text)
 
908
        self.endLine()
 
909
 
 
910
 
 
911
    def putPageDecoration(self):
 
912
        "Draw some decoration on each page."
 
913
 
 
914
        # Use some abbreviations.
 
915
        o = self.options
 
916
        c = self.canvas
 
917
        tm, bm, lm, rm = self.frame
 
918
 
 
919
        # Restore default font.
 
920
        c.setFont(o.fontName, o.fontSize)
 
921
 
 
922
        c.setLineWidth(0.5) # in pt.
 
923
 
 
924
        # Background color.
 
925
        c.setFillColor(o.bgCol)
 
926
        pf = o.realPaperFormat.size
 
927
        c.rect(0, 0, pf[0], pf[1], stroke=0, fill=1)
 
928
 
 
929
        # Header.
 
930
        c.setFillColorRGB(0, 0, 0)
 
931
        c.line(lm, tm + .5*cm, rm, tm + .5*cm)
 
932
        c.setFont('Times-Italic', 12)
 
933
 
 
934
        if self.pdfPath == sys.stdout:
 
935
            filename = o.title or ' '
 
936
        else:
 
937
            path = os.path.basename(self.srcPath)
 
938
            filename = o.title or path
 
939
 
 
940
        c.drawString(lm, tm + 0.75*cm + 2, filename)
 
941
 
 
942
        # Footer.
 
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)
 
945
 
 
946
        # Box around main frame.
 
947
        # c.rect(lm, bm, rm - lm, tm - bm)
 
948
 
 
949
 
 
950
class PythonPDFLayouter (PDFLayouter):
 
951
    """A class to layout a simple multi-page PDF document.
 
952
    """
 
953
 
 
954
    ### API for adding specific Python entities.
 
955
 
 
956
    def addKw(self, t):
 
957
        "Add a keyword."
 
958
 
 
959
        o = self.options
 
960
 
 
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)
 
965
 
 
966
        self.setFillColorAndFont(o.kwCol, font)
 
967
 
 
968
        # Do bookmarking...
 
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()
 
973
 
 
974
                if pos == self.txm:
 
975
                    self.startPositions = []
 
976
 
 
977
                self.startPos = pos
 
978
 
 
979
                if not hasattr(self, 'startPositions'):
 
980
                    self.startPositions = []
 
981
 
 
982
                if pos not in self.startPositions:
 
983
                    self.startPositions.append(pos)
 
984
 
 
985
                # Memorize certain keywords.
 
986
                self.itemFound = t
 
987
 
 
988
            else:
 
989
                self.itemFound = None
 
990
                self.startPos = None
 
991
 
 
992
        self.putText(t)
 
993
 
 
994
 
 
995
    def addIdent(self, t):
 
996
        "Add an identifier."
 
997
 
 
998
        o = self.options
 
999
 
 
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)
 
1004
 
 
1005
        self.setFillColorAndFont(o.identCol, font)
 
1006
        self.putText(t)
 
1007
 
 
1008
        # Bookmark certain identifiers (class and function names).
 
1009
        if not o.noOutline and not o.multiPage:
 
1010
            item = self.itemFound
 
1011
            if item:
 
1012
                # Add line height to current vert. position.
 
1013
                pos = self.text.getY() + o.fontSize
 
1014
 
 
1015
                nameTag = "p%sy%s" % (self.pageNum, pos)
 
1016
                c = self.canvas
 
1017
                i = self.startPositions.index(self.startPos)
 
1018
                c.bookmarkHorizontalAbsolute(nameTag, pos)
 
1019
                c.addOutlineEntry('%s %s' % (item, t), nameTag, i)
 
1020
 
 
1021
 
 
1022
    def addParam(self, t):
 
1023
        "Add a parameter."
 
1024
 
 
1025
        o = self.options
 
1026
        font = (o.fontName, o.fontSize)
 
1027
        self.setFillColorAndFont(o.paramCol, font)
 
1028
        self.text.putText(t)
 
1029
 
 
1030
 
 
1031
    def addSimpleString(self, t):
 
1032
        "Add a simple string."
 
1033
 
 
1034
        o = self.options
 
1035
        font = (o.fontName, o.fontSize)
 
1036
        self.setFillColorAndFont(o.strngCol, font)
 
1037
        self.putText(t)
 
1038
 
 
1039
 
 
1040
    def addTripleStringBegin(self):
 
1041
        "Memorize begin of a multi-line string."
 
1042
 
 
1043
        # Memorise that we started a multi-line string.
 
1044
        self.multiLineStringStarted = 1
 
1045
 
 
1046
 
 
1047
    def addTripleStringEnd(self, t):
 
1048
        "Add a multi-line string."
 
1049
 
 
1050
        self.putText(t)
 
1051
 
 
1052
        # Forget about the multi-line string again.
 
1053
        self.multiLineStringStarted = 0
 
1054
 
 
1055
 
 
1056
    def addComm(self, t):
 
1057
        "Add a comment."
 
1058
 
 
1059
        o = self.options
 
1060
 
 
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)
 
1065
 
 
1066
        self.setFillColorAndFont(o.commCol, font)
 
1067
        self.putText(t)
 
1068
 
 
1069
 
 
1070
    def addRest(self, line, eol):
 
1071
        "Add a regular thing."
 
1072
 
 
1073
        o = self.options
 
1074
 
 
1075
        # Nothing else to be done, print line as-is...
 
1076
        if line:
 
1077
            font = (o.fontName, o.fontSize)
 
1078
            self.setFillColorAndFont(o.restCol, font)
 
1079
 
 
1080
            # ... except if the multi-line-string flag is set, then we
 
1081
            # decide to change the current color to that of strings and
 
1082
            # just go on.
 
1083
            if self.multiLineStringStarted:
 
1084
                self.setFillColorAndFont(o.strngCol, font)
 
1085
 
 
1086
            self.putText(line)
 
1087
 
 
1088
        # Print an empty line.
 
1089
        else:
 
1090
            if eol != -1:
 
1091
                self.putText('')
 
1092
 
 
1093
    ### End of API.
 
1094
 
 
1095
 
 
1096
class EmptyPythonPDFLayouter (PythonPDFLayouter):
 
1097
    """A PDF layout with no decoration and no margins.
 
1098
 
 
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.
 
1104
    """
 
1105
 
 
1106
    def setMainFrame(self, frame=None):
 
1107
        "Make a frame extending to all paper edges."
 
1108
 
 
1109
        width, height = self.options.realPaperFormat.size
 
1110
        self.frame = height, 0, 0, width
 
1111
 
 
1112
 
 
1113
    def putPageDecoration(self):
 
1114
        "Draw no decoration at all."
 
1115
 
 
1116
        pass
 
1117
 
 
1118
 
 
1119
### Pretty-printing classes.
 
1120
 
 
1121
class PDFPrinter:
 
1122
    """Generic PDF Printer class.
 
1123
 
 
1124
    Does not do much, but write a PDF file created from
 
1125
    any ASCII input file.
 
1126
    """
 
1127
 
 
1128
    outFileExt = '.pdf'
 
1129
 
 
1130
 
 
1131
    def __init__(self, options=None):
 
1132
        "Initialisation."
 
1133
 
 
1134
        self.data = None     # Contains the input file.
 
1135
        self.inPath = None   # Path of input file.
 
1136
        self.Layouter = PDFLayouter
 
1137
 
 
1138
        if type(options) != type(None):
 
1139
            self.options = options
 
1140
        else:
 
1141
            self.options = Options()
 
1142
 
 
1143
 
 
1144
    ### I/O.
 
1145
 
 
1146
    def readFile(self, path):
 
1147
        "Read the content of a file."
 
1148
 
 
1149
        if path == sys.stdin:
 
1150
            f = path
 
1151
        else:
 
1152
            f = open(path)
 
1153
 
 
1154
        self.inPath = path
 
1155
 
 
1156
        data = f.read()
 
1157
        o = self.options
 
1158
        self.data = re.sub('\t', ' '*o.tabSize, data)
 
1159
        f.close()
 
1160
 
 
1161
 
 
1162
    def formatLine(self, line, eol=0):
 
1163
        "Format one line of Python source code."
 
1164
 
 
1165
        font = ('Courier', 8)
 
1166
        self.layouter.setFillColorAndFont(Color(0, 0, 0), font)
 
1167
        self.layouter.putText(line)
 
1168
 
 
1169
 
 
1170
    def writeData(self, srcCodeLines, inPath, outPath=None):
 
1171
        "Convert Python source code lines into a PDF document."
 
1172
 
 
1173
        # Create a layouter object.
 
1174
        self.layouter = self.Layouter(self.options)
 
1175
        l = self.layouter
 
1176
 
 
1177
        # Loop over all tagged source lines, dissect them into
 
1178
        # Python entities ourself and let the layouter do the
 
1179
        # rendering.
 
1180
        splitCodeLines = string.split(srcCodeLines, '\n')
 
1181
 
 
1182
        ### Must also handle the case of outPath being sys.stdout!!
 
1183
        l.begin(inPath, len(splitCodeLines))
 
1184
        l.beginDocument()
 
1185
 
 
1186
        for line in splitCodeLines:
 
1187
            l.beginLine()
 
1188
            self.formatLine(line)
 
1189
            l.endLine()
 
1190
 
 
1191
        l.endDocument()
 
1192
        l.end()
 
1193
 
 
1194
 
 
1195
    def writeFile(self, data, inPath=None, outPath=None):
 
1196
        "Write some data into a file."
 
1197
 
 
1198
        if inPath == sys.stdin:
 
1199
            self.outPath = sys.stdout
 
1200
        else:
 
1201
            if not outPath:
 
1202
                path = os.path.splitext(self.inPath)[0]
 
1203
                self.outPath = path + self.outFileExt
 
1204
 
 
1205
        self.writeData(data, inPath, outPath or self.outPath)
 
1206
 
 
1207
 
 
1208
    def process(self, inPath, outPath=None):
 
1209
        "The real 'action point' for working with Pretty-Printers."
 
1210
 
 
1211
        self.readFile(inPath)
 
1212
        self.writeFile(self.data, inPath, outPath)
 
1213
 
 
1214
 
 
1215
class PythonPDFPrinter (PDFPrinter):
 
1216
    """A class to nicely format tagged Python source code.
 
1217
 
 
1218
    """
 
1219
 
 
1220
    comm  = 'COMMENT'
 
1221
    kw    = 'KEYWORD'
 
1222
    strng = 'STRING'
 
1223
    ident = 'IDENT'
 
1224
    param = 'PARAMETER'
 
1225
 
 
1226
    outFileExt = '.pdf'
 
1227
 
 
1228
 
 
1229
    def __init__(self, options=None):
 
1230
        "Initialisation, calling self._didInit() at the end."
 
1231
 
 
1232
        if type(options) != type(None):
 
1233
            self.options = options
 
1234
        else:
 
1235
            self.options = Options()
 
1236
 
 
1237
        self._didInit()
 
1238
 
 
1239
 
 
1240
    def _didInit(self):
 
1241
        "Post-Initialising"
 
1242
 
 
1243
        # Define regular expression patterns.
 
1244
        s = self
 
1245
        comp = re.compile
 
1246
 
 
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('(.*)')
 
1255
 
 
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
 
1264
 
 
1265
        self.matchList = ((cMatch, 'Comm'),
 
1266
            (kMatch, 'Kw'),
 
1267
            (iMatch, 'Ident'),
 
1268
            (pMatch, 'Param'),
 
1269
            (sMatch, 'SimpleString'),
 
1270
            (s1Match, 'TripleStringBegin'),
 
1271
            (s2Match, 'TripleStringEnd'),
 
1272
            (aMatch, 'Rest'))
 
1273
 
 
1274
        self.Layouter = PythonPDFLayouter
 
1275
 
 
1276
        # Load fontifier.
 
1277
        self.tagFunc = loadFontifier(self.options)
 
1278
 
 
1279
 
 
1280
    ###
 
1281
 
 
1282
    def formatLine(self, line, eol=0):
 
1283
        "Format one line of Python source code."
 
1284
 
 
1285
        # Values for eol: -1:no-eol, 0:dunno-yet, 1:do-eol.
 
1286
 
 
1287
        for match, meth in self.matchList:
 
1288
            res = match(line)
 
1289
 
 
1290
            if res:
 
1291
                groups = res.groups()
 
1292
                method = getattr(self, '_format%s' % meth)
 
1293
                method(groups, eol)
 
1294
                break
 
1295
 
 
1296
 
 
1297
    def _formatIdent(self, groups, eol):
 
1298
        "Format a Python identifier."
 
1299
 
 
1300
        before, id, after = groups
 
1301
        self.formatLine(before, -1)
 
1302
        self.layouter.addIdent(id)
 
1303
        self.formatLine(after, eol)
 
1304
 
 
1305
 
 
1306
    def _formatParam(self, groups, eol):
 
1307
        "Format a Python parameter."
 
1308
 
 
1309
        before, param, after = groups
 
1310
        self.formatLine(before, -1)
 
1311
        self.layouter.addParam(before)
 
1312
        self.formatLine(after, eol)
 
1313
 
 
1314
 
 
1315
    def _formatSimpleString(self, groups, eol):
 
1316
        "Format a Python one-line string."
 
1317
 
 
1318
        before, s, after = groups
 
1319
        self.formatLine(before, -1)
 
1320
        self.layouter.addSimpleString(s)
 
1321
        self.formatLine(after, eol)
 
1322
 
 
1323
 
 
1324
    def _formatTripleStringBegin(self, groups, eol):
 
1325
        "Format a Python multi-line line string (1)."
 
1326
 
 
1327
        before, after = groups
 
1328
        self.formatLine(before, -1)
 
1329
        self.layouter.addTripleStringBegin()
 
1330
        self.formatLine(after, 1)
 
1331
 
 
1332
 
 
1333
    def _formatTripleStringEnd(self, groups, eol):
 
1334
        "Format a Python multi-line line string (2)."
 
1335
 
 
1336
        before, after = groups
 
1337
        self.layouter.addTripleStringEnd(before)
 
1338
        self.formatLine(after, 1)
 
1339
 
 
1340
 
 
1341
    def _formatKw(self, groups, eol):
 
1342
        "Format a Python keyword."
 
1343
 
 
1344
        before, kw, after = groups
 
1345
        self.formatLine(before, -1)
 
1346
        self.layouter.addKw(kw)
 
1347
        self.formatLine(after, eol)
 
1348
 
 
1349
 
 
1350
    def _formatComm(self, groups, eol):
 
1351
        "Format a Python comment."
 
1352
 
 
1353
        before, comment, after = groups
 
1354
        self.formatLine(before, -1)
 
1355
        self.layouter.addComm(comment)
 
1356
        self.formatLine(after, 1)
 
1357
 
 
1358
 
 
1359
    def _formatRest(self, groups, eol):
 
1360
        "Format a piece of a Python line w/o anything special."
 
1361
 
 
1362
        line = groups[0]
 
1363
        self.layouter.addRest(line, eol)
 
1364
 
 
1365
 
 
1366
    ###
 
1367
 
 
1368
    def writeData(self, srcCodeLines, inPath, outPath=None):
 
1369
        "Convert Python source code lines into a PDF document."
 
1370
 
 
1371
        # Create a layouter object.
 
1372
        self.layouter = self.Layouter(self.options)
 
1373
        l = self.layouter
 
1374
 
 
1375
        # Loop over all tagged source lines, dissect them into
 
1376
        # Python entities ourself and let the layouter do the
 
1377
        # rendering.
 
1378
        splitCodeLines = string.split(srcCodeLines, '\n')
 
1379
        l.begin(inPath, len(splitCodeLines))
 
1380
        l.beginDocument()
 
1381
 
 
1382
        for line in splitCodeLines:
 
1383
            l.beginLine()
 
1384
            self.formatLine(line)
 
1385
            l.endLine()
 
1386
 
 
1387
        l.endDocument()
 
1388
        l.end()
 
1389
 
 
1390
 
 
1391
    def process(self, inPath, outPath=None):
 
1392
        "The real 'action point' for working with Pretty-Printers."
 
1393
 
 
1394
        self.readFile(inPath)
 
1395
        self.taggedData = self._fontify(self.data)
 
1396
        self.writeFile(self.taggedData, inPath, outPath)
 
1397
 
 
1398
 
 
1399
    ### Fontifying.
 
1400
 
 
1401
    def _fontify(self, pytext):
 
1402
        ""
 
1403
 
 
1404
        formats = {
 
1405
            'rest'       : ('', ''),
 
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) }
 
1411
 
 
1412
        # Parse.
 
1413
        taglist = self.tagFunc(pytext)
 
1414
 
 
1415
        # Prepend special 'rest' tag.
 
1416
        taglist[:0] = [('rest', 0, len(pytext), None)]
 
1417
 
 
1418
        # Prepare splitting.
 
1419
        splits = []
 
1420
        self._addSplits(splits, pytext, formats, taglist)
 
1421
 
 
1422
        # Do splitting & inserting.
 
1423
        splits.sort()
 
1424
        l = []
 
1425
        li = 0
 
1426
 
 
1427
        for ri, dummy, insert in splits:
 
1428
            if ri > li:
 
1429
                l.append(pytext[li:ri])
 
1430
 
 
1431
            l.append(insert)
 
1432
            li = ri
 
1433
 
 
1434
        if li < len(pytext):
 
1435
            l.append(pytext[li:])
 
1436
 
 
1437
        return string.join(l, '')
 
1438
 
 
1439
 
 
1440
    def _addSplits(self, splits, text, formats, taglist):
 
1441
        ""
 
1442
 
 
1443
        # Helper for fontify().
 
1444
        for id, left, right, sublist in taglist:
 
1445
 
 
1446
            try:
 
1447
                pre, post = formats[id]
 
1448
            except KeyError:
 
1449
                # msg = 'Warning: no format '
 
1450
                # msg = msg + 'for %s specified\n'%repr(id)
 
1451
                # sys.stderr.write(msg)
 
1452
                pre, post = '', ''
 
1453
 
 
1454
            if type(pre) != type(''):
 
1455
                pre = pre(text[left:right])
 
1456
 
 
1457
            if type(post) != type(''):
 
1458
                post = post(text[left:right])
 
1459
 
 
1460
            # len(splits) is a dummy used to make sorting stable.
 
1461
            splits.append((left, len(splits), pre))
 
1462
 
 
1463
            if sublist:
 
1464
                self._addSplits(splits, text, formats, sublist)
 
1465
 
 
1466
            splits.append((right, len(splits), post))
 
1467
 
 
1468
 
 
1469
### Main
 
1470
 
 
1471
def main(cmdline):
 
1472
    "Process command line as if it were sys.argv"
 
1473
 
 
1474
    # Create default options and initialize with argv
 
1475
    # from the command line.
 
1476
    options = Options()
 
1477
    options.updateWithContentsOfArgv(cmdline[1:])
 
1478
 
 
1479
    # Print help message if desired, then exit.
 
1480
    if options.h or options.help:
 
1481
        print __doc__
 
1482
        sys.exit()
 
1483
 
 
1484
    # Apply modest consistency checks and exit if needed.
 
1485
    cmdStr = string.join(cmdline, ' ')
 
1486
    find = string.find
 
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
 
1491
 
 
1492
    # Create PDF converter and pass options to it.
 
1493
    if options.input:
 
1494
        input = string.lower(options.input)
 
1495
 
 
1496
        if input == 'python':
 
1497
            P = PythonPDFPrinter
 
1498
        elif input == 'ascii':
 
1499
            P = PDFPrinter
 
1500
        else:
 
1501
            details = "Input file type must be 'python' or 'ascii'."
 
1502
            raise 'ValueError', details
 
1503
 
 
1504
    else:
 
1505
        P = PythonPDFPrinter
 
1506
 
 
1507
    p = P(options)
 
1508
 
 
1509
    # Display options if needed.
 
1510
    if options.v or options.verbose:
 
1511
        pass # p.options.display()
 
1512
 
 
1513
    # Start working.
 
1514
    verbose = options.v or options.verbose
 
1515
 
 
1516
    if options.stdout:
 
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)
 
1521
    else:
 
1522
        if verbose:
 
1523
            print 'py2pdf: working on:'
 
1524
 
 
1525
        for f in options.files:
 
1526
            try:
 
1527
                if verbose:
 
1528
                    print '  %s' % f
 
1529
                if f != '-':
 
1530
                    p.process(f)
 
1531
                else:
 
1532
                    p.process(sys.stdin, sys.stdout)
 
1533
            except IOError:
 
1534
                if verbose:
 
1535
                    print '(IOError!)',
 
1536
 
 
1537
        if verbose:
 
1538
            print
 
1539
            print 'Done.'
 
1540
 
 
1541
 
 
1542
def process(*args, **kwargs):
 
1543
    "Provides a way of using py2pdf from within other Python scripts."
 
1544
 
 
1545
    noValOpts = 'h v verbose landscape stdout lineNum marcs help multiPage noOutline'
 
1546
    noValOpts = string.split(noValOpts, ' ')
 
1547
 
 
1548
    s = 'py2pdf.py'
 
1549
    for k, v in kwargs.items():
 
1550
        if len(k) == 1:
 
1551
            s = s + ' -%s' % k
 
1552
            if k not in noValOpts:
 
1553
                s = s + ' %s' % v
 
1554
        elif len(k) > 1:
 
1555
            s = s + ' --%s' % k
 
1556
            if k not in noValOpts:
 
1557
                s = s + '=%s' % v
 
1558
    s = s + ' ' + string.join(args, ' ')
 
1559
    s = string.split(s, ' ')
 
1560
 
 
1561
    main(s)
 
1562
 
 
1563
 
 
1564
###
 
1565
 
 
1566
if __name__=='__main__': #NORUNTESTS
 
1567
    main(sys.argv)
 
 
b'\\ No newline at end of file'