~syleam/openobject-server/exit-properly

« back to all changes in this revision

Viewing changes to bin/reportlab/platypus/doctemplate.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
#Copyright ReportLab Europe Ltd. 2000-2004
 
2
#see license.txt for license details
 
3
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/doctemplate.py
 
4
 
 
5
__version__=''' $Id$ '''
 
6
 
 
7
__doc__="""
 
8
This module contains the core structure of platypus.
 
9
 
 
10
Platypus constructs documents.  Document styles are determined by DocumentTemplates.
 
11
 
 
12
Each DocumentTemplate contains one or more PageTemplates which defines the look of the
 
13
pages of the document.
 
14
 
 
15
Each PageTemplate has a procedure for drawing the "non-flowing" part of the page
 
16
(for example the header, footer, page number, fixed logo graphic, watermark, etcetera) and
 
17
a set of Frames which enclose the flowing part of the page (for example the paragraphs,
 
18
tables, or non-fixed diagrams of the text).
 
19
 
 
20
A document is built when a DocumentTemplate is fed a sequence of Flowables.
 
21
The action of the build consumes the flowables in order and places them onto
 
22
frames on pages as space allows.  When a frame runs out of space the next frame
 
23
of the page is used.  If no frame remains a new page is created.  A new page
 
24
can also be created if a page break is forced.
 
25
 
 
26
The special invisible flowable NextPageTemplate can be used to specify
 
27
the page template for the next page (which by default is the one being used
 
28
for the current frame).
 
29
"""
 
30
 
 
31
from reportlab.platypus.flowables import *
 
32
from reportlab.platypus.paragraph import Paragraph
 
33
from reportlab.platypus.frames import Frame
 
34
from reportlab.rl_config import defaultPageSize, verbose
 
35
import reportlab.lib.sequencer
 
36
 
 
37
from types import *
 
38
import sys
 
39
 
 
40
class LayoutError(Exception):
 
41
    pass
 
42
 
 
43
def _doNothing(canvas, doc):
 
44
    "Dummy callback for onPage"
 
45
    pass
 
46
 
 
47
 
 
48
class IndexingFlowable(Flowable):
 
49
    """Abstract interface definition for flowables which might
 
50
    hold references to other pages or themselves be targets
 
51
    of cross-references.  XRefStart, XRefDest, Table of Contents,
 
52
    Indexes etc."""
 
53
    def isIndexing(self):
 
54
        return 1
 
55
 
 
56
    def isSatisfied(self):
 
57
        return 1
 
58
 
 
59
    def notify(self, kind, stuff):
 
60
        """This will be called by the framework wherever 'stuff' happens.
 
61
        'kind' will be a value that can be used to decide whether to
 
62
        pay attention or not."""
 
63
        pass
 
64
 
 
65
    def beforeBuild(self):
 
66
        """Called by multiBuild before it starts; use this to clear
 
67
        old contents"""
 
68
        pass
 
69
 
 
70
    def afterBuild(self):
 
71
        """Called after build ends but before isSatisfied"""
 
72
        pass
 
73
 
 
74
 
 
75
class ActionFlowable(Flowable):
 
76
    '''This Flowable is never drawn, it can be used for data driven controls
 
77
       For example to change a page template (from one column to two, for example)
 
78
       use NextPageTemplate which creates an ActionFlowable.
 
79
    '''
 
80
    def __init__(self,action=()):
 
81
        if type(action) not in (ListType, TupleType):
 
82
            action = (action,)
 
83
        self.action = tuple(action)
 
84
 
 
85
    def wrap(self, availWidth, availHeight):
 
86
        '''Should never be called.'''
 
87
        raise NotImplementedError
 
88
 
 
89
    def draw(self):
 
90
        '''Should never be called.'''
 
91
        raise NotImplementedError
 
92
 
 
93
    def apply(self,doc):
 
94
        '''
 
95
        This is called by the doc.build processing to allow the instance to
 
96
        implement its behaviour
 
97
        '''
 
98
        action = self.action[0]
 
99
        args = tuple(self.action[1:])
 
100
        arn = 'handle_'+action
 
101
        try:
 
102
            apply(getattr(doc,arn), args)
 
103
        except AttributeError, aerr:
 
104
            if aerr.args[0]==arn:
 
105
                raise NotImplementedError, "Can't handle ActionFlowable(%s)" % action
 
106
            else:
 
107
                raise
 
108
        except "bogus":
 
109
            t, v, unused = sys.exc_info()
 
110
            raise t, "%s\n   handle_%s args=%s"%(v,action,args)
 
111
 
 
112
    def __call__(self):
 
113
        return self
 
114
 
 
115
    def identity(self, maxLen=None):
 
116
        return "ActionFlowable: %s" % str(self.action)
 
117
 
 
118
class NextFrameFlowable(ActionFlowable):
 
119
    def __init__(self,ix,resume=0):
 
120
        ActionFlowable.__init__(self,('nextFrame',ix,resume))
 
121
 
 
122
class CurrentFrameFlowable(ActionFlowable):
 
123
    def __init__(self,ix,resume=0):
 
124
        ActionFlowable.__init__(self,('currentFrame',ix,resume))
 
125
 
 
126
class _FrameBreak(ActionFlowable):
 
127
    '''
 
128
    A special ActionFlowable that allows setting doc._nextFrameIndex
 
129
 
 
130
    eg story.append(FrameBreak('mySpecialFrame'))
 
131
    '''
 
132
    def __call__(self,ix=None,resume=0):
 
133
        r = self.__class__(self.action+(resume,))
 
134
        r._ix = ix
 
135
        return r
 
136
 
 
137
    def apply(self,doc):
 
138
        if getattr(self,'_ix',None): doc._nextFrameIndex = self._ix
 
139
        ActionFlowable.apply(self,doc)
 
140
 
 
141
FrameBreak = _FrameBreak('frameEnd')
 
142
PageBegin = ActionFlowable('pageBegin')
 
143
 
 
144
def _evalMeasurement(n):
 
145
    if type(n) is type(''):
 
146
        from paraparser import _num
 
147
        n = _num(n)
 
148
        if type(n) is type(()): n = n[1]
 
149
    return n
 
150
 
 
151
class Indenter(ActionFlowable):
 
152
    """Increases or decreases left and right margins of frame.
 
153
 
 
154
    This allows one to have a 'context-sensitive' indentation
 
155
    and makes nested lists way easier.
 
156
    """
 
157
 
 
158
    def __init__(self, left=0, right=0):
 
159
        self.left = _evalMeasurement(left)
 
160
        self.right = _evalMeasurement(right)
 
161
 
 
162
    def apply(self, doc):
 
163
        doc.frame._leftExtraIndent = doc.frame._leftExtraIndent + self.left
 
164
        doc.frame._rightExtraIndent = doc.frame._rightExtraIndent + self.right
 
165
 
 
166
 
 
167
class NextPageTemplate(ActionFlowable):
 
168
    """When you get to the next page, use the template specified (change to two column, for example)  """
 
169
    def __init__(self,pt):
 
170
        ActionFlowable.__init__(self,('nextPageTemplate',pt))
 
171
 
 
172
 
 
173
class PageTemplate:
 
174
    """
 
175
    essentially a list of Frames and an onPage routine to call at the start
 
176
    of a page when this is selected. onPageEnd gets called at the end.
 
177
    derived classes can also implement beforeDrawPage and afterDrawPage if they want
 
178
    """
 
179
    def __init__(self,id=None,frames=[],onPage=_doNothing, onPageEnd=_doNothing,
 
180
                 pagesize=None):
 
181
        if type(frames) not in (ListType,TupleType): frames = [frames]
 
182
        assert filter(lambda x: not isinstance(x,Frame), frames)==[], "frames argument error"
 
183
        self.id = id
 
184
        self.frames = frames
 
185
        self.onPage = onPage
 
186
        self.onPageEnd = onPageEnd
 
187
        self.pagesize = pagesize
 
188
 
 
189
    def beforeDrawPage(self,canv,doc):
 
190
        """Override this if you want additional functionality or prefer
 
191
        a class based page routine.  Called before any flowables for
 
192
        this page are processed."""
 
193
        pass
 
194
 
 
195
    def checkPageSize(self,canv,doc):
 
196
        '''This gets called by the template framework
 
197
        If canv size != template size then the canv size is set to
 
198
        the template size or if that's not available to the
 
199
        doc size.
 
200
        '''
 
201
        #### NEVER EVER EVER COMPARE FLOATS FOR EQUALITY
 
202
        #RGB converting pagesizes to ints means we are accurate to one point
 
203
        #RGB I suggest we should be aiming a little better
 
204
        cp = None
 
205
        dp = None
 
206
        sp = None
 
207
        if canv._pagesize: cp = map(int, canv._pagesize)
 
208
        if self.pagesize: sp = map(int, self.pagesize)
 
209
        if doc.pagesize: dp = map(int, doc.pagesize)
 
210
        if cp!=sp:
 
211
            if sp:
 
212
                canv.setPageSize(self.pagesize)
 
213
            elif cp!=dp:
 
214
                canv.setPageSize(doc.pagesize)
 
215
 
 
216
    def afterDrawPage(self, canv, doc):
 
217
        """This is called after the last flowable for the page has
 
218
        been processed.  You might use this if the page header or
 
219
        footer needed knowledge of what flowables were drawn on
 
220
        this page."""
 
221
        pass
 
222
 
 
223
 
 
224
class BaseDocTemplate:
 
225
    """
 
226
    First attempt at defining a document template class.
 
227
 
 
228
    The basic idea is simple.
 
229
    0)  The document has a list of data associated with it
 
230
        this data should derive from flowables. We'll have
 
231
        special classes like PageBreak, FrameBreak to do things
 
232
        like forcing a page end etc.
 
233
 
 
234
    1)  The document has one or more page templates.
 
235
 
 
236
    2)  Each page template has one or more frames.
 
237
 
 
238
    3)  The document class provides base methods for handling the
 
239
        story events and some reasonable methods for getting the
 
240
        story flowables into the frames.
 
241
 
 
242
    4)  The document instances can override the base handler routines.
 
243
 
 
244
    Most of the methods for this class are not called directly by the user,
 
245
    but in some advanced usages they may need to be overridden via subclassing.
 
246
 
 
247
    EXCEPTION: doctemplate.build(...) must be called for most reasonable uses
 
248
    since it builds a document using the page template.
 
249
 
 
250
    Each document template builds exactly one document into a file specified
 
251
    by the filename argument on initialization.
 
252
 
 
253
    Possible keyword arguments for the initialization:
 
254
 
 
255
    pageTemplates: A list of templates.  Must be nonempty.  Names
 
256
      assigned to the templates are used for referring to them so no two used
 
257
      templates should have the same name.  For example you might want one template
 
258
      for a title page, one for a section first page, one for a first page of
 
259
      a chapter and two more for the interior of a chapter on odd and even pages.
 
260
      If this argument is omitted then at least one pageTemplate should be provided
 
261
      using the addPageTemplates method before the document is built.
 
262
    pageSize: a 2-tuple or a size constant from reportlab/lib/pagesizes.pu.
 
263
     Used by the SimpleDocTemplate subclass which does NOT accept a list of
 
264
     pageTemplates but makes one for you; ignored when using pageTemplates.
 
265
 
 
266
    showBoundary: if set draw a box around the frame boundaries.
 
267
    leftMargin:
 
268
    rightMargin:
 
269
    topMargin:
 
270
    bottomMargin:  Margin sizes in points (default 1 inch)
 
271
      These margins may be overridden by the pageTemplates.  They are primarily of interest
 
272
      for the SimpleDocumentTemplate subclass.
 
273
    allowSplitting:  If set flowables (eg, paragraphs) may be split across frames or pages
 
274
      (default: 1)
 
275
    title: Internal title for document (does not automatically display on any page)
 
276
    author: Internal author for document (does not automatically display on any page)
 
277
    """
 
278
    _initArgs = {   'pagesize':defaultPageSize,
 
279
                    'pageTemplates':[],
 
280
                    'showBoundary':0,
 
281
                    'leftMargin':inch,
 
282
                    'rightMargin':inch,
 
283
                    'topMargin':inch,
 
284
                    'bottomMargin':inch,
 
285
                    'allowSplitting':1,
 
286
                    'title':None,
 
287
                    'author':None,
 
288
                    'invariant':None,
 
289
                    '_pageBreakQuick':1}
 
290
    _invalidInitArgs = ()
 
291
    _firstPageTemplateIndex = 0
 
292
 
 
293
    def __init__(self, filename, **kw):
 
294
        """create a document template bound to a filename (see class documentation for keyword arguments)"""
 
295
        self.filename = filename
 
296
 
 
297
        for k in self._initArgs.keys():
 
298
            if not kw.has_key(k):
 
299
                v = self._initArgs[k]
 
300
            else:
 
301
                if k in self._invalidInitArgs:
 
302
                    raise ValueError, "Invalid argument %s" % k
 
303
                v = kw[k]
 
304
            setattr(self,k,v)
 
305
        #print "pagesize is", self.pagesize
 
306
 
 
307
        p = self.pageTemplates
 
308
        self.pageTemplates = []
 
309
        self.addPageTemplates(p)
 
310
 
 
311
        # facility to assist multi-build and cross-referencing.
 
312
        # various hooks can put things into here - key is what
 
313
        # you want, value is a page number.  This can then be
 
314
        # passed to indexing flowables.
 
315
        self._pageRefs = {}
 
316
        self._indexingFlowables = []
 
317
 
 
318
 
 
319
        #callback facility for progress monitoring
 
320
        self._onPage = None
 
321
        self._onProgress = None
 
322
        self._flowableCount = 0  # so we know how far to go
 
323
 
 
324
 
 
325
        #infinite loop detection if we start doing lots of empty pages
 
326
        self._curPageFlowableCount = 0
 
327
        self._emptyPages = 0
 
328
        self._emptyPagesAllowed = 10
 
329
 
 
330
        #context sensitive margins - set by story, not from outside
 
331
        self._leftExtraIndent = 0.0
 
332
        self._rightExtraIndent = 0.0
 
333
 
 
334
        self._calc()
 
335
        self.afterInit()
 
336
 
 
337
    def _calc(self):
 
338
        self._rightMargin = self.pagesize[0] - self.rightMargin
 
339
        self._topMargin = self.pagesize[1] - self.topMargin
 
340
        self.width = self._rightMargin - self.leftMargin
 
341
        self.height = self._topMargin - self.bottomMargin
 
342
 
 
343
    def setPageCallBack(self, func):
 
344
        'Simple progress monitor - func(pageNo) called on each new page'
 
345
        self._onPage = func
 
346
 
 
347
    def setProgressCallBack(self, func):
 
348
        '''Cleverer progress monitor - func(typ, value) called regularly'''
 
349
        self._onProgress = func
 
350
 
 
351
    def clean_hanging(self):
 
352
        'handle internal postponed actions'
 
353
        while len(self._hanging):
 
354
            self.handle_flowable(self._hanging)
 
355
 
 
356
    def addPageTemplates(self,pageTemplates):
 
357
        'add one or a sequence of pageTemplates'
 
358
        if type(pageTemplates) not in (ListType,TupleType):
 
359
            pageTemplates = [pageTemplates]
 
360
        #this test below fails due to inconsistent imports!
 
361
        #assert filter(lambda x: not isinstance(x,PageTemplate), pageTemplates)==[], "pageTemplates argument error"
 
362
        for t in pageTemplates:
 
363
            self.pageTemplates.append(t)
 
364
 
 
365
    def handle_documentBegin(self):
 
366
        '''implement actions at beginning of document'''
 
367
        self._hanging = [PageBegin]
 
368
        self.pageTemplate = self.pageTemplates[self._firstPageTemplateIndex]
 
369
        self.page = 0
 
370
        self.beforeDocument()
 
371
 
 
372
    def handle_pageBegin(self):
 
373
        '''Perform actions required at beginning of page.
 
374
        shouldn't normally be called directly'''
 
375
        self.page = self.page + 1
 
376
        self.pageTemplate.beforeDrawPage(self.canv,self)
 
377
        self.pageTemplate.checkPageSize(self.canv,self)
 
378
        self.pageTemplate.onPage(self.canv,self)
 
379
        for f in self.pageTemplate.frames: f._reset()
 
380
        self.beforePage()
 
381
        #keep a count of flowables added to this page.  zero indicates bad stuff
 
382
        self._curPageFlowableCount = 0
 
383
        if hasattr(self,'_nextFrameIndex'):
 
384
            del self._nextFrameIndex
 
385
        self.frame = self.pageTemplate.frames[0]
 
386
        self.handle_frameBegin()
 
387
 
 
388
    def handle_pageEnd(self):
 
389
        ''' show the current page
 
390
            check the next page template
 
391
            hang a page begin
 
392
        '''
 
393
        #detect infinite loops...
 
394
        if self._curPageFlowableCount == 0:
 
395
            self._emptyPages = self._emptyPages + 1
 
396
        else:
 
397
            self._emptyPages = 0
 
398
        if self._emptyPages >= self._emptyPagesAllowed:
 
399
            if 1:
 
400
                raise LayoutError("More than %d pages generated without content - halting layout.  Likely that a flowable is too large for any frame." % self._emptyPagesAllowed)
 
401
            else:
 
402
                pass    #attempt to restore to good state
 
403
        else:
 
404
            if self._onProgress:
 
405
                self._onProgress('PAGE', self.canv.getPageNumber())
 
406
            self.pageTemplate.afterDrawPage(self.canv, self)
 
407
            self.pageTemplate.onPageEnd(self.canv, self)
 
408
            self.afterPage()
 
409
            self.canv.showPage()
 
410
            if hasattr(self,'_nextPageTemplateIndex'):
 
411
                self.pageTemplate = self.pageTemplates[self._nextPageTemplateIndex]
 
412
                del self._nextPageTemplateIndex
 
413
            if self._emptyPages==0:
 
414
                pass    #store good state here
 
415
        self._hanging.append(PageBegin)
 
416
 
 
417
    def handle_pageBreak(self,slow=None):
 
418
        '''some might choose not to end all the frames'''
 
419
        if self._pageBreakQuick and not slow:
 
420
            self.handle_pageEnd()
 
421
        else:
 
422
            n = len(self._hanging)
 
423
            while len(self._hanging)==n:
 
424
                self.handle_frameEnd()
 
425
 
 
426
    def handle_frameBegin(self,resume=0):
 
427
        '''What to do at the beginning of a frame'''
 
428
        f = self.frame
 
429
        if f._atTop:
 
430
            if self.showBoundary or self.frame.showBoundary:
 
431
                self.frame.drawBoundary(self.canv)
 
432
        f._leftExtraIndent = self._leftExtraIndent
 
433
        f._rightExtraIndent = self._rightExtraIndent
 
434
 
 
435
    def handle_frameEnd(self,resume=0):
 
436
        ''' Handles the semantics of the end of a frame. This includes the selection of
 
437
            the next frame or if this is the last frame then invoke pageEnd.
 
438
        '''
 
439
 
 
440
        self._leftExtraIndent = self.frame._leftExtraIndent
 
441
        self._rightExtraIndent = self.frame._rightExtraIndent
 
442
 
 
443
        if hasattr(self,'_nextFrameIndex'):
 
444
            frame = self.pageTemplate.frames[self._nextFrameIndex]
 
445
            del self._nextFrameIndex
 
446
            self.handle_frameBegin(resume)
 
447
        elif hasattr(self.frame,'lastFrame') or self.frame is self.pageTemplate.frames[-1]:
 
448
            self.handle_pageEnd()
 
449
            self.frame = None
 
450
        else:
 
451
            f = self.frame
 
452
            self.frame = self.pageTemplate.frames[self.pageTemplate.frames.index(f) + 1]
 
453
            self.handle_frameBegin()
 
454
 
 
455
    def handle_nextPageTemplate(self,pt):
 
456
        '''On endPage chenge to the page template with name or index pt'''
 
457
        if type(pt) is StringType:
 
458
            for t in self.pageTemplates:
 
459
                if t.id == pt:
 
460
                    self._nextPageTemplateIndex = self.pageTemplates.index(t)
 
461
                    return
 
462
            raise ValueError, "can't find template('%s')"%pt
 
463
        elif type(pt) is IntType:
 
464
            self._nextPageTemplateIndex = pt
 
465
        else:
 
466
            raise TypeError, "argument pt should be string or integer"
 
467
 
 
468
    def handle_nextFrame(self,fx):
 
469
        '''On endFrame chenge to the frame with name or index fx'''
 
470
        if type(fx) is StringType:
 
471
            for f in self.pageTemplate.frames:
 
472
                if f.id == fx:
 
473
                    self._nextFrameIndex = self.pageTemplate.frames.index(f)
 
474
                    return
 
475
            raise ValueError, "can't find frame('%s')"%fx
 
476
        elif type(fx) is IntType:
 
477
            self._nextFrameIndex = fx
 
478
        else:
 
479
            raise TypeError, "argument fx should be string or integer"
 
480
 
 
481
    def handle_currentFrame(self,fx):
 
482
        '''chenge to the frame with name or index fx'''
 
483
        if type(fx) is StringType:
 
484
            for f in self.pageTemplate.frames:
 
485
                if f.id == fx:
 
486
                    self._nextFrameIndex = self.pageTemplate.frames.index(f)
 
487
                    return
 
488
            raise ValueError, "can't find frame('%s')"%fx
 
489
        elif type(fx) is IntType:
 
490
            self._nextFrameIndex = fx
 
491
        else:
 
492
            raise TypeError, "argument fx should be string or integer"
 
493
 
 
494
    def handle_breakBefore(self, flowables):
 
495
        '''preprocessing step to allow pageBreakBefore and frameBreakBefore attributes'''
 
496
        first = flowables[0]
 
497
        # if we insert a page break before, we'll process that, see it again,
 
498
        # and go in an infinite loop.  So we need to set a flag on the object
 
499
        # saying 'skip me'.  This should be unset on the next pass
 
500
        if hasattr(first, '_skipMeNextTime'):
 
501
            delattr(first, '_skipMeNextTime')
 
502
            return
 
503
        # this could all be made much quicker by putting the attributes
 
504
        # in to the flowables with a defult value of 0
 
505
        if hasattr(first,'pageBreakBefore') and first.pageBreakBefore == 1:
 
506
            first._skipMeNextTime = 1
 
507
            flowables.insert(0, PageBreak())
 
508
            return
 
509
        if hasattr(first,'style') and hasattr(first.style, 'pageBreakBefore') and first.style.pageBreakBefore == 1:
 
510
            first._skipMeNextTime = 1
 
511
            flowables.insert(0, PageBreak())
 
512
            return
 
513
        if hasattr(first,'frameBreakBefore') and first.frameBreakBefore == 1:
 
514
            first._skipMeNextTime = 1
 
515
            flowables.insert(0, FrameBreak())
 
516
            return
 
517
        if hasattr(first,'style') and hasattr(first.style, 'frameBreakBefore') and first.style.frameBreakBefore == 1:
 
518
            first._skipMeNextTime = 1
 
519
            flowables.insert(0, FrameBreak())
 
520
            return
 
521
 
 
522
 
 
523
    def handle_keepWithNext(self, flowables):
 
524
        "implements keepWithNext"
 
525
        i = 0
 
526
        n = len(flowables)
 
527
        while i<n and flowables[i].getKeepWithNext(): i = i + 1
 
528
        if i:
 
529
            i = i + 1
 
530
            K = KeepTogether(flowables[:i])
 
531
            for f in K._flowables:
 
532
                f.keepWithNext = 0
 
533
            del flowables[:i]
 
534
            flowables.insert(0,K)
 
535
 
 
536
    def handle_flowable(self,flowables):
 
537
        '''try to handle one flowable from the front of list flowables.'''
 
538
 
 
539
        #allow document a chance to look at, modify or ignore
 
540
        #the object(s) about to be processed
 
541
        self.filterFlowables(flowables)
 
542
 
 
543
        self.handle_breakBefore(flowables)
 
544
        self.handle_keepWithNext(flowables)
 
545
        f = flowables[0]
 
546
        #print 'handling flowable %s' % f.identity()
 
547
        del flowables[0]
 
548
        if f is None:
 
549
            return
 
550
 
 
551
        if isinstance(f,PageBreak):
 
552
            if isinstance(f,SlowPageBreak):
 
553
                self.handle_pageBreak(slow=1)
 
554
            else:
 
555
                self.handle_pageBreak()
 
556
            self.afterFlowable(f)
 
557
        elif isinstance(f,ActionFlowable):
 
558
            f.apply(self)
 
559
            self.afterFlowable(f)
 
560
        else:
 
561
            #try to fit it then draw it
 
562
            if self.frame.add(f, self.canv, trySplit=self.allowSplitting):
 
563
                self._curPageFlowableCount = self._curPageFlowableCount + 1
 
564
                self.afterFlowable(f)
 
565
            else:
 
566
                #if isinstance(f, KeepTogether): print 'could not add it to frame'
 
567
                if self.allowSplitting:
 
568
                    # see if this is a splittable thing
 
569
                    S = self.frame.split(f,self.canv)
 
570
                    #print '%d parts to sequence on page %d' % (len(S), self.page)
 
571
                    n = len(S)
 
572
                else:
 
573
                    n = 0
 
574
                #if isinstance(f, KeepTogether): print 'n=%d' % n
 
575
                if n:
 
576
                    if self.frame.add(S[0], self.canv, trySplit=0):
 
577
                        self._curPageFlowableCount = self._curPageFlowableCount + 1
 
578
                        self.afterFlowable(S[0])
 
579
                    else:
 
580
                        raise LayoutError("Splitting error(n==%d) on page %d in\n%s" % (n,self.page,f.identity(30)))
 
581
                    del S[0]
 
582
                    for f in xrange(n-1):
 
583
                        flowables.insert(f,S[f])    # put split flowables back on the list
 
584
                else:
 
585
                    if hasattr(f,'_postponed'):
 
586
                        raise LayoutError("Flowable %s too large on page %d" % (f.identity(30), self.page))
 
587
                    # this ought to be cleared when they are finally drawn!
 
588
                    f._postponed = 1
 
589
                    flowables.insert(0,f)           # put the flowable back
 
590
                    self.handle_frameEnd()
 
591
 
 
592
    #these are provided so that deriving classes can refer to them
 
593
    _handle_documentBegin = handle_documentBegin
 
594
    _handle_pageBegin = handle_pageBegin
 
595
    _handle_pageEnd = handle_pageEnd
 
596
    _handle_frameBegin = handle_frameBegin
 
597
    _handle_frameEnd = handle_frameEnd
 
598
    _handle_flowable = handle_flowable
 
599
    _handle_nextPageTemplate = handle_nextPageTemplate
 
600
    _handle_currentFrame = handle_currentFrame
 
601
    _handle_nextFrame = handle_nextFrame
 
602
 
 
603
    def _startBuild(self, filename=None, canvasmaker=canvas.Canvas):
 
604
        self._calc()
 
605
        self.canv = canvasmaker(filename or self.filename,
 
606
                                pagesize=self.pagesize,
 
607
                                invariant=self.invariant)
 
608
        if self._onPage:
 
609
            self.canv.setPageCallBack(self._onPage)
 
610
        self.handle_documentBegin()
 
611
 
 
612
    def _endBuild(self):
 
613
        if self._hanging!=[] and self._hanging[-1] is PageBegin:
 
614
            del self._hanging[-1]
 
615
            self.clean_hanging()
 
616
        else:
 
617
            self.clean_hanging()
 
618
            self.handle_pageBreak()
 
619
 
 
620
        if getattr(self,'_doSave',1): self.canv.save()
 
621
        if self._onPage: self.canv.setPageCallBack(None)
 
622
 
 
623
    def build(self, flowables, filename=None, canvasmaker=canvas.Canvas):
 
624
        """Build the document from a list of flowables.
 
625
           If the filename argument is provided then that filename is used
 
626
           rather than the one provided upon initialization.
 
627
           If the canvasmaker argument is provided then it will be used
 
628
           instead of the default.  For example a slideshow might use
 
629
           an alternate canvas which places 6 slides on a page (by
 
630
           doing translations, scalings and redefining the page break
 
631
           operations).
 
632
        """
 
633
        #assert filter(lambda x: not isinstance(x,Flowable), flowables)==[], "flowables argument error"
 
634
        flowableCount = len(flowables)
 
635
        if self._onProgress:
 
636
            self._onProgress('STARTED',0)
 
637
            self._onProgress('SIZE_EST', len(flowables))
 
638
        self._startBuild(filename,canvasmaker)
 
639
 
 
640
        while len(flowables):
 
641
            self.clean_hanging()
 
642
            try:
 
643
                first = flowables[0]
 
644
                self.handle_flowable(flowables)
 
645
            except:
 
646
                #if it has trace info, add it to the traceback message.
 
647
                if hasattr(first, '_traceInfo') and first._traceInfo:
 
648
                    exc = sys.exc_info()[1]
 
649
                    args = list(exc.args)
 
650
                    tr = first._traceInfo
 
651
                    args[0] = args[0] + '\n(srcFile %s, line %d char %d to line %d char %d)' % (
 
652
                        tr.srcFile,
 
653
                        tr.startLineNo,
 
654
                        tr.startLinePos,
 
655
                        tr.endLineNo,
 
656
                        tr.endLinePos
 
657
                        )
 
658
                    exc.args = tuple(args)
 
659
                raise
 
660
            if self._onProgress:
 
661
                self._onProgress('PROGRESS',flowableCount - len(flowables))
 
662
 
 
663
        self._endBuild()
 
664
        if self._onProgress:
 
665
            self._onProgress('FINISHED',0)
 
666
 
 
667
    def _allSatisfied(self):
 
668
        """Called by multi-build - are all cross-references resolved?"""
 
669
        allHappy = 1
 
670
        for f in self._indexingFlowables:
 
671
            if not f.isSatisfied():
 
672
                allHappy = 0
 
673
                break
 
674
        return allHappy
 
675
 
 
676
    def notify(self, kind, stuff):
 
677
        """"Forward to any listeners"""
 
678
        for l in self._indexingFlowables:
 
679
            l.notify(kind, stuff)
 
680
 
 
681
    def pageRef(self, label):
 
682
        """hook to register a page number"""
 
683
        if verbose: print "pageRef called with label '%s' on page %d" % (
 
684
            label, self.page)
 
685
        self._pageRefs[label] = self.page
 
686
 
 
687
    def multiBuild(self, story,
 
688
                   filename=None,
 
689
                   canvasmaker=canvas.Canvas,
 
690
                   maxPasses = 10):
 
691
        """Makes multiple passes until all indexing flowables
 
692
        are happy."""
 
693
        self._indexingFlowables = []
 
694
        #scan the story and keep a copy
 
695
        for thing in story:
 
696
            if thing.isIndexing():
 
697
                self._indexingFlowables.append(thing)
 
698
        #print 'scanned story, found these indexing flowables:\n'
 
699
        #print self._indexingFlowables
 
700
 
 
701
        #better fix for filename is a 'file' problem
 
702
        self._doSave = 0
 
703
        passes = 0
 
704
        while 1:
 
705
            passes = passes + 1
 
706
            if self._onProgress:
 
707
                self.onProgress('PASS', passes)
 
708
            if verbose: print 'building pass '+str(passes) + '...',
 
709
 
 
710
            for fl in self._indexingFlowables:
 
711
                fl.beforeBuild()
 
712
 
 
713
            # work with a copy of the story, since it is consumed
 
714
            tempStory = story[:]
 
715
            self.build(tempStory, filename, canvasmaker)
 
716
            #self.notify('debug',None)
 
717
 
 
718
            #clean up so multi-build does not go wrong - the frame
 
719
            #packer might have tacked an attribute onto some flowables
 
720
            for elem in story:
 
721
                if hasattr(elem, '_postponed'):
 
722
                    del elem._postponed
 
723
 
 
724
            for fl in self._indexingFlowables:
 
725
                fl.afterBuild()
 
726
 
 
727
            happy = self._allSatisfied()
 
728
 
 
729
            if happy:
 
730
                self._doSave = 0
 
731
                self.canv.save()
 
732
                break
 
733
            if passes > maxPasses:
 
734
                raise IndexError, "Index entries not resolved after %d passes" % maxPasses
 
735
 
 
736
        if verbose: print 'saved'
 
737
 
 
738
    #these are pure virtuals override in derived classes
 
739
    #NB these get called at suitable places by the base class
 
740
    #so if you derive and override the handle_xxx methods
 
741
    #it's up to you to ensure that they maintain the needed consistency
 
742
    def afterInit(self):
 
743
        """This is called after initialisation of the base class."""
 
744
        pass
 
745
 
 
746
    def beforeDocument(self):
 
747
        """This is called before any processing is
 
748
        done on the document."""
 
749
        pass
 
750
 
 
751
    def beforePage(self):
 
752
        """This is called at the beginning of page
 
753
        processing, and immediately before the
 
754
        beforeDrawPage method of the current page
 
755
        template."""
 
756
        pass
 
757
 
 
758
    def afterPage(self):
 
759
        """This is called after page processing, and
 
760
        immediately after the afterDrawPage method
 
761
        of the current page template."""
 
762
        pass
 
763
 
 
764
    def filterFlowables(self,flowables):
 
765
        '''called to filter flowables at the start of the main handle_flowable method.
 
766
        Upon return if flowables[0] has been set to None it is discarded and the main
 
767
        method returns.
 
768
        '''
 
769
        pass
 
770
 
 
771
    def afterFlowable(self, flowable):
 
772
        '''called after a flowable has been rendered'''
 
773
        pass
 
774
 
 
775
 
 
776
class SimpleDocTemplate(BaseDocTemplate):
 
777
    """A special case document template that will handle many simple documents.
 
778
       See documentation for BaseDocTemplate.  No pageTemplates are required
 
779
       for this special case.   A page templates are inferred from the
 
780
       margin information and the onFirstPage, onLaterPages arguments to the build method.
 
781
 
 
782
       A document which has all pages with the same look except for the first
 
783
       page may can be built using this special approach.
 
784
    """
 
785
    _invalidInitArgs = ('pageTemplates',)
 
786
 
 
787
    def handle_pageBegin(self):
 
788
        '''override base method to add a change of page template after the firstpage.
 
789
        '''
 
790
        self._handle_pageBegin()
 
791
        self._handle_nextPageTemplate('Later')
 
792
 
 
793
    def build(self,flowables,onFirstPage=_doNothing, onLaterPages=_doNothing):
 
794
        """build the document using the flowables.  Annotate the first page using the onFirstPage
 
795
               function and later pages using the onLaterPages function.  The onXXX pages should follow
 
796
               the signature
 
797
 
 
798
                  def myOnFirstPage(canvas, document):
 
799
                      # do annotations and modify the document
 
800
                      ...
 
801
 
 
802
               The functions can do things like draw logos, page numbers,
 
803
               footers, etcetera. They can use external variables to vary
 
804
               the look (for example providing page numbering or section names).
 
805
        """
 
806
        self._calc()    #in case we changed margins sizes etc
 
807
        frameT = Frame(self.leftMargin, self.bottomMargin, self.width, self.height, id='normal')
 
808
        self.addPageTemplates([PageTemplate(id='First',frames=frameT, onPage=onFirstPage,pagesize=self.pagesize),
 
809
                        PageTemplate(id='Later',frames=frameT, onPage=onLaterPages,pagesize=self.pagesize)])
 
810
        if onFirstPage is _doNothing and hasattr(self,'onFirstPage'):
 
811
            self.pageTemplates[0].beforeDrawPage = self.onFirstPage
 
812
        if onLaterPages is _doNothing and hasattr(self,'onLaterPages'):
 
813
            self.pageTemplates[1].beforeDrawPage = self.onLaterPages
 
814
        BaseDocTemplate.build(self,flowables)
 
815
 
 
816
 
 
817
def progressCB(typ, value):
 
818
    """Example prototype for progress monitoring.
 
819
 
 
820
    This aims to provide info about what is going on
 
821
    during a big job.  It should enable, for example, a reasonably
 
822
    smooth progress bar to be drawn.  We design the argument
 
823
    signature to be predictable and conducive to programming in
 
824
    other (type safe) languages.  If set, this will be called
 
825
    repeatedly with pairs of values.  The first is a string
 
826
    indicating the type of call; the second is a numeric value.
 
827
 
 
828
    typ 'STARTING', value = 0
 
829
    typ 'SIZE_EST', value = numeric estimate of job size
 
830
    typ 'PASS', value = number of this rendering pass
 
831
    typ 'PROGRESS', value = number between 0 and SIZE_EST
 
832
    typ 'PAGE', value = page number of page
 
833
    type 'FINISHED', value = 0
 
834
 
 
835
    The sequence is
 
836
        STARTING - always called once
 
837
        SIZE_EST - always called once
 
838
        PROGRESS - called often
 
839
        PAGE - called often when page is emitted
 
840
        FINISHED - called when really, really finished
 
841
 
 
842
    some juggling is needed to accurately estimate numbers of
 
843
    pages in pageDrawing mode.
 
844
 
 
845
    NOTE: the SIZE_EST is a guess.  It is possible that the
 
846
    PROGRESS value may slightly exceed it, or may even step
 
847
    back a little on rare occasions.  The only way to be
 
848
    really accurate would be to do two passes, and I don't
 
849
    want to take that performance hit.
 
850
    """
 
851
    print 'PROGRESS MONITOR:  %-10s   %d' % (typ, value)
 
852
 
 
853
if __name__ == '__main__':
 
854
 
 
855
    def myFirstPage(canvas, doc):
 
856
        canvas.saveState()
 
857
        canvas.setStrokeColor(red)
 
858
        canvas.setLineWidth(5)
 
859
        canvas.line(66,72,66,PAGE_HEIGHT-72)
 
860
        canvas.setFont('Times-Bold',24)
 
861
        canvas.drawString(108, PAGE_HEIGHT-108, "TABLE OF CONTENTS DEMO")
 
862
        canvas.setFont('Times-Roman',12)
 
863
        canvas.drawString(4 * inch, 0.75 * inch, "First Page")
 
864
        canvas.restoreState()
 
865
 
 
866
    def myLaterPages(canvas, doc):
 
867
        canvas.saveState()
 
868
        canvas.setStrokeColor(red)
 
869
        canvas.setLineWidth(5)
 
870
        canvas.line(66,72,66,PAGE_HEIGHT-72)
 
871
        canvas.setFont('Times-Roman',12)
 
872
        canvas.drawString(4 * inch, 0.75 * inch, "Page %d" % doc.page)
 
873
        canvas.restoreState()
 
874
 
 
875
    def run():
 
876
        objects_to_draw = []
 
877
        from reportlab.lib.styles import ParagraphStyle
 
878
        #from paragraph import Paragraph
 
879
        from doctemplate import SimpleDocTemplate
 
880
 
 
881
        #need a style
 
882
        normal = ParagraphStyle('normal')
 
883
        normal.firstLineIndent = 18
 
884
        normal.spaceBefore = 6
 
885
        from reportlab.lib.randomtext import randomText
 
886
        import random
 
887
        for i in range(15):
 
888
            height = 0.5 + (2*random.random())
 
889
            box = XBox(6 * inch, height * inch, 'Box Number %d' % i)
 
890
            objects_to_draw.append(box)
 
891
            para = Paragraph(randomText(), normal)
 
892
            objects_to_draw.append(para)
 
893
 
 
894
        SimpleDocTemplate('doctemplate.pdf').build(objects_to_draw,
 
895
            onFirstPage=myFirstPage,onLaterPages=myLaterPages)
 
896
 
 
897
    run()