--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/reportlab/platypus/doctemplate.py Wed Sep 03 16:10:51 2008 +0000
@@ -0,0 +1,1073 @@
+#Copyright ReportLab Europe Ltd. 2000-2004
+#see license.txt for license details
+#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/doctemplate.py
+
+__version__=''' $Id$ '''
+
+__doc__="""
+This module contains the core structure of platypus.
+
+rlatypus constructs documents. Document styles are determined by DocumentTemplates.
+
+Each DocumentTemplate contains one or more PageTemplates which defines the look of the
+pages of the document.
+
+Each PageTemplate has a procedure for drawing the "non-flowing" part of the page
+(for example the header, footer, page number, fixed logo graphic, watermark, etcetera) and
+a set of Frames which enclose the flowing part of the page (for example the paragraphs,
+tables, or non-fixed diagrams of the text).
+
+A document is built when a DocumentTemplate is fed a sequence of Flowables.
+The action of the build consumes the flowables in order and places them onto
+frames on pages as space allows. When a frame runs out of space the next frame
+of the page is used. If no frame remains a new page is created. A new page
+can also be created if a page break is forced.
+
+The special invisible flowable NextPageTemplate can be used to specify
+the page template for the next page (which by default is the one being used
+for the current frame).
+"""
+
+from reportlab.platypus.flowables import *
+from reportlab.lib.units import inch
+from reportlab.platypus.paragraph import Paragraph
+from reportlab.platypus.frames import Frame
+from reportlab.rl_config import defaultPageSize, verbose
+import reportlab.lib.sequencer
+from reportlab.pdfgen import canvas
+
+from types import *
+import sys
+import logging
+logger = logging.getLogger("reportlab.platypus")
+
+class LayoutError(Exception):
+ pass
+
+def _doNothing(canvas, doc):
+ "Dummy callback for onPage"
+ pass
+
+class PTCycle(list):
+ def __init__(self):
+ self._restart = 0
+ self._idx = 0
+ list.__init__(self)
+
+ def cyclicIterator(self):
+ while 1:
+ yield self[self._idx]
+ self._idx += 1
+ if self._idx>=len(self):
+ self._idx = self._restart
+
+class IndexingFlowable(Flowable):
+ """Abstract interface definition for flowables which might
+ hold references to other pages or themselves be targets
+ of cross-references. XRefStart, XRefDest, Table of Contents,
+ Indexes etc."""
+ def isIndexing(self):
+ return 1
+
+ def isSatisfied(self):
+ return 1
+
+ def notify(self, kind, stuff):
+ """This will be called by the framework wherever 'stuff' happens.
+ 'kind' will be a value that can be used to decide whether to
+ pay attention or not."""
+ pass
+
+ def beforeBuild(self):
+ """Called by multiBuild before it starts; use this to clear
+ old contents"""
+ pass
+
+ def afterBuild(self):
+ """Called after build ends but before isSatisfied"""
+ pass
+
+class ActionFlowable(Flowable):
+ '''This Flowable is never drawn, it can be used for data driven controls
+ For example to change a page template (from one column to two, for example)
+ use NextPageTemplate which creates an ActionFlowable.
+ '''
+ def __init__(self,action=()):
+ #must call super init to ensure it has a width and height (of zero),
+ #as in some cases the packer might get called on it...
+ Flowable.__init__(self)
+ if type(action) not in (ListType, TupleType):
+ action = (action,)
+ self.action = tuple(action)
+
+ def apply(self,doc):
+ '''
+ This is called by the doc.build processing to allow the instance to
+ implement its behaviour
+ '''
+ action = self.action[0]
+ args = tuple(self.action[1:])
+ arn = 'handle_'+action
+ if arn=="handle_nextPageTemplate" and args[0]=='main':
+ pass
+ try:
+ getattr(doc,arn)(*args)
+ except AttributeError, aerr:
+ if aerr.args[0]==arn:
+ raise NotImplementedError, "Can't handle ActionFlowable(%s)" % action
+ else:
+ raise
+ except "bogus":
+ t, v, unused = sys.exc_info()
+ raise t, "%s\n handle_%s args=%s"%(v,action,args)
+
+ def __call__(self):
+ return self
+
+ def identity(self, maxLen=None):
+ return "ActionFlowable: %s%s" % (str(self.action),self._frameName())
+
+class LCActionFlowable(ActionFlowable):
+ locChanger = 1 #we cause a frame or page change
+
+ def wrap(self, availWidth, availHeight):
+ '''Should never be called.'''
+ raise NotImplementedError
+
+ def draw(self):
+ '''Should never be called.'''
+ raise NotImplementedError
+
+class NextFrameFlowable(ActionFlowable):
+ def __init__(self,ix,resume=0):
+ ActionFlowable.__init__(self,('nextFrame',ix,resume))
+
+class CurrentFrameFlowable(LCActionFlowable):
+ def __init__(self,ix,resume=0):
+ ActionFlowable.__init__(self,('currentFrame',ix,resume))
+
+class NullActionFlowable(ActionFlowable):
+ def apply(self):
+ pass
+
+class _FrameBreak(LCActionFlowable):
+ '''
+ A special ActionFlowable that allows setting doc._nextFrameIndex
+
+ eg story.append(FrameBreak('mySpecialFrame'))
+ '''
+ def __call__(self,ix=None,resume=0):
+ r = self.__class__(self.action+(resume,))
+ r._ix = ix
+ return r
+
+ def apply(self,doc):
+ if getattr(self,'_ix',None):
+ doc.handle_nextFrame(self._ix)
+ ActionFlowable.apply(self,doc)
+
+FrameBreak = _FrameBreak('frameEnd')
+PageBegin = LCActionFlowable('pageBegin')
+
+def _evalMeasurement(n):
+ if type(n) is type(''):
+ from paraparser import _num
+ n = _num(n)
+ if type(n) is type(()): n = n[1]
+ return n
+
+class FrameActionFlowable(Flowable):
+ _fixedWidth = _fixedHeight = 1
+ def __init__(self,*arg,**kw):
+ raise NotImplementedError('Abstract Class')
+
+ def frameAction(self,frame):
+ raise NotImplementedError('Abstract Class')
+
+class Indenter(FrameActionFlowable):
+ """Increases or decreases left and right margins of frame.
+
+ This allows one to have a 'context-sensitive' indentation
+ and makes nested lists way easier.
+ """
+ def __init__(self, left=0, right=0):
+ self.left = _evalMeasurement(left)
+ self.right = _evalMeasurement(right)
+
+ def frameAction(self, frame):
+ frame._leftExtraIndent += self.left
+ frame._rightExtraIndent += self.right
+
+class NotAtTopPageBreak(FrameActionFlowable):
+ def __init__(self):
+ pass
+
+ def frameAction(self,frame):
+ if not frame._atTop:
+ frame.add_generated_content(PageBreak())
+
+class NextPageTemplate(ActionFlowable):
+ """When you get to the next page, use the template specified (change to two column, for example) """
+ def __init__(self,pt):
+ ActionFlowable.__init__(self,('nextPageTemplate',pt))
+
+class PageTemplate:
+ """
+ essentially a list of Frames and an onPage routine to call at the start
+ of a page when this is selected. onPageEnd gets called at the end.
+ derived classes can also implement beforeDrawPage and afterDrawPage if they want
+ """
+ def __init__(self,id=None,frames=[],onPage=_doNothing, onPageEnd=_doNothing,
+ pagesize=None):
+ if type(frames) not in (ListType,TupleType): frames = [frames]
+ assert filter(lambda x: not isinstance(x,Frame), frames)==[], "frames argument error"
+ self.id = id
+ self.frames = frames
+ self.onPage = onPage
+ self.onPageEnd = onPageEnd
+ self.pagesize = pagesize
+
+ def beforeDrawPage(self,canv,doc):
+ """Override this if you want additional functionality or prefer
+ a class based page routine. Called before any flowables for
+ this page are processed."""
+ pass
+
+ def checkPageSize(self,canv,doc):
+ """This gets called by the template framework
+ If canv size != template size then the canv size is set to
+ the template size or if that's not available to the
+ doc size.
+ """
+ #### NEVER EVER EVER COMPARE FLOATS FOR EQUALITY
+ #RGB converting pagesizes to ints means we are accurate to one point
+ #RGB I suggest we should be aiming a little better
+ cp = None
+ dp = None
+ sp = None
+ if canv._pagesize: cp = map(int, canv._pagesize)
+ if self.pagesize: sp = map(int, self.pagesize)
+ if doc.pagesize: dp = map(int, doc.pagesize)
+ if cp!=sp:
+ if sp:
+ canv.setPageSize(self.pagesize)
+ elif cp!=dp:
+ canv.setPageSize(doc.pagesize)
+
+ def afterDrawPage(self, canv, doc):
+ """This is called after the last flowable for the page has
+ been processed. You might use this if the page header or
+ footer needed knowledge of what flowables were drawn on
+ this page."""
+ pass
+
+class BaseDocTemplate:
+ """
+ First attempt at defining a document template class.
+
+ The basic idea is simple.
+ 0) The document has a list of data associated with it
+ this data should derive from flowables. We'll have
+ special classes like PageBreak, FrameBreak to do things
+ like forcing a page end etc.
+
+ 1) The document has one or more page templates.
+
+ 2) Each page template has one or more frames.
+
+ 3) The document class provides base methods for handling the
+ story events and some reasonable methods for getting the
+ story flowables into the frames.
+
+ 4) The document instances can override the base handler routines.
+
+ Most of the methods for this class are not called directly by the user,
+ but in some advanced usages they may need to be overridden via subclassing.
+
+ EXCEPTION: doctemplate.build(...) must be called for most reasonable uses
+ since it builds a document using the page template.
+
+ Each document template builds exactly one document into a file specified
+ by the filename argument on initialization.
+
+ Possible keyword arguments for the initialization:
+
+ pageTemplates: A list of templates. Must be nonempty. Names
+ assigned to the templates are used for referring to them so no two used
+ templates should have the same name. For example you might want one template
+ for a title page, one for a section first page, one for a first page of
+ a chapter and two more for the interior of a chapter on odd and even pages.
+ If this argument is omitted then at least one pageTemplate should be provided
+ using the addPageTemplates method before the document is built.
+ pageSize: a 2-tuple or a size constant from reportlab/lib/pagesizes.pu.
+ Used by the SimpleDocTemplate subclass which does NOT accept a list of
+ pageTemplates but makes one for you; ignored when using pageTemplates.
+
+ showBoundary: if set draw a box around the frame boundaries.
+ leftMargin:
+ rightMargin:
+ topMargin:
+ bottomMargin: Margin sizes in points (default 1 inch)
+ These margins may be overridden by the pageTemplates. They are primarily of interest
+ for the SimpleDocumentTemplate subclass.
+ allowSplitting: If set flowables (eg, paragraphs) may be split across frames or pages
+ (default: 1)
+ title: Internal title for document (does not automatically display on any page)
+ author: Internal author for document (does not automatically display on any page)
+ """
+ _initArgs = { 'pagesize':defaultPageSize,
+ 'pageTemplates':[],
+ 'showBoundary':0,
+ 'leftMargin':inch,
+ 'rightMargin':inch,
+ 'topMargin':inch,
+ 'bottomMargin':inch,
+ 'allowSplitting':1,
+ 'title':None,
+ 'author':None,
+ 'subject':None,
+ 'keywords':[],
+ 'invariant':None,
+ 'pageCompression':None,
+ '_pageBreakQuick':1,
+ 'rotation':0,
+ '_debug':0}
+ _invalidInitArgs = ()
+ _firstPageTemplateIndex = 0
+
+ def __init__(self, filename, **kw):
+ """create a document template bound to a filename (see class documentation for keyword arguments)"""
+ self.filename = filename
+ self._nameSpace = dict(doc=self)
+ self._lifetimes = {}
+
+ for k in self._initArgs.keys():
+ if not kw.has_key(k):
+ v = self._initArgs[k]
+ else:
+ if k in self._invalidInitArgs:
+ raise ValueError, "Invalid argument %s" % k
+ v = kw[k]
+ setattr(self,k,v)
+
+ p = self.pageTemplates
+ self.pageTemplates = []
+ self.addPageTemplates(p)
+
+ # facility to assist multi-build and cross-referencing.
+ # various hooks can put things into here - key is what
+ # you want, value is a page number. This can then be
+ # passed to indexing flowables.
+ self._pageRefs = {}
+ self._indexingFlowables = []
+
+ #callback facility for progress monitoring
+ self._onPage = None
+ self._onProgress = None
+ self._flowableCount = 0 # so we know how far to go
+
+ #infinite loop detection if we start doing lots of empty pages
+ self._curPageFlowableCount = 0
+ self._emptyPages = 0
+ self._emptyPagesAllowed = 10
+
+ #context sensitive margins - set by story, not from outside
+ self._leftExtraIndent = 0.0
+ self._rightExtraIndent = 0.0
+
+ self._calc()
+ self.afterInit()
+
+ def _calc(self):
+ self._rightMargin = self.pagesize[0] - self.rightMargin
+ self._topMargin = self.pagesize[1] - self.topMargin
+ self.width = self._rightMargin - self.leftMargin
+ self.height = self._topMargin - self.bottomMargin
+
+ def setPageCallBack(self, func):
+ 'Simple progress monitor - func(pageNo) called on each new page'
+ self._onPage = func
+
+ def setProgressCallBack(self, func):
+ '''Cleverer progress monitor - func(typ, value) called regularly'''
+ self._onProgress = func
+
+ def clean_hanging(self):
+ 'handle internal postponed actions'
+ while len(self._hanging):
+ self.handle_flowable(self._hanging)
+
+ def addPageTemplates(self,pageTemplates):
+ 'add one or a sequence of pageTemplates'
+ if type(pageTemplates) not in (ListType,TupleType):
+ pageTemplates = [pageTemplates]
+ #this test below fails due to inconsistent imports!
+ #assert filter(lambda x: not isinstance(x,PageTemplate), pageTemplates)==[], "pageTemplates argument error"
+ for t in pageTemplates:
+ self.pageTemplates.append(t)
+
+ def handle_documentBegin(self):
+ '''implement actions at beginning of document'''
+ self._hanging = [PageBegin]
+ self.pageTemplate = self.pageTemplates[self._firstPageTemplateIndex]
+ self.page = 0
+ self.beforeDocument()
+
+ def handle_pageBegin(self):
+ """Perform actions required at beginning of page.
+ shouldn't normally be called directly"""
+ self.page += 1
+ if self._debug: logger.debug("beginning page %d" % self.page)
+ self.pageTemplate.beforeDrawPage(self.canv,self)
+ self.pageTemplate.checkPageSize(self.canv,self)
+ self.pageTemplate.onPage(self.canv,self)
+ for f in self.pageTemplate.frames: f._reset()
+ self.beforePage()
+ #keep a count of flowables added to this page. zero indicates bad stuff
+ self._curPageFlowableCount = 0
+ if hasattr(self,'_nextFrameIndex'):
+ del self._nextFrameIndex
+ self.frame = self.pageTemplate.frames[0]
+ self.frame._debug = self._debug
+ self.handle_frameBegin()
+
+ def handle_pageEnd(self):
+ ''' show the current page
+ check the next page template
+ hang a page begin
+ '''
+ self._removeVars(('page','frame'))
+ #detect infinite loops...
+ if self._curPageFlowableCount == 0:
+ self._emptyPages += 1
+ else:
+ self._emptyPages = 0
+ if self._emptyPages >= self._emptyPagesAllowed:
+ if 1:
+ ident = "More than %d pages generated without content - halting layout. Likely that a flowable is too large for any frame." % self._emptyPagesAllowed
+ #leave to keep apart from the raise
+ raise LayoutError(ident)
+ else:
+ pass #attempt to restore to good state
+ else:
+ if self._onProgress:
+ self._onProgress('PAGE', self.canv.getPageNumber())
+ self.pageTemplate.afterDrawPage(self.canv, self)
+ self.pageTemplate.onPageEnd(self.canv, self)
+ self.afterPage()
+ if self._debug: logger.debug("ending page %d" % self.page)
+ self.canv.setPageRotation(getattr(self.pageTemplate,'rotation',self.rotation))
+ self.canv.showPage()
+
+ if hasattr(self,'_nextPageTemplateCycle'):
+ #they are cycling through pages'; we keep the index
+ self.pageTemplate = self._nextPageTemplateCycle.next()
+ elif hasattr(self,'_nextPageTemplateIndex'):
+ self.pageTemplate = self.pageTemplates[self._nextPageTemplateIndex]
+ del self._nextPageTemplateIndex
+ if self._emptyPages==0:
+ pass #store good state here
+ self._hanging.append(PageBegin)
+
+ def handle_pageBreak(self,slow=None):
+ '''some might choose not to end all the frames'''
+ if self._pageBreakQuick and not slow:
+ self.handle_pageEnd()
+ else:
+ n = len(self._hanging)
+ while len(self._hanging)==n:
+ self.handle_frameEnd()
+
+ def handle_frameBegin(self,resume=0):
+ '''What to do at the beginning of a frame'''
+ f = self.frame
+ if f._atTop:
+ if self.showBoundary or self.frame.showBoundary:
+ self.frame.drawBoundary(self.canv)
+ f._leftExtraIndent = self._leftExtraIndent
+ f._rightExtraIndent = self._rightExtraIndent
+
+ def handle_frameEnd(self,resume=0):
+ ''' Handles the semantics of the end of a frame. This includes the selection of
+ the next frame or if this is the last frame then invoke pageEnd.
+ '''
+ self._removeVars(('frame',))
+ self._leftExtraIndent = self.frame._leftExtraIndent
+ self._rightExtraIndent = self.frame._rightExtraIndent
+
+ f = self.frame
+ if hasattr(self,'_nextFrameIndex'):
+ self.frame = self.pageTemplate.frames[self._nextFrameIndex]
+ self.frame._debug = self._debug
+ del self._nextFrameIndex
+ self.handle_frameBegin(resume)
+ elif hasattr(f,'lastFrame') or f is self.pageTemplate.frames[-1]:
+ self.handle_pageEnd()
+ self.frame = None
+ else:
+ self.frame = self.pageTemplate.frames[self.pageTemplate.frames.index(f) + 1]
+ self.frame._debug = self._debug
+ self.handle_frameBegin()
+
+ def handle_nextPageTemplate(self,pt):
+ '''On endPage change to the page template with name or index pt'''
+ if type(pt) is StringType:
+ if hasattr(self, '_nextPageTemplateCycle'): del self._nextPageTemplateCycle
+ for t in self.pageTemplates:
+ if t.id == pt:
+ self._nextPageTemplateIndex = self.pageTemplates.index(t)
+ return
+ raise ValueError, "can't find template('%s')"%pt
+ elif type(pt) is IntType:
+ if hasattr(self, '_nextPageTemplateCycle'): del self._nextPageTemplateCycle
+ self._nextPageTemplateIndex = pt
+ elif type(pt) in (ListType, TupleType):
+ #used for alternating left/right pages
+ #collect the refs to the template objects, complain if any are bad
+ c = PTCycle()
+ for ptn in pt:
+ found = 0
+ if ptn=='*': #special case name used to short circuit the iteration
+ c._restart = len(c)
+ continue
+ for t in self.pageTemplates:
+ if t.id == ptn:
+ c.append(t)
+ found = 1
+ if not found:
+ raise ValueError("Cannot find page template called %s" % templateName)
+ if not c:
+ raise ValueError("No valid page templates in cycle")
+ elif c._restart>len(c):
+ raise ValueError("Invalid cycle restart position")
+
+ #ensure we start on the first one
+ self._nextPageTemplateCycle = c.cyclicIterator()
+ else:
+ raise TypeError("argument pt should be string or integer or list")
+
+ def handle_nextFrame(self,fx,resume=0):
+ '''On endFrame change to the frame with name or index fx'''
+ if type(fx) is StringType:
+ for f in self.pageTemplate.frames:
+ if f.id == fx:
+ self._nextFrameIndex = self.pageTemplate.frames.index(f)
+ return
+ raise ValueError("can't find frame('%s') in %r(%s) which has frames %r"%(fx,self.pageTemplate,self.pageTemplate.id,[(f,f.id) for f in self.pageTemplate.frames]))
+ elif type(fx) is IntType:
+ self._nextFrameIndex = fx
+ else:
+ raise TypeError, "argument fx should be string or integer"
+
+ def handle_currentFrame(self,fx,resume=0):
+ '''change to the frame with name or index fx'''
+ self.handle_nextFrame(fx,resume)
+ self.handle_frameEnd(resume)
+
+ def handle_breakBefore(self, flowables):
+ '''preprocessing step to allow pageBreakBefore and frameBreakBefore attributes'''
+ first = flowables[0]
+ # if we insert a page break before, we'll process that, see it again,
+ # and go in an infinite loop. So we need to set a flag on the object
+ # saying 'skip me'. This should be unset on the next pass
+ if hasattr(first, '_skipMeNextTime'):
+ delattr(first, '_skipMeNextTime')
+ return
+ # this could all be made much quicker by putting the attributes
+ # in to the flowables with a defult value of 0
+ if hasattr(first,'pageBreakBefore') and first.pageBreakBefore == 1:
+ first._skipMeNextTime = 1
+ first.insert(0, PageBreak())
+ return
+ if hasattr(first,'style') and hasattr(first.style, 'pageBreakBefore') and first.style.pageBreakBefore == 1:
+ first._skipMeNextTime = 1
+ flowables.insert(0, PageBreak())
+ return
+ if hasattr(first,'frameBreakBefore') and first.frameBreakBefore == 1:
+ first._skipMeNextTime = 1
+ flowables.insert(0, FrameBreak())
+ return
+ if hasattr(first,'style') and hasattr(first.style, 'frameBreakBefore') and first.style.frameBreakBefore == 1:
+ first._skipMeNextTime = 1
+ flowables.insert(0, FrameBreak())
+ return
+
+ def handle_keepWithNext(self, flowables):
+ "implements keepWithNext"
+ i = 0
+ n = len(flowables)
+ while i<n and flowables[i].getKeepWithNext(): i += 1
+ if i:
+ if i<n and not getattr(flowables[i],'locChanger',None): i += 1
+ K = KeepTogether(flowables[:i])
+ for f in K._content[:-1]:
+ f.__dict__['keepWithNext'] = 0
+ del flowables[:i]
+ flowables.insert(0,K)
+
+ def _fIdent(self,f,maxLen=None,frame=None):
+ if frame: f._frame = frame
+ try:
+ return f.identity(maxLen)
+ finally:
+ if frame: del f._frame
+
+ def _addGeneratedContent(self,flowables,frame):
+ S = getattr(frame,'_generated_content',None)
+ if S:
+ for i,f in enumerate(S):
+ flowables.insert(i,f)
+ del frame._generated_content
+
+ def handle_flowable(self,flowables):
+ '''try to handle one flowable from the front of list flowables.'''
+
+ #allow document a chance to look at, modify or ignore
+ #the object(s) about to be processed
+ self.filterFlowables(flowables)
+
+ self.handle_breakBefore(flowables)
+ self.handle_keepWithNext(flowables)
+ f = flowables[0]
+ del flowables[0]
+ if f is None:
+ return
+
+ if isinstance(f,PageBreak):
+ if isinstance(f,SlowPageBreak):
+ self.handle_pageBreak(slow=1)
+ else:
+ self.handle_pageBreak()
+ self.afterFlowable(f)
+ elif isinstance(f,ActionFlowable):
+ f.apply(self)
+ self.afterFlowable(f)
+ else:
+ frame = self.frame
+ canv = self.canv
+ #try to fit it then draw it
+ if frame.add(f, canv, trySplit=self.allowSplitting):
+ if not isinstance(f,FrameActionFlowable):
+ self._curPageFlowableCount += 1
+ self.afterFlowable(f)
+ self._addGeneratedContent(flowables,frame)
+ else:
+ if self.allowSplitting:
+ # see if this is a splittable thing
+ S = frame.split(f,canv)
+ n = len(S)
+ else:
+ n = 0
+ if n:
+ if not isinstance(S[0],(PageBreak,SlowPageBreak,ActionFlowable)):
+ if frame.add(S[0], canv, trySplit=0):
+ self._curPageFlowableCount += 1
+ self.afterFlowable(S[0])
+ self._addGeneratedContent(flowables,frame)
+ else:
+ ident = "Splitting error(n==%d) on page %d in\n%s" % (n,self.page,self._fIdent(f,60,frame))
+ #leave to keep apart from the raise
+ raise LayoutError(ident)
+ del S[0]
+ for i,f in enumerate(S):
+ flowables.insert(i,f) # put split flowables back on the list
+ else:
+ if hasattr(f,'_postponed'):
+ ident = "Flowable %s too large on page %d" % (self._fIdent(f,60,frame), self.page)
+ #leave to keep apart from the raise
+ raise LayoutError(ident)
+ # this ought to be cleared when they are finally drawn!
+ f._postponed = 1
+ flowables.insert(0,f) # put the flowable back
+ self.handle_frameEnd()
+
+ #these are provided so that deriving classes can refer to them
+ _handle_documentBegin = handle_documentBegin
+ _handle_pageBegin = handle_pageBegin
+ _handle_pageEnd = handle_pageEnd
+ _handle_frameBegin = handle_frameBegin
+ _handle_frameEnd = handle_frameEnd
+ _handle_flowable = handle_flowable
+ _handle_nextPageTemplate = handle_nextPageTemplate
+ _handle_currentFrame = handle_currentFrame
+ _handle_nextFrame = handle_nextFrame
+
+ def _startBuild(self, filename=None, canvasmaker=canvas.Canvas):
+ self._calc()
+
+ #each distinct pass gets a sequencer
+ self.seq = reportlab.lib.sequencer.Sequencer()
+
+ self.canv = canvasmaker(filename or self.filename,
+ pagesize=self.pagesize,
+ invariant=self.invariant,
+ pageCompression=self.pageCompression)
+
+ self.canv.setAuthor(self.author)
+ self.canv.setTitle(self.title)
+ self.canv.setSubject(self.subject)
+ self.canv.setKeywords(self.keywords)
+
+ if self._onPage:
+ self.canv.setPageCallBack(self._onPage)
+ self.handle_documentBegin()
+
+ def _endBuild(self):
+ self._removeVars(('build','page','frame'))
+ if self._hanging!=[] and self._hanging[-1] is PageBegin:
+ del self._hanging[-1]
+ self.clean_hanging()
+ else:
+ self.clean_hanging()
+ self.handle_pageBreak()
+
+ if getattr(self,'_doSave',1): self.canv.save()
+ if self._onPage: self.canv.setPageCallBack(None)
+
+ def build(self, flowables, filename=None, canvasmaker=canvas.Canvas):
+ """Build the document from a list of flowables.
+ If the filename argument is provided then that filename is used
+ rather than the one provided upon initialization.
+ If the canvasmaker argument is provided then it will be used
+ instead of the default. For example a slideshow might use
+ an alternate canvas which places 6 slides on a page (by
+ doing translations, scalings and redefining the page break
+ operations).
+ """
+ #assert filter(lambda x: not isinstance(x,Flowable), flowables)==[], "flowables argument error"
+ flowableCount = len(flowables)
+ if self._onProgress:
+ self._onProgress('STARTED',0)
+ self._onProgress('SIZE_EST', len(flowables))
+ self._startBuild(filename,canvasmaker)
+
+ #pagecatcher can drag in information from embedded PDFs and we want ours
+ #to take priority, so cache and reapply our own info dictionary after the build.
+ canv = self.canv
+ self._savedInfo = canv._doc.info
+ handled = 0
+
+ try:
+ canv._doctemplate = self
+ while len(flowables):
+ self.clean_hanging()
+ try:
+ first = flowables[0]
+ self.handle_flowable(flowables)
+ handled += 1
+ except:
+ #if it has trace info, add it to the traceback message.
+ if hasattr(first, '_traceInfo') and first._traceInfo:
+ exc = sys.exc_info()[1]
+ args = list(exc.args)
+ tr = first._traceInfo
+ args[0] += '\n(srcFile %s, line %d char %d to line %d char %d)' % (
+ tr.srcFile,
+ tr.startLineNo,
+ tr.startLinePos,
+ tr.endLineNo,
+ tr.endLinePos
+ )
+ exc.args = tuple(args)
+ raise
+ if self._onProgress:
+ self._onProgress('PROGRESS',flowableCount - len(flowables))
+ finally:
+ del canv._doctemplate
+
+
+ #reapply pagecatcher info
+ canv._doc.info = self._savedInfo
+
+ self._endBuild()
+ if self._onProgress:
+ self._onProgress('FINISHED',0)
+
+ def _allSatisfied(self):
+ """Called by multi-build - are all cross-references resolved?"""
+ allHappy = 1
+ for f in self._indexingFlowables:
+ if not f.isSatisfied():
+ allHappy = 0
+ break
+ return allHappy
+
+ def notify(self, kind, stuff):
+ """Forward to any listeners"""
+ for l in self._indexingFlowables:
+ l.notify(kind, stuff)
+
+ def pageRef(self, label):
+ """hook to register a page number"""
+ if verbose: print "pageRef called with label '%s' on page %d" % (
+ label, self.page)
+ self._pageRefs[label] = self.page
+
+ def multiBuild(self, story,
+ filename=None,
+ canvasmaker=canvas.Canvas,
+ maxPasses = 10):
+ """Makes multiple passes until all indexing flowables
+ are happy."""
+ self._indexingFlowables = []
+ #scan the story and keep a copy
+ for thing in story:
+ if thing.isIndexing():
+ self._indexingFlowables.append(thing)
+
+ #better fix for filename is a 'file' problem
+ self._doSave = 0
+ passes = 0
+ while 1:
+ passes += 1
+ if self._onProgress:
+ self._onProgress('PASS', passes)
+ if verbose: print 'building pass '+str(passes) + '...',
+
+ for fl in self._indexingFlowables:
+ fl.beforeBuild()
+
+ # work with a copy of the story, since it is consumed
+ tempStory = story[:]
+ self.build(tempStory, filename, canvasmaker)
+ #self.notify('debug',None)
+
+ #clean up so multi-build does not go wrong - the frame
+ #packer might have tacked an attribute onto some flowables
+ for elem in story:
+ if hasattr(elem, '_postponed'):
+ del elem._postponed
+
+ for fl in self._indexingFlowables:
+ fl.afterBuild()
+
+ happy = self._allSatisfied()
+
+ if happy:
+ self._doSave = 0
+ self.canv.save()
+ break
+ if passes > maxPasses:
+ raise IndexError, "Index entries not resolved after %d passes" % maxPasses
+
+ if verbose: print 'saved'
+
+ #these are pure virtuals override in derived classes
+ #NB these get called at suitable places by the base class
+ #so if you derive and override the handle_xxx methods
+ #it's up to you to ensure that they maintain the needed consistency
+ def afterInit(self):
+ """This is called after initialisation of the base class."""
+ pass
+
+ def beforeDocument(self):
+ """This is called before any processing is
+ done on the document."""
+ pass
+
+ def beforePage(self):
+ """This is called at the beginning of page
+ processing, and immediately before the
+ beforeDrawPage method of the current page
+ template."""
+ pass
+
+ def afterPage(self):
+ """This is called after page processing, and
+ immediately after the afterDrawPage method
+ of the current page template."""
+ pass
+
+ def filterFlowables(self,flowables):
+ '''called to filter flowables at the start of the main handle_flowable method.
+ Upon return if flowables[0] has been set to None it is discarded and the main
+ method returns.
+ '''
+ pass
+
+ def afterFlowable(self, flowable):
+ '''called after a flowable has been rendered'''
+ pass
+
+ _allowedLifetimes = 'page','frame','build','forever'
+ def docAssign(self,var,expr,lifetime):
+ if not isinstance(expr,(str,unicode)): expr=str(expr)
+ expr=expr.strip()
+ var=var.strip()
+ self.docExec('%s=(%s)'%(var.strip(),expr.strip()),lifetime)
+
+ def docExec(self,stmt,lifetime):
+ stmt=stmt.strip()
+ NS=self._nameSpace
+ K0=NS.keys()
+ try:
+ if lifetime not in self._allowedLifetimes:
+ raise ValueError('bad lifetime %r not in %r'%(lifetime,self._allowedLifetimes))
+ exec stmt in {},NS
+ except:
+ exc = sys.exc_info()[1]
+ args = list(exc.args)
+ args[-1] += '\ndocExec %s lifetime=%r failed!' % (stmt,lifetime)
+ exc.args = tuple(args)
+ for k in NS.iterkeys():
+ if k not in K0:
+ del NS[k]
+ raise
+ self._addVars([k for k in NS.iterkeys() if k not in K0],lifetime)
+
+ def _addVars(self,vars,lifetime):
+ '''add namespace variables to lifetimes lists'''
+ LT=self._lifetimes
+ for var in vars:
+ for v in LT.itervalues():
+ if var in v:
+ v.remove(var)
+ LT.setdefault(lifetime,set([])).add(var)
+
+ def _removeVars(self,lifetimes):
+ '''remove namespace variables for with lifetime in lifetimes'''
+ LT=self._lifetimes
+ NS=self._nameSpace
+ for lifetime in lifetimes:
+ for k in LT.setdefault(lifetime,[]):
+ try:
+ del NS[k]
+ except KeyError:
+ pass
+ del LT[lifetime]
+
+ def docEval(self,expr):
+ try:
+ return eval(expr.strip(),{},self._nameSpace)
+ except:
+ exc = sys.exc_info()[1]
+ args = list(exc.args)
+ args[-1] += '\ndocEval %s failed!' % expr
+ exc.args = tuple(args)
+ raise
+
+class SimpleDocTemplate(BaseDocTemplate):
+ """A special case document template that will handle many simple documents.
+ See documentation for BaseDocTemplate. No pageTemplates are required
+ for this special case. A page templates are inferred from the
+ margin information and the onFirstPage, onLaterPages arguments to the build method.
+
+ A document which has all pages with the same look except for the first
+ page may can be built using this special approach.
+ """
+ _invalidInitArgs = ('pageTemplates',)
+
+ def handle_pageBegin(self):
+ '''override base method to add a change of page template after the firstpage.
+ '''
+ self._handle_pageBegin()
+ self._handle_nextPageTemplate('Later')
+
+ def build(self,flowables,onFirstPage=_doNothing, onLaterPages=_doNothing, canvasmaker=canvas.Canvas):
+ """build the document using the flowables. Annotate the first page using the onFirstPage
+ function and later pages using the onLaterPages function. The onXXX pages should follow
+ the signature
+
+ def myOnFirstPage(canvas, document):
+ # do annotations and modify the document
+ ...
+
+ The functions can do things like draw logos, page numbers,
+ footers, etcetera. They can use external variables to vary
+ the look (for example providing page numbering or section names).
+ """
+ self._calc() #in case we changed margins sizes etc
+ frameT = Frame(self.leftMargin, self.bottomMargin, self.width, self.height, id='normal')
+ self.addPageTemplates([PageTemplate(id='First',frames=frameT, onPage=onFirstPage,pagesize=self.pagesize),
+ PageTemplate(id='Later',frames=frameT, onPage=onLaterPages,pagesize=self.pagesize)])
+ if onFirstPage is _doNothing and hasattr(self,'onFirstPage'):
+ self.pageTemplates[0].beforeDrawPage = self.onFirstPage
+ if onLaterPages is _doNothing and hasattr(self,'onLaterPages'):
+ self.pageTemplates[1].beforeDrawPage = self.onLaterPages
+ BaseDocTemplate.build(self,flowables, canvasmaker=canvasmaker)
+
+def progressCB(typ, value):
+ """Example prototype for progress monitoring.
+
+ This aims to provide info about what is going on
+ during a big job. It should enable, for example, a reasonably
+ smooth progress bar to be drawn. We design the argument
+ signature to be predictable and conducive to programming in
+ other (type safe) languages. If set, this will be called
+ repeatedly with pairs of values. The first is a string
+ indicating the type of call; the second is a numeric value.
+
+ typ 'STARTING', value = 0
+ typ 'SIZE_EST', value = numeric estimate of job size
+ typ 'PASS', value = number of this rendering pass
+ typ 'PROGRESS', value = number between 0 and SIZE_EST
+ typ 'PAGE', value = page number of page
+ type 'FINISHED', value = 0
+
+ The sequence is
+ STARTING - always called once
+ SIZE_EST - always called once
+ PROGRESS - called often
+ PAGE - called often when page is emitted
+ FINISHED - called when really, really finished
+
+ some juggling is needed to accurately estimate numbers of
+ pages in pageDrawing mode.
+
+ NOTE: the SIZE_EST is a guess. It is possible that the
+ PROGRESS value may slightly exceed it, or may even step
+ back a little on rare occasions. The only way to be
+ really accurate would be to do two passes, and I don't
+ want to take that performance hit.
+ """
+ print 'PROGRESS MONITOR: %-10s %d' % (typ, value)
+
+if __name__ == '__main__':
+
+ def myFirstPage(canvas, doc):
+ from reportlab.lib.colors import red
+ PAGE_HEIGHT = canvas._pagesize[1]
+ canvas.saveState()
+ canvas.setStrokeColor(red)
+ canvas.setLineWidth(5)
+ canvas.line(66,72,66,PAGE_HEIGHT-72)
+ canvas.setFont('Times-Bold',24)
+ canvas.drawString(108, PAGE_HEIGHT-108, "TABLE OF CONTENTS DEMO")
+ canvas.setFont('Times-Roman',12)
+ canvas.drawString(4 * inch, 0.75 * inch, "First Page")
+ canvas.restoreState()
+
+ def myLaterPages(canvas, doc):
+ from reportlab.lib.colors import red
+ PAGE_HEIGHT = canvas._pagesize[1]
+ canvas.saveState()
+ canvas.setStrokeColor(red)
+ canvas.setLineWidth(5)
+ canvas.line(66,72,66,PAGE_HEIGHT-72)
+ canvas.setFont('Times-Roman',12)
+ canvas.drawString(4 * inch, 0.75 * inch, "Page %d" % doc.page)
+ canvas.restoreState()
+
+ def run():
+ objects_to_draw = []
+ from reportlab.lib.styles import ParagraphStyle
+ #from paragraph import Paragraph
+ from doctemplate import SimpleDocTemplate
+
+ #need a style
+ normal = ParagraphStyle('normal')
+ normal.firstLineIndent = 18
+ normal.spaceBefore = 6
+ from reportlab.lib.randomtext import randomText
+ import random
+ for i in range(15):
+ height = 0.5 + (2*random.random())
+ box = XBox(6 * inch, height * inch, 'Box Number %d' % i)
+ objects_to_draw.append(box)
+ para = Paragraph(randomText(), normal)
+ objects_to_draw.append(para)
+
+ SimpleDocTemplate('doctemplate.pdf').build(objects_to_draw,
+ onFirstPage=myFirstPage,onLaterPages=myLaterPages)
+
+ run()