diff --git a/bin/reportlab/platypus/__init__.py b/bin/reportlab/platypus/__init__.py new file mode 100755 index 00000000000..7a410633350 --- /dev/null +++ b/bin/reportlab/platypus/__init__.py @@ -0,0 +1,15 @@ +#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/__init__.py +__version__=''' $Id: __init__.py 2775 2006-02-15 12:17:24Z rgbecker $ ''' +__doc__='' +from reportlab.platypus.flowables import Flowable, Image, Macro, PageBreak, Preformatted, Spacer, XBox, \ + CondPageBreak, KeepTogether, TraceInfo, FailOnWrap, FailOnDraw, PTOContainer, \ + KeepInFrame, ParagraphAndImage, ImageAndFlowables +from reportlab.platypus.paragraph import Paragraph, cleanBlockQuotedText, ParaLines +from reportlab.platypus.paraparser import ParaFrag +from reportlab.platypus.tables import Table, TableStyle, CellStyle, LongTable +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate import BaseDocTemplate, NextPageTemplate, PageTemplate, ActionFlowable, \ + SimpleDocTemplate, FrameBreak, PageBegin, Indenter +from xpreformatted import XPreformatted diff --git a/bin/reportlab/platypus/doctemplate.py b/bin/reportlab/platypus/doctemplate.py new file mode 100644 index 00000000000..f38d6506b1f --- /dev/null +++ b/bin/reportlab/platypus/doctemplate.py @@ -0,0 +1,953 @@ +#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: doctemplate.py 2852 2006-05-08 15:04:15Z rgbecker $ ''' + +__doc__=""" +This module contains the core structure of platypus. + +Platypus 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 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 + try: + apply(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): + 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 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, + '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 + + 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 + ''' + #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 + cyc = self._nextPageTemplateCycle + idx = self._nextPageTemplateIndex + self.pageTemplate = cyc[idx] #which is one of the ones in the list anyway + #bump up by 1 + self._nextPageTemplateIndex = (idx + 1) % len(cyc) + 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._leftExtraIndent = self.frame._leftExtraIndent + self._rightExtraIndent = self.frame._rightExtraIndent + + 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(self.frame,'lastFrame') or self.frame is self.pageTemplate.frames[-1]: + self.handle_pageEnd() + self.frame = None + else: + f = self.frame + 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 + cycle = [] + for templateName in pt: + found = 0 + for t in self.pageTemplates: + if t.id == templateName: + cycle.append(t) + found = 1 + if not found: + raise ValueError("Cannot find page template called %s" % templateName) + #double-check all of them are there... + + first = cycle[0] + #ensure we start on the first one + self._nextPageTemplateCycle = cycle + self._nextPageTemplateIndex = 0 #indexes into the cycle + 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')"%fx + 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 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 + + +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): + 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): + 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() diff --git a/bin/reportlab/platypus/figures.py b/bin/reportlab/platypus/figures.py new file mode 100644 index 00000000000..00aeef7c898 --- /dev/null +++ b/bin/reportlab/platypus/figures.py @@ -0,0 +1,372 @@ +#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/figures.py +"""This includes some demos of platypus for use in the API proposal""" +__version__=''' $Id: figures.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +import os + +from reportlab.lib import colors +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.utils import recursiveImport +from reportlab.platypus import Frame +from reportlab.platypus import Flowable +from reportlab.platypus import Paragraph +from reportlab.lib.units import inch +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER +from reportlab.lib.validators import isColor +from reportlab.lib.colors import toColor + +captionStyle = ParagraphStyle('Caption', fontName='Times-Italic', fontSize=10, alignment=TA_CENTER) + +class Figure(Flowable): + def __init__(self, width, height, caption="", + captionFont="Times-Italic", captionSize=12, + background=None, + captionTextColor=toColor('black'), + captionBackColor=None, + border=1): + Flowable.__init__(self) + self.width = width + self.figureHeight = height + self.caption = caption + self.captionFont = captionFont + self.captionSize = captionSize + self.captionTextColor = captionTextColor + self.captionBackColor = captionBackColor + self._captionData = None + self.captionHeight = 0 # work out later + self.background = background + self.border = border + self.spaceBefore = 12 + self.spaceAfter = 12 + + def _getCaptionPara(self): + caption = self.caption + captionFont = self.captionFont + captionSize = self.captionSize + captionTextColor = self.captionTextColor + captionBackColor = self.captionBackColor + if self._captionData!=(caption,captionFont,captionSize,captionTextColor,captionBackColor): + self._captionData = (caption,captionFont,captionSize,captionTextColor,captionBackColor) + self.captionStyle = ParagraphStyle( + 'Caption', + fontName=captionFont, + fontSize=captionSize, + leading=1.2*captionSize, + textColor = captionTextColor, + backColor = captionBackColor, + spaceBefore=captionSize * 0.5, + alignment=TA_CENTER) + #must build paragraph now to get sequencing in synch with rest of story + self.captionPara = Paragraph(self.caption, self.captionStyle) + + def wrap(self, availWidth, availHeight): + # try to get the caption aligned + if self.caption: + self._getCaptionPara() + (w, h) = self.captionPara.wrap(self.width, availHeight - self.figureHeight) + self.captionHeight = h + self.height = self.captionHeight + self.figureHeight + if w>self.width: self.width = w + else: + self.height = self.figureHeight + self.dx = 0.5 * (availWidth - self.width) + return (self.width, self.height) + + def draw(self): + self.canv.translate(self.dx, 0) + if self.caption: + self._getCaptionPara() + self.drawCaption() + self.canv.translate(0, self.captionHeight) + if self.background: + self.drawBackground() + if self.border: + self.drawBorder() + self.drawFigure() + + def drawBorder(self): + self.canv.rect(0, 0, self.width, self.figureHeight) + + def _doBackground(self, color): + self.canv.saveState() + self.canv.setFillColor(self.background) + self.canv.rect(0, 0, self.width, self.figureHeight, fill=1) + self.canv.restoreState() + + def drawBackground(self): + """For use when using a figure on a differently coloured background. + Allows you to specify a colour to be used as a background for the figure.""" + if isColor(self.background): + self._doBackground(self.background) + else: + try: + c = toColor(self.background) + self._doBackground(c) + except: + pass + + def drawCaption(self): + self.captionPara.drawOn(self.canv, 0, 0) + + def drawFigure(self): + pass + +def drawPage(canvas,x, y, width, height): + #draws something which looks like a page + pth = canvas.beginPath() + corner = 0.05*width + + # shaded backdrop offset a little + canvas.setFillColorRGB(0.5,0.5,0.5) + canvas.rect(x + corner, y - corner, width, height, stroke=0, fill=1) + + #'sheet of paper' in light yellow + canvas.setFillColorRGB(1,1,0.9) + canvas.setLineWidth(0) + canvas.rect(x, y, width, height, stroke=1, fill=1) + + #reset + canvas.setFillColorRGB(0,0,0) + canvas.setStrokeColorRGB(0,0,0) + +class PageFigure(Figure): + """Shows a blank page in a frame, and draws on that. Used in + illustrations of how PLATYPUS works.""" + def __init__(self, background=None): + Figure.__init__(self, 3*inch, 3*inch) + self.caption = 'Figure 1 - a blank page' + self.captionStyle = captionStyle + self.background = background + + def drawVirtualPage(self): + pass + + def drawFigure(self): + drawPage(self.canv, 0.625*inch, 0.25*inch, 1.75*inch, 2.5*inch) + self.canv.translate(0.625*inch, 0.25*inch) + self.canv.scale(1.75/8.27, 2.5/11.69) + self.drawVirtualPage() + +class PlatPropFigure1(PageFigure): + """This shows a page with a frame on it""" + def __init__(self): + PageFigure.__init__(self) + self.caption = "Figure 1 - a page with a simple frame" + def drawVirtualPage(self): + demo1(self.canv) + +class FlexFigure(Figure): + """Base for a figure class with a caption. Can grow or shrink in proportion""" + def __init__(self, width, height, caption, background=None): + Figure.__init__(self, width, height, caption, + captionFont="Helvetica-Oblique", captionSize=8, + background=None) + self.shrinkToFit = 1 #if set and wrap is too tight, shrinks + self.growToFit = 1 #if set and wrap is too tight, shrinks + self.scaleFactor = None + self.captionStyle = ParagraphStyle( + 'Caption', + fontName='Times', #'Helvetica-Oblique', + fontSize=4, #8, + spaceBefore=9, #3, + alignment=TA_CENTER + ) + self._scaleFactor = None + self.background = background + + def _scale(self,availWidth,availHeight): + "Rescale to fit according to the rules, but only once" + if self._scaleFactor is None or self.width>availWidth or self.height>availHeight: + w, h = Figure.wrap(self, availWidth, availHeight) + captionHeight = h - self.figureHeight + if self.scaleFactor is None: + #scale factor None means auto + self._scaleFactor = min(availWidth/self.width,(availHeight-captionHeight)/self.figureHeight) + else: #they provided a factor + self._scaleFactor = self.scaleFactor + if self._scaleFactor<1 and self.shrinkToFit: + self.width = self.width * self._scaleFactor - 0.0001 + self.figureHeight = self.figureHeight * self._scaleFactor + elif self._scaleFactor>1 and self.growToFit: + self.width = self.width*self._scaleFactor - 0.0001 + self.figureHeight = self.figureHeight * self._scaleFactor + + def wrap(self, availWidth, availHeight): + self._scale(availWidth,availHeight) + return Figure.wrap(self, availWidth, availHeight) + + def split(self, availWidth, availHeight): + self._scale(availWidth,availHeight) + return Figure.split(self, availWidth, availHeight) + +class ImageFigure(FlexFigure): + """Image with a caption below it""" + def __init__(self, filename, caption, background=None): + assert os.path.isfile(filename), 'image file %s not found' % filename + from reportlab.lib.utils import ImageReader + w, h = ImageReader(filename).getSize() + self.filename = filename + FlexFigure.__init__(self, w, h, caption, background) + + def drawFigure(self): + self.canv.drawInlineImage(self.filename, + 0, 0,self.width, self.figureHeight) + +class DrawingFigure(FlexFigure): + """Drawing with a caption below it. Clunky, scaling fails.""" + def __init__(self, modulename, classname, caption, baseDir=None, background=None): + module = recursiveImport(modulename, baseDir) + klass = getattr(module, classname) + self.drawing = klass() + FlexFigure.__init__(self, + self.drawing.width, + self.drawing.height, + caption, + background) + self.growToFit = 1 + + def drawFigure(self): + self.canv.scale(self._scaleFactor, self._scaleFactor) + self.drawing.drawOn(self.canv, 0, 0) + + +try: + from rlextra.pageCatcher.pageCatcher import restoreForms, storeForms, storeFormsInMemory, restoreFormsInMemory + _hasPageCatcher = 1 +except ImportError: + _hasPageCatcher = 0 +if _hasPageCatcher: + #################################################################### + # + # PageCatcher plugins + # These let you use our PageCatcher product to add figures + # to other documents easily. + #################################################################### + class PageCatcherCachingMixIn: + "Helper functions to cache pages for figures" + + def getFormName(self, pdfFileName, pageNo): + #naming scheme works within a directory only + dirname, filename = os.path.split(pdfFileName) + root, ext = os.path.splitext(filename) + return '%s_page%d' % (root, pageNo) + + + def needsProcessing(self, pdfFileName, pageNo): + "returns 1 if no forms or form is older" + formName = self.getFormName(pdfFileName, pageNo) + if os.path.exists(formName + '.frm'): + formModTime = os.stat(formName + '.frm')[8] + pdfModTime = os.stat(pdfFileName)[8] + return (pdfModTime > formModTime) + else: + return 1 + + def processPDF(self, pdfFileName, pageNo): + formName = self.getFormName(pdfFileName, pageNo) + storeForms(pdfFileName, formName + '.frm', + prefix= formName + '_', + pagenumbers=[pageNo]) + #print 'stored %s.frm' % formName + return formName + '.frm' + + class cachePageCatcherFigureNonA4(FlexFigure, PageCatcherCachingMixIn): + """PageCatcher page with a caption below it. Size to be supplied.""" + # This should merge with PageFigure into one class that reuses + # form information to determine the page orientation... + def __init__(self, filename, pageNo, caption, width, height, background=None): + self.dirname, self.filename = os.path.split(filename) + if self.dirname == '': + self.dirname = os.curdir + self.pageNo = pageNo + self.formName = self.getFormName(self.filename, self.pageNo) + '_' + str(pageNo) + FlexFigure.__init__(self, width, height, caption, background) + + def drawFigure(self): + self.canv.saveState() + if not self.canv.hasForm(self.formName): + restorePath = self.dirname + os.sep + self.filename + #does the form file exist? if not, generate it. + formFileName = self.getFormName(restorePath, self.pageNo) + '.frm' + if self.needsProcessing(restorePath, self.pageNo): + #print 'preprocessing PDF %s page %s' % (restorePath, self.pageNo) + self.processPDF(restorePath, self.pageNo) + names = restoreForms(formFileName, self.canv) + self.canv.scale(self._scaleFactor, self._scaleFactor) + self.canv.doForm(self.formName) + self.canv.restoreState() + + class cachePageCatcherFigure(cachePageCatcherFigureNonA4): + """PageCatcher page with a caption below it. Presumes A4, Portrait. + This needs our commercial PageCatcher product, or you'll get a blank.""" + def __init__(self, filename, pageNo, caption, width=595, height=842, background=None): + cachePageCatcherFigureNonA4.__init__(self, filename, pageNo, caption, width, height, background=background) + + class PageCatcherFigureNonA4(FlexFigure): + """PageCatcher page with a caption below it. Size to be supplied.""" + # This should merge with PageFigure into one class that reuses + # form information to determine the page orientation... + _cache = {} + def __init__(self, filename, pageNo, caption, width, height, background=None, caching=None): + fn = self.filename = filename + self.pageNo = pageNo + fn = fn.replace(os.sep,'_').replace('/','_').replace('\\','_').replace('-','_').replace(':','_') + self.prefix = fn.replace('.','_')+'_'+str(pageNo)+'_' + self.formName = self.prefix + str(pageNo) + self.caching = caching + FlexFigure.__init__(self, width, height, caption, background) + + def drawFigure(self): + if not self.canv.hasForm(self.formName): + if self.filename in self._cache: + f,data = self._cache[self.filename] + else: + f = open(self.filename,'rb') + pdf = f.read() + f.close() + f, data = storeFormsInMemory(pdf, pagenumbers=[self.pageNo], prefix=self.prefix) + if self.caching=='memory': + self._cache[self.filename] = f, data + f = restoreFormsInMemory(data, self.canv) + self.canv.saveState() + self.canv.scale(self._scaleFactor, self._scaleFactor) + self.canv.doForm(self.formName) + self.canv.restoreState() + + class PageCatcherFigure(PageCatcherFigureNonA4): + """PageCatcher page with a caption below it. Presumes A4, Portrait. + This needs our commercial PageCatcher product, or you'll get a blank.""" + def __init__(self, filename, pageNo, caption, width=595, height=842, background=None, caching=None): + PageCatcherFigureNonA4.__init__(self, filename, pageNo, caption, width, height, background=background, caching=caching) + +def demo1(canvas): + frame = Frame( + 2*inch, # x + 4*inch, # y at bottom + 4*inch, # width + 5*inch, # height + showBoundary = 1 # helps us see what's going on + ) + bodyStyle = ParagraphStyle('Body', fontName='Times-Roman', fontSize=24, leading=28, spaceBefore=6) + para1 = Paragraph('Spam spam spam spam. ' * 5, bodyStyle) + para2 = Paragraph('Eggs eggs eggs. ' * 5, bodyStyle) + mydata = [para1, para2] + + #this does the packing and drawing. The frame will consume + #items from the front of the list as it prints them + frame.addFromList(mydata,canvas) + +def test1(): + c = Canvas('figures.pdf') + f = Frame(inch, inch, 6*inch, 9*inch, showBoundary=1) + v = PlatPropFigure1() + v.captionTextColor = toColor('blue') + v.captionBackColor = toColor('lightyellow') + f.addFromList([v],c) + c.save() + +if __name__ == '__main__': + test1() diff --git a/bin/reportlab/platypus/flowables.py b/bin/reportlab/platypus/flowables.py new file mode 100644 index 00000000000..f22dd9ee696 --- /dev/null +++ b/bin/reportlab/platypus/flowables.py @@ -0,0 +1,1051 @@ +#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/flowables.py +__version__=''' $Id: flowables.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' +__doc__=""" +A flowable is a "floating element" in a document whose exact position is determined by the +other elements that precede it, such as a paragraph, a diagram interspersed between paragraphs, +a section header, etcetera. Examples of non-flowables include page numbering annotations, +headers, footers, fixed diagrams or logos, among others. + +Flowables are defined here as objects which know how to determine their size and which +can draw themselves onto a page with respect to a relative "origin" position determined +at a higher level. The object's draw() method should assume that (0,0) corresponds to the +bottom left corner of the enclosing rectangle that will contain the object. The attributes +vAlign and hAlign may be used by 'packers' as hints as to how the object should be placed. + +Some Flowables also know how to "split themselves". For example a +long paragraph might split itself between one page and the next. + +Packers should set the canv attribute during wrap, split & draw operations to allow +the flowable to work out sizes etc in the proper context. + +The "text" of a document usually consists mainly of a sequence of flowables which +flow into a document from top to bottom (with column and page breaks controlled by +higher level components). +""" +import os +import string +from copy import deepcopy +from types import ListType, TupleType, StringType + +from reportlab.lib.colors import red, gray, lightgrey +from reportlab.lib.utils import fp_str +from reportlab.pdfbase import pdfutils + +from reportlab.rl_config import _FUZZ, overlapAttachedSpace +__all__=('TraceInfo','Flowable','XBox','Preformatted','Image','Spacer','PageBreak','SlowPageBreak', + 'CondPageBreak','KeepTogether','Macro','CallerMacro','ParagraphAndImage', + 'FailOnWrap','HRFlowable','PTOContainer','KeepInFrame','UseUpSpace') + + +class TraceInfo: + "Holder for info about where an object originated" + def __init__(self): + self.srcFile = '(unknown)' + self.startLineNo = -1 + self.startLinePos = -1 + self.endLineNo = -1 + self.endLinePos = -1 + +############################################################# +# Flowable Objects - a base class and a few examples. +# One is just a box to get some metrics. We also have +# a paragraph, an image and a special 'page break' +# object which fills the space. +############################################################# +class Flowable: + """Abstract base class for things to be drawn. Key concepts: + 1. It knows its size + 2. It draws in its own coordinate system (this requires the + base API to provide a translate() function. + """ + _fixedWidth = 0 #assume wrap results depend on arguments? + _fixedHeight = 0 + + def __init__(self): + self.width = 0 + self.height = 0 + self.wrapped = 0 + + #these are hints to packers/frames as to how the floable should be positioned + self.hAlign = 'LEFT' #CENTER/CENTRE or RIGHT + self.vAlign = 'BOTTOM' #MIDDLE or TOP + + #optional holder for trace info + self._traceInfo = None + self._showBoundary = None + + #many flowables handle text and must be processed in the + #absence of a canvas. tagging them with their encoding + #helps us to get conversions right. Use Python codec names. + self.encoding = None + + + def _drawOn(self,canv): + '''ensure canv is set on and then draw''' + self.canv = canv + self.draw()#this is the bit you overload + del self.canv + + def drawOn(self, canvas, x, y, _sW=0): + "Tell it to draw itself on the canvas. Do not override" + if _sW and hasattr(self,'hAlign'): + a = self.hAlign + if a in ['CENTER','CENTRE']: + x = x + 0.5*_sW + elif a == 'RIGHT': + x = x + _sW + elif a != 'LEFT': + raise ValueError, "Bad hAlign value "+str(a) + canvas.saveState() + canvas.translate(x, y) + self._drawOn(canvas) + if hasattr(self, '_showBoundary') and self._showBoundary: + #diagnostic tool support + canvas.setStrokeColor(gray) + canvas.rect(0,0,self.width, self.height) + canvas.restoreState() + + def wrapOn(self, canv, aW, aH): + '''intended for use by packers allows setting the canvas on + during the actual wrap''' + self.canv = canv + w, h = self.wrap(aW,aH) + del self.canv + return w, h + + def wrap(self, availWidth, availHeight): + """This will be called by the enclosing frame before objects + are asked their size, drawn or whatever. It returns the + size actually used.""" + return (self.width, self.height) + + def minWidth(self): + """This should return the minimum required width""" + return getattr(self,'_minWidth',self.width) + + def splitOn(self, canv, aW, aH): + '''intended for use by packers allows setting the canvas on + during the actual split''' + self.canv = canv + S = self.split(aW,aH) + del self.canv + return S + + def split(self, availWidth, availheight): + """This will be called by more sophisticated frames when + wrap fails. Stupid flowables should return []. Clever flowables + should split themselves and return a list of flowables""" + return [] + + def getKeepWithNext(self): + """returns boolean determining whether the next flowable should stay with this one""" + if hasattr(self,'keepWithNext'): return self.keepWithNext + elif hasattr(self,'style') and hasattr(self.style,'keepWithNext'): return self.style.keepWithNext + else: return 0 + + def getSpaceAfter(self): + """returns how much space should follow this item if another item follows on the same page.""" + if hasattr(self,'spaceAfter'): return self.spaceAfter + elif hasattr(self,'style') and hasattr(self.style,'spaceAfter'): return self.style.spaceAfter + else: return 0 + + def getSpaceBefore(self): + """returns how much space should precede this item if another item precedess on the same page.""" + if hasattr(self,'spaceBefore'): return self.spaceBefore + elif hasattr(self,'style') and hasattr(self.style,'spaceBefore'): return self.style.spaceBefore + else: return 0 + + def isIndexing(self): + """Hook for IndexingFlowables - things which have cross references""" + return 0 + + def identity(self, maxLen=None): + ''' + This method should attempt to return a string that can be used to identify + a particular flowable uniquely. The result can then be used for debugging + and or error printouts + ''' + if hasattr(self, 'getPlainText'): + r = self.getPlainText(identify=1) + elif hasattr(self, 'text'): + r = str(self.text) + else: + r = '...' + if r and maxLen: + r = r[:maxLen] + return "<%s at %s%s>%s" % (self.__class__.__name__, hex(id(self)), self._frameName(), r) + + def _frameName(self): + f = getattr(self,'_frame',None) + if f and f.id: return ' frame=%s' % f.id + return '' + +class XBox(Flowable): + """Example flowable - a box with an x through it and a caption. + This has a known size, so does not need to respond to wrap().""" + def __init__(self, width, height, text = 'A Box'): + Flowable.__init__(self) + self.width = width + self.height = height + self.text = text + + def __repr__(self): + return "XBox(w=%s, h=%s, t=%s)" % (self.width, self.height, self.text) + + def draw(self): + self.canv.rect(0, 0, self.width, self.height) + self.canv.line(0, 0, self.width, self.height) + self.canv.line(0, self.height, self.width, 0) + + #centre the text + self.canv.setFont('Times-Roman',12) + self.canv.drawCentredString(0.5*self.width, 0.5*self.height, self.text) + +def _trimEmptyLines(lines): + #don't want the first or last to be empty + while len(lines) and string.strip(lines[0]) == '': + lines = lines[1:] + while len(lines) and string.strip(lines[-1]) == '': + lines = lines[:-1] + return lines + +def _dedenter(text,dedent=0): + ''' + tidy up text - carefully, it is probably code. If people want to + indent code within a source script, you can supply an arg to dedent + and it will chop off that many character, otherwise it leaves + left edge intact. + ''' + lines = string.split(text, '\n') + if dedent>0: + templines = _trimEmptyLines(lines) + lines = [] + for line in templines: + line = string.rstrip(line[dedent:]) + lines.append(line) + else: + lines = _trimEmptyLines(lines) + + return lines + +class Preformatted(Flowable): + """This is like the HTML
 tag.
+    It attempts to display text exactly as you typed it in a fixed width "typewriter" font.
+    The line breaks are exactly where you put
+    them, and it will not be wrapped."""
+    def __init__(self, text, style, bulletText = None, dedent=0):
+        """text is the text to display. If dedent is set then common leading space
+        will be chopped off the front (for example if the entire text is indented
+        6 spaces or more then each line will have 6 spaces removed from the front).
+        """
+        self.style = style
+        self.bulletText = bulletText
+        self.lines = _dedenter(text,dedent)
+
+    def __repr__(self):
+        bT = self.bulletText
+        H = "Preformatted("
+        if bT is not None:
+            H = "Preformatted(bulletText=%s," % repr(bT)
+        return "%s'''\\ \n%s''')" % (H, string.join(self.lines,'\n'))
+
+    def wrap(self, availWidth, availHeight):
+        self.width = availWidth
+        self.height = self.style.leading*len(self.lines)
+        return (self.width, self.height)
+
+    def split(self, availWidth, availHeight):
+        #returns two Preformatted objects
+
+        #not sure why they can be called with a negative height
+        if availHeight < self.style.leading:
+            return []
+
+        linesThatFit = int(availHeight * 1.0 / self.style.leading)
+
+        text1 = string.join(self.lines[0:linesThatFit], '\n')
+        text2 = string.join(self.lines[linesThatFit:], '\n')
+        style = self.style
+        if style.firstLineIndent != 0:
+            style = deepcopy(style)
+            style.firstLineIndent = 0
+        return [Preformatted(text1, self.style), Preformatted(text2, style)]
+
+
+    def draw(self):
+        #call another method for historical reasons.  Besides, I
+        #suspect I will be playing with alternate drawing routines
+        #so not doing it here makes it easier to switch.
+
+        cur_x = self.style.leftIndent
+        cur_y = self.height - self.style.fontSize
+        self.canv.addLiteral('%PreformattedPara')
+        if self.style.textColor:
+            self.canv.setFillColor(self.style.textColor)
+        tx = self.canv.beginText(cur_x, cur_y)
+        #set up the font etc.
+        tx.setFont( self.style.fontName,
+                    self.style.fontSize,
+                    self.style.leading)
+
+        for text in self.lines:
+            tx.textLine(text)
+        self.canv.drawText(tx)
+
+class Image(Flowable):
+    """an image (digital picture).  Formats supported by PIL/Java 1.4 (the Python/Java Imaging Library
+       are supported.  At the present time images as flowables are always centered horozontally
+       in the frame. We allow for two kinds of lazyness to allow for many images in a document
+       which could lead to file handle starvation.
+       lazy=1 don't open image until required.
+       lazy=2 open image when required then shut it.
+    """
+    _fixedWidth = 1
+    _fixedHeight = 1
+    def __init__(self, filename, width=None, height=None, kind='direct', mask="auto", lazy=1):
+        """If size to draw at not specified, get it from the image."""
+        self.hAlign = 'CENTER'
+        self._mask = mask
+        # if it is a JPEG, will be inlined within the file -
+        # but we still need to know its size now
+        fp = hasattr(filename,'read')
+        if fp:
+            self._file = filename
+            self.filename = `filename`
+        else:
+            self._file = self.filename = filename
+        if not fp and os.path.splitext(filename)[1] in ['.jpg', '.JPG', '.jpeg', '.JPEG']:
+            from reportlab.lib.utils import open_for_read
+            f = open_for_read(filename, 'b')
+            info = pdfutils.readJPEGInfo(f)
+            f.close()
+            self.imageWidth = info[0]
+            self.imageHeight = info[1]
+            self._img = None
+            self._setup(width,height,kind,0)
+        elif fp:
+            self._setup(width,height,kind,0)
+        else:
+            self._setup(width,height,kind,lazy)
+
+    def _setup(self,width,height,kind,lazy):
+        self._lazy = lazy
+        self._width = width
+        self._height = height
+        self._kind = kind
+        if lazy<=0: self._setup_inner()
+
+    def _setup_inner(self):
+        width = self._width
+        height = self._height
+        kind = self._kind
+        img = self._img
+        if img: self.imageWidth, self.imageHeight = img.getSize()
+        if self._lazy>=2: del self._img
+        if kind in ['direct','absolute']:
+            self.drawWidth = width or self.imageWidth
+            self.drawHeight = height or self.imageHeight
+        elif kind in ['percentage','%']:
+            self.drawWidth = self.imageWidth*width*0.01
+            self.drawHeight = self.imageHeight*height*0.01
+        elif kind in ['bound','proportional']:
+            factor = min(float(width)/self.imageWidth,float(height)/self.imageHeight)
+            self.drawWidth = self.imageWidth*factor
+            self.drawHeight = self.imageHeight*factor
+
+    def __getattr__(self,a):
+        if a=='_img':
+            from reportlab.lib.utils import ImageReader  #this may raise an error
+            self._img = ImageReader(self._file)
+            del self._file
+            return self._img
+        elif a in ('drawWidth','drawHeight','imageWidth','imageHeight'):
+            self._setup_inner()
+            return self.__dict__[a]
+        raise AttributeError(a)
+
+    def wrap(self, availWidth, availHeight):
+        #the caller may decide it does not fit.
+        return (self.drawWidth, self.drawHeight)
+
+    def draw(self):
+        lazy = self._lazy
+        if lazy>=2: self._lazy = 1
+        self.canv.drawImage(    self._img or self.filename,
+                                0,
+                                0,
+                                self.drawWidth,
+                                self.drawHeight,
+                                mask=self._mask,
+                                )
+        if lazy>=2:
+            self._img = None
+            self._lazy = lazy
+
+    def identity(self,maxLen=None):
+        r = Flowable.identity(self,maxLen)
+        if r[-4:]=='>...' and type(self.filename) is StringType:
+            r = "%s filename=%s>" % (r[:-4],self.filename)
+        return r
+
+class Spacer(Flowable):
+    """A spacer just takes up space and doesn't draw anything - it guarantees
+       a gap between objects."""
+    _fixedWidth = 1
+    _fixedHeight = 1
+    def __init__(self, width, height):
+        self.width = width
+        self.height = height
+
+    def __repr__(self):
+        return "%s(%s, %s)" % (self.__class__.__name__,self.width, self.height)
+
+    def draw(self):
+        pass
+
+class UseUpSpace(Flowable):
+    def __init__(self):
+        pass
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    def wrap(self, availWidth, availHeight):
+        self.width = availWidth
+        self.height = availHeight
+        return (availWidth,availHeight-1e-8)  #step back a point
+
+    def draw(self):
+        pass
+
+class PageBreak(UseUpSpace):
+    """Move on to the next page in the document.
+       This works by consuming all remaining space in the frame!"""
+
+class SlowPageBreak(PageBreak):
+    pass
+
+class CondPageBreak(Spacer):
+    """Throw a page if not enough vertical space"""
+    def __init__(self, height):
+        self.height = height
+
+    def __repr__(self):
+        return "CondPageBreak(%s)" %(self.height,)
+
+    def wrap(self, availWidth, availHeight):
+        if availHeightaH and (not self._maxHeight or aH>self._maxHeight)
+        C1 = self._H0>aH
+        if C0 or C1:
+            if C0:
+                from doctemplate import FrameBreak
+                A = FrameBreak
+            else:
+                from doctemplate import NullActionFlowable
+                A = NullActionFlowable
+            S.insert(0,A())
+        return S
+
+    def identity(self, maxLen=None):
+        msg = " containing :%s" % (hex(id(self)),self._frameName(),"\n".join([f.identity() for f in self._content]))
+        if maxLen:
+            return msg[0:maxLen]
+        else:
+            return msg
+
+class Macro(Flowable):
+    """This is not actually drawn (i.e. it has zero height)
+    but is executed when it would fit in the frame.  Allows direct
+    access to the canvas through the object 'canvas'"""
+    def __init__(self, command):
+        self.command = command
+    def __repr__(self):
+        return "Macro(%s)" % repr(self.command)
+    def wrap(self, availWidth, availHeight):
+        return (0,0)
+    def draw(self):
+        exec self.command in globals(), {'canvas':self.canv}
+
+class CallerMacro(Flowable):
+    '''
+    like Macro, but with callable command(s)
+    drawCallable(self)
+    wrapCallable(self,aW,aH)
+    '''
+    def __init__(self, drawCallable=None, wrapCallable=None):
+        _ = lambda *args: None
+        self._drawCallable = drawCallable or _
+        self._wrapCallable = wrapCallable or _
+    def __repr__(self):
+        return "CallerMacro(%s)" % repr(self.command)
+    def wrap(self, aW, aH):
+        self._wrapCallable(self,aW,aH)
+        return (0,0)
+    def draw(self):
+        self._drawCallable(self)
+
+class ParagraphAndImage(Flowable):
+    '''combine a Paragraph and an Image'''
+    def __init__(self,P,I,xpad=3,ypad=3,side='right'):
+        self.P = P
+        self.I = I
+        self.xpad = xpad
+        self.ypad = ypad
+        self._side = side
+
+    def getSpaceBefore(self):
+        return max(self.P.getSpaceBefore(),self.I.getSpaceBefore())
+
+    def getSpaceAfter(self):
+        return max(self.P.getSpaceAfter(),self.I.getSpaceAfter())
+
+    def wrap(self,availWidth,availHeight):
+        wI, hI = self.I.wrap(availWidth,availHeight)
+        self.wI = wI
+        self.hI = hI
+        # work out widths array for breaking
+        self.width = availWidth
+        P = self.P
+        style = P.style
+        xpad = self.xpad
+        ypad = self.ypad
+        leading = style.leading
+        leftIndent = style.leftIndent
+        later_widths = availWidth - leftIndent - style.rightIndent
+        intermediate_widths = later_widths - xpad - wI
+        first_line_width = intermediate_widths - style.firstLineIndent
+        P.width = 0
+        nIW = int((hI+ypad)/leading)
+        P.blPara = P.breakLines([first_line_width] + nIW*[intermediate_widths]+[later_widths])
+        if self._side=='left':
+            self._offsets = [wI+xpad]*(1+nIW)+[0]
+        P.height = len(P.blPara.lines)*leading
+        self.height = max(hI,P.height)
+        return (self.width, self.height)
+
+    def split(self,availWidth, availHeight):
+        P, wI, hI, ypad = self.P, self.wI, self.hI, self.ypad
+        if hI+ypad>availHeight or len(P.frags)<=0: return []
+        S = P.split(availWidth,availHeight)
+        if not S: return S
+        P = self.P = S[0]
+        del S[0]
+        style = P.style
+        P.height = len(self.P.blPara.lines)*style.leading
+        self.height = max(hI,P.height)
+        return [self]+S
+
+    def draw(self):
+        canv = self.canv
+        if self._side=='left':
+            self.I.drawOn(canv,0,self.height-self.hI)
+            self.P._offsets = self._offsets
+            try:
+                self.P.drawOn(canv,0,0)
+            finally:
+                del self.P._offsets
+        else:
+            self.I.drawOn(canv,self.width-self.wI-self.xpad,self.height-self.hI)
+            self.P.drawOn(canv,0,0)
+
+class FailOnWrap(Flowable):
+    def wrap(self, availWidth, availHeight):
+        raise ValueError("FailOnWrap flowable wrapped and failing as ordered!")
+
+    def draw(self):
+        pass
+
+class FailOnDraw(Flowable):
+    def wrap(self, availWidth, availHeight):
+        return (0,0)
+
+    def draw(self):
+        raise ValueError("FailOnDraw flowable drawn, and failing as ordered!")
+
+class HRFlowable(Flowable):
+    '''Like the hr tag'''
+    def __init__(self,
+            width="80%",
+            thickness=1,
+            lineCap='round',
+            color=lightgrey,
+            spaceBefore=1, spaceAfter=1,
+            hAlign='CENTER', vAlign='BOTTOM',
+            dash=None):
+        Flowable.__init__(self)
+        self.width = width
+        self.lineWidth = thickness
+        self.lineCap=lineCap
+        self.spaceBefore = spaceBefore
+        self.spaceAfter = spaceAfter
+        self.color = color
+        self.hAlign = hAlign
+        self.vAlign = vAlign
+        self.dash = dash
+
+    def __repr__(self):
+        return "HRFlowable(width=%s, height=%s)" % (self.width, self.height)
+
+    def wrap(self, availWidth, availHeight):
+        w = self.width
+        if type(w) is type(''):
+            w = w.strip()
+            if w.endswith('%'): w = availWidth*float(w[:-1])*0.01
+            else: w = float(w)
+        w = min(w,availWidth)
+        self._width = w
+        return w, self.lineWidth
+
+    def draw(self):
+        canv = self.canv
+        canv.saveState()
+        canv.setLineWidth(self.lineWidth)
+        canv.setLineCap({'butt':0,'round':1, 'square': 2}[self.lineCap.lower()])
+        canv.setStrokeColor(self.color)
+        if self.dash: canv.setDash(self.dash)
+        canv.line(0, 0, self._width, self.height)
+        canv.restoreState()
+
+class _PTOInfo:
+    def __init__(self,trailer,header):
+        self.trailer = _flowableSublist(trailer)
+        self.header = _flowableSublist(header)
+
+class _Container(_ContainerSpace):  #Abstract some common container like behaviour
+    def drawOn(self, canv, x, y, _sW=0, scale=1.0, content=None, aW=None):
+        '''we simulate being added to a frame'''
+        pS = 0
+        if aW is None: aW = self.width
+        aW = scale*(aW+_sW)
+        if content is None:
+            content = self._content
+        y += self.height*scale
+        for c in content:
+            w, h = c.wrapOn(canv,aW,0xfffffff)
+            if w<_FUZZ or h<_FUZZ: continue
+            if c is not content[0]: h += max(c.getSpaceBefore()-pS,0)
+            y -= h
+            c.drawOn(canv,x,y,_sW=aW-w)
+            if c is not content[-1]:
+                pS = c.getSpaceAfter()
+                y -= pS
+
+class PTOContainer(_Container,Flowable):
+    '''PTOContainer(contentList,trailerList,headerList)
+    
+    A container for flowables decorated with trailer & header lists.
+    If the split operation would be called then the trailer and header
+    lists are injected before and after the split. This allows specialist
+    "please turn over" and "continued from previous" like behaviours.''' 
+    def __init__(self,content,trailer=None,header=None):
+        I = _PTOInfo(trailer,header)
+        self._content = C = []
+        for _ in _flowableSublist(content):
+            if isinstance(_,PTOContainer):
+                C.extend(_._content)
+            else:
+                C.append(_)
+                if not hasattr(_,'_ptoinfo'): _._ptoinfo = I
+
+    def wrap(self,availWidth,availHeight):
+        self.width, self.height = _listWrapOn(self._content,availWidth,self.canv)
+        return self.width,self.height
+
+    def split(self, availWidth, availHeight):
+        if availHeight<0: return []
+        canv = self.canv
+        C = self._content
+        x = i = H = pS = hx = 0
+        n = len(C)
+        I2W = {}
+        for x in xrange(n):
+            c = C[x]
+            I = c._ptoinfo
+            if I not in I2W.keys():
+                T = I.trailer
+                Hdr = I.header
+                tW, tH = _listWrapOn(T, availWidth, self.canv)
+                tSB = T[0].getSpaceBefore()
+                I2W[I] = T,tW,tH,tSB
+            else:
+                T,tW,tH,tSB = I2W[I]
+            _, h = c.wrapOn(canv,availWidth,0xfffffff)
+            if x:
+                hx = max(c.getSpaceBefore()-pS,0)
+                h += hx
+            pS = c.getSpaceAfter()
+            H += h+pS
+            tHS = tH+max(tSB,pS)
+            if H+tHS>=availHeight-_FUZZ: break
+            i += 1
+
+        #first retract last thing we tried
+        H -= (h+pS)
+
+        #attempt a sub split on the last one we have
+        aH = (availHeight-H-tHS-hx)*0.99999
+        if aH>=0.05*availHeight:
+            SS = c.splitOn(canv,availWidth,aH)
+        else:
+            SS = []
+        F = [UseUpSpace()]
+
+        if len(SS)>1:
+            R1 = C[:i] + SS[:1] + T + F
+            R2 = Hdr + SS[1:]+C[i+1:]
+        elif not i:
+            return []
+        else:
+            R1 = C[:i]+T+F
+            R2 = Hdr + C[i:]
+        T =  R1 + [PTOContainer(R2,deepcopy(I.trailer),deepcopy(I.header))]
+        return T
+
+#utility functions used by KeepInFrame
+def _hmodel(s0,s1,h0,h1):
+    # calculate the parameters in the model
+    # h = a/s**2 + b/s
+    a11 = 1./s0**2
+    a12 = 1./s0
+    a21 = 1./s1**2
+    a22 = 1./s1
+    det = a11*a22-a12*a21
+    b11 = a22/det
+    b12 = -a12/det
+    b21 = -a21/det
+    b22 = a11/det
+    a = b11*h0+b12*h1
+    b = b21*h0+b22*h1
+    return a,b
+
+def _qsolve(h,(a,b)):
+    '''solve the model v = a/s**2 + b/s for an s which gives us v==h'''
+    if abs(a)<=_FUZZ:
+        return b/h
+    t = 0.5*b/a
+    from math import sqrt
+    f = -h/a
+    r = t*t-f
+    if r<0: return None
+    r = sqrt(r)
+    if t>=0:
+        s1 = -t - r 
+    else:
+        s1 = -t + r
+    s2 = f/s1
+    return max(1./s1, 1./s2)
+
+class KeepInFrame(_Container,Flowable):
+    def __init__(self, maxWidth, maxHeight, content=[], mergeSpace=1, mode='shrink', name=''):
+        '''mode describes the action to take when overflowing
+            error       raise an error in the normal way
+            continue    ignore ie just draw it and report maxWidth, maxHeight
+            shrink      shrinkToFit
+            truncate    fit as much as possible
+        '''
+        self.name = name
+        self.maxWidth = maxWidth
+        self.maxHeight = maxHeight
+        self.mode = mode
+        assert mode in ('error','overflow','shrink','truncate'), '%s invalid mode value %s' % (self.identity(),mode)
+        assert maxHeight>=0,  '%s invalid maxHeight value %s' % (self.identity(),maxHeight)
+        if mergeSpace is None: mergeSpace = overlapAttachedSpace
+        self.mergespace = mergeSpace
+        self._content = content
+
+    def _getAvailableWidth(self):
+        return self.maxWidth - self._leftExtraIndent - self._rightExtraIndent
+
+    def identity(self, maxLen=None):
+        return "<%s at %s%s%s> size=%sx%s" % (self.__class__.__name__, hex(id(self)), self._frameName(),
+                getattr(self,'name','') and (' name="%s"'% getattr(self,'name','')) or '',
+                getattr(self,'maxWidth','') and (' maxWidth=%s'%fp_str(getattr(self,'maxWidth',0))) or '',
+                getattr(self,'maxHeight','')and (' maxHeight=%s' % fp_str(getattr(self,'maxHeight')))or '')
+
+    def wrap(self,availWidth,availHeight):
+        from doctemplate import LayoutError
+        mode = self.mode
+        maxWidth = float(self.maxWidth or availWidth)
+        maxHeight = float(self.maxHeight or availHeight)
+        W, H = _listWrapOn(self._content,availWidth,self.canv)
+        if (mode=='error' and (W>availWidth+_FUZZ or H>availHeight+_FUZZ)):
+            ident = 'content %sx%s too large for %s' % (W,H,self.identity(30))
+            #leave to keep apart from the raise
+            raise LayoutError(ident)
+        elif W<=availWidth+_FUZZ and H<=availHeight+_FUZZ:
+            self.width = W-_FUZZ      #we take what we get
+            self.height = H-_FUZZ
+        elif (maxWidth>=availWidth+_FUZZ or maxHeight>=availHeight+_FUZZ):
+            ident = 'Specified size too large for available space %sx%s in %s' % (availWidth,availHeight,self.identity(30))
+            #leave to keep apart from the raise
+            raise LayoutError(ident)
+        elif mode in ('overflow','truncate'):   #we lie
+            self.width = min(maxWidth,W)-_FUZZ
+            self.height = min(maxHeight,H)-_FUZZ
+        else:
+            def func(x):
+                W, H = _listWrapOn(self._content,x*availWidth,self.canv)
+                W /= x
+                H /= x
+                return W, H
+            W0 = W
+            H0 = H
+            s0 = 1
+            if W>maxWidth+_FUZZ:
+                #squeeze out the excess width and or Height
+                s1 = W/maxWidth
+                W, H = func(s1)
+                if H<=maxHeight+_FUZZ:
+                    self.width = W-_FUZZ
+                    self.height = H-_FUZZ
+                    self._scale = s1
+                    return W,H
+                s0 = s1
+                H0 = H
+                W0 = W
+            s1 = H/maxHeight
+            W, H = func(s1)
+            self.width = W-_FUZZ
+            self.height = H-_FUZZ
+            self._scale = s1
+            if H=maxHeight+_FUZZ:
+                #the standard case W should be OK, H is short we want
+                #to find the smallest s with H<=maxHeight
+                H1 = H
+                for f in 0, 0.01, 0.05, 0.10, 0.15:
+                    #apply the quadratic model
+                    s = _qsolve(maxHeight*(1-f),_hmodel(s0,s1,H0,H1))
+                    W, H = func(s)
+                    if H<=maxHeight+_FUZZ and W<=maxWidth+_FUZZ:
+                        self.width = W-_FUZZ
+                        self.height = H-_FUZZ
+                        self._scale = s
+                        break
+
+        return self.width, self.height
+
+    def drawOn(self, canv, x, y, _sW=0):
+        scale = getattr(self,'_scale',1.0)
+        truncate = self.mode=='truncate'
+        ss = scale!=1.0 or truncate
+        if ss:
+            canv.saveState()
+            if truncate:
+                p = canv.beginPath()
+                p.rect(x, y, self.width,self.height)
+                canv.clipPath(p,stroke=0)
+            else:
+                canv.translate(x,y)
+                x=y=0
+                canv.scale(1.0/scale, 1.0/scale)
+        _Container.drawOn(self, canv, x, y, _sW=_sW, scale=scale)
+        if ss: canv.restoreState()
+
+class ImageAndFlowables(_Container,Flowable):
+    '''combine a list of flowables and an Image'''
+    def __init__(self,I,F,imageLeftPadding=0,imageRightPadding=3,imageTopPadding=0,imageBottomPadding=3,
+                    imageSide='right'):
+        self._content = _flowableSublist(F)
+        self._I = I
+        self._irpad = imageRightPadding
+        self._ilpad = imageLeftPadding
+        self._ibpad = imageBottomPadding
+        self._itpad = imageTopPadding
+        self._side = imageSide
+
+    def getSpaceAfter(self):
+        if hasattr(self,'_C1'):
+            C = self._C1
+        elif hasattr(self,'_C0'):
+            C = self._C0
+        else:
+            C = self._content
+        return _Container.getSpaceAfter(self,C)
+
+    def getSpaceBefore(self):
+        return max(self._I.getSpaceBefore(),_Container.getSpaceBefore(self))
+
+    def _reset(self):
+        for a in ('_wrapArgs','_C0','_C1'):
+            try:
+                delattr(self,a)
+            except:
+                pass
+
+    def wrap(self,availWidth,availHeight):
+        canv = self.canv
+        if hasattr(self,'_wrapArgs'):
+            if self._wrapArgs==(availWidth,availHeight):
+                return self.width,self.height
+            self._reset()
+        self._wrapArgs = availWidth, availHeight
+        wI, hI = self._I.wrap(availWidth,availHeight)
+        self._wI = wI
+        self._hI = hI
+        ilpad = self._ilpad
+        irpad = self._irpad
+        ibpad = self._ibpad
+        itpad = self._itpad
+        self._iW = availWidth - irpad - wI - ilpad
+        aH = itpad + hI + ibpad
+        W,H0,self._C0,self._C1 = self._findSplit(canv,self._iW,aH)
+        self.width = availWidth
+        aH = self._aH = max(aH,H0)
+        if not self._C1:
+            self.height = aH
+        else:
+            W1,H1 = _listWrapOn(self._C1,availWidth,canv)
+            self.height = aH+H1
+        return self.width, self.height
+
+    def split(self,availWidth, availHeight):
+        if hasattr(self,'_wrapArgs'):
+            if self._wrapArgs!=(availWidth,availHeight):
+                self._reset()
+        W,H=self.wrap(availWidth,availHeight)
+        if self._aH>availHeight: return []
+        C1 = self._C1
+        if C1:
+            c0 = C1[0]
+            S = c0.split(availWidth,availHeight-self._aH)
+            if not S:
+                self._C1 = []
+                self.height = self._aH
+            else:
+                self._C1 = [S[0]]
+                self.height = self._aH + S[0].height
+                C1 = S[1:]+C1[1:]
+        else:
+            self._C1 = []
+            self.height = self._aH
+        return [self]+C1
+
+    def drawOn(self, canv, x, y, _sW=0):
+        if self._side=='left':
+            Ix = x + self._ilpad
+            Fx = Ix+ self._irpad + self._wI
+        else:
+            Ix = x + self.width-self._wI-self._irpad - self._ilpad
+            Fx = x
+        self._I.drawOn(canv,Ix,y+self.height-self._itpad-self._hI)
+        _Container.drawOn(self, canv, Fx, y, content=self._C0, aW=self._iW)
+        if self._C1:
+            _Container.drawOn(self, canv, x, y-self._aH,content=self._C1)
+
+    def _findSplit(self,canv,availWidth,availHeight,mergeSpace=1,obj=None):
+        '''return max width, required height for a list of flowables F'''
+        W = 0
+        H = 0
+        pS = sB = 0
+        atTop = 1
+        F = self._content
+        for i,f in enumerate(F):
+            w,h = f.wrapOn(canv,availWidth,0xfffffff)
+            if w<=_FUZZ or h<=_FUZZ: continue
+            W = max(W,w)
+            if not atTop:
+                s = f.getSpaceBefore()
+                if mergeSpace: s = max(s-pS,0)
+                H += s
+            else:
+                if obj is not None: obj._spaceBefore = f.getSpaceBefore()
+                atTop = 0
+            if H>=availHeight:
+                return W, availHeight, F[:i],F[i:]
+            H += h
+            if H>availHeight:
+                from paragraph import Paragraph
+                aH = availHeight-(H-h)
+                if isinstance(f,(Paragraph,Preformatted)):
+                    leading = f.style.leading
+                    nH = leading*int(aH/float(leading))+_FUZZ
+                    if nH0:
+            flowable._frame = self
+            flowable.canv = canv #so they can use stringWidth etc
+            w, h = flowable.wrap(aW, h)
+            del flowable.canv, flowable._frame
+        else:
+            return 0
+
+        h += s
+        y -= h
+
+        if y < p-_FUZZ:
+            if not rl_config.allowTableBoundsErrors and ((h>self._aH or w>aW) and not trySplit):
+                raise "LayoutError", "Flowable %s (%sx%s points) too large for frame (%sx%s points)." % (
+                    flowable.__class__, w,h, aW,self._aH)
+            return 0
+        else:
+            #now we can draw it, and update the current point.
+            flowable._frame = self
+            flowable.drawOn(canv, self._x + self._leftExtraIndent, y, _sW=aW-w)
+            if self._debug: logger.debug('drew %s' % flowable.identity())
+            del flowable._frame
+            s = flowable.getSpaceAfter()
+            y -= s
+            if self._oASpace: self._prevASpace = s
+            if y!=self._y: self._atTop = 0
+            self._y = y
+            return 1
+
+    add = _add
+
+    def split(self,flowable,canv):
+        '''Ask the flowable to split using up the available space.'''
+        y = self._y
+        p = self._y1p
+        s = 0
+        if not self._atTop: s = flowable.getSpaceBefore()
+        flowable.canv = canv    #some flowables might need this
+        r = flowable.split(self._aW, y-p-s)
+        del flowable.canv
+        return r
+
+    def drawBoundary(self,canv):
+        "draw the frame boundary as a rectangle (primarily for debugging)."
+        from reportlab.lib.colors import Color, CMYKColor, toColor
+        sb = self.showBoundary
+        isColor = type(sb) in (type(''),type(()),type([])) or isinstance(sb,Color)
+        if isColor:
+            sb = toColor(sb,self)
+            if sb is self: isColor = 0
+            else:
+                canv.saveState()
+                canv.setStrokeColor(sb)
+        canv.rect(
+                self._x1,
+                self._y1,
+                self._x2 - self._x1,
+                self._y2 - self._y1
+                )
+        if isColor: canv.restoreState()
+
+    def addFromList(self, drawlist, canv):
+        """Consumes objects from the front of the list until the
+        frame is full.  If it cannot fit one object, raises
+        an exception."""
+
+        if self._debug: logger.debug("enter Frame.addFromlist() for frame %s" % self.id)
+        if self.showBoundary:
+            self.drawBoundary(canv)
+
+        while len(drawlist) > 0:
+            head = drawlist[0]
+            if self.add(head,canv,trySplit=0):
+                del drawlist[0]
+            else:
+                #leave it in the list for later
+                break
diff --git a/bin/reportlab/platypus/para.py b/bin/reportlab/platypus/para.py
new file mode 100644
index 00000000000..554c11003b0
--- /dev/null
+++ b/bin/reportlab/platypus/para.py
@@ -0,0 +1,2394 @@
+"""new experimental paragraph implementation
+
+Intended to allow support for paragraphs in paragraphs, hotlinks,
+embedded flowables, and underlining.  The main entry point is the
+function
+
+def Paragraph(text, style, bulletText=None, frags=None)
+
+Which is intended to be plug compatible with the "usual" platypus
+paragraph except that it supports more functionality.
+
+In this implementation you may embed paragraphs inside paragraphs
+to create hierarchically organized documents.
+
+This implementation adds the following paragraph-like tags (which
+support the same attributes as paragraphs, for font specification, etc).
+
+- Unnumberred lists (ala html):
+
+    
    +
  • first one
  • +
  • second one
  • +
+ +Also
    (default) or
      ,
        . + +- Numberred lists (ala html): + +
          +
        1. first one
        2. +
        3. second one
        4. +
        + +Also
          (default) or
            ,
              . + +- Display lists (ala HTML): + +For example + +
              +
              frogs
              Little green slimy things. Delicious with garlic
              +
              kittens
              cute, furry, not edible
              +
              bunnies
              cute, furry,. Delicious with garlic
              +
              + +ALSO the following additional internal paragraph markup tags are supported + +underlined text + +hyperlinked text +hyperlinked text + +Go to the end (go to document internal destination) +Go to the beginning + +This is the document start + (define document destination inside paragraph, color is optional) + +""" + +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.lib.utils import fp_str +from reportlab.platypus.flowables import Flowable +from reportlab.lib import colors + +from types import StringType, UnicodeType, InstanceType, TupleType, ListType, FloatType + +# SET THIS TO CAUSE A VIEWING BUG WITH ACROREAD 3 (for at least one input) +# CAUSEERROR = 0 + +debug = 0 + +DUMPPROGRAM = 0 + +TOOSMALLSPACE = 1e-5 + +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY + +# indent changes effect the next line +# align changes effect the current line + +# need to fix spacing questions... if ends with space then space may be inserted + +# NEGATIVE SPACE SHOULD NEVER BE EXPANDED (IN JUSTIFICATION, EG) + +class paragraphEngine: + # text origin of 0,0 is upper left corner + def __init__(self, program = None): + from reportlab.lib.colors import black + if program is None: + program = [] + self.lineOpHandlers = [] # for handling underlining and hyperlinking, etc + self.program = program + self.indent = self.rightIndent = 0.0 + self.baseindent = 0.0 # adjust this to add more indentation for bullets, eg + self.fontName = "Helvetica" + self.fontSize = 10 + self.leading = 12 + self.fontColor = black + self.x = self.y = self.rise = 0.0 + from reportlab.lib.enums import TA_LEFT + self.alignment = TA_LEFT + self.textStateStack = [] + + TEXT_STATE_VARIABLES = ("indent", "rightIndent", "fontName", "fontSize", + "leading", "fontColor", "lineOpHandlers", "rise", + "alignment") + #"textStateStack") + + def pushTextState(self): + state = [] + for var in self.TEXT_STATE_VARIABLES: + val = getattr(self, var) + state.append(val) + #self.textStateStack.append(state) + self.textStateStack = self.textStateStack+[state] # fresh copy + #print "push", self.textStateStack + #print "push", len(self.textStateStack), state + return state + + def popTextState(self): + state = self.textStateStack[-1] + self.textStateStack = self.textStateStack[:-1] + #print "pop", self.textStateStack + state = state[:] # copy for destruction + #print "pop", len(self.textStateStack), state + #print "handlers before", self.lineOpHandlers + for var in self.TEXT_STATE_VARIABLES: + val = state[0] + del state[0] + setattr(self, var, val) + + def format(self, maxwidth, maxheight, program, leading=0): + "return program with line operations added if at least one line fits" + # note: a generated formatted segment should not be formatted again + startstate = self.__dict__.copy() + #remainder = self.cleanProgram(program) + remainder = program[:] + #program1 = remainder[:] # debug only + lineprogram = [] + #if maxheight=self.leading and remainder: + #print "getting line with statestack", len(self.textStateStack) + #heightremaining = heightremaining - self.leading + indent = self.indent + rightIndent = self.rightIndent + linewidth = maxwidth - indent - rightIndent + beforelinestate = self.__dict__.copy() + if linewidthleading: + heightremaining = heightremaining-leading + else: + room = 0 + #self.resetState(beforelinestate) + self.__dict__.update(beforelinestate) + break # no room for this line +## if debug: +## print "line", line +## if lineIsFull: print "is full" +## else: print "is partially full" +## print "consumes", cursor, "elements" +## print "covers", currentLength, "of", maxwidth + alignment = self.alignment # last declared alignment for this line used + # recompute linewidth using the used indent + #linewidth = maxwidth - usedIndent - rightIndent + remainder = remainder[cursor:] + if not remainder: + # trim off the extra end of line + del line[-1] + # do justification if any + #line = self.shrinkWrap(line + if alignment==TA_LEFT: + #if debug: + # print "ALIGN LEFT" + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + pass + elif alignment==TA_CENTER: + #if debug: + # print "ALIGN CENTER" + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + line = self.centerAlign(line, currentLength, maxLength) + elif alignment==TA_RIGHT: + #if debug: + # print "ALIGN RIGHT" + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + line = self.rightAlign(line, currentLength, maxLength) + elif alignment==TA_JUSTIFY: + #if debug: + # print "JUSTIFY" + if remainder and lineIsFull: + if justStrings: + line = simpleJustifyAlign(line, currentLength, maxLength) + else: + line = self.justifyAlign(line, currentLength, maxLength) + else: + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + if debug: + print "no justify because line is not full or end of para" + else: + raise ValueError, "bad alignment "+repr(alignment) + if not justStrings: + line = self.cleanProgram(line) + lineprogram.extend(line) + laststate = self.__dict__.copy() + #self.resetState(startstate) + self.__dict__.update(startstate) + heightused = maxheight - heightremaining + return (lineprogram, remainder, laststate, heightused) + + def getState(self): + # inlined + return self.__dict__.copy() + + def resetState(self, state): + # primarily inlined + self.__dict__.update(state) + +## def sizeOfWord(self, word): +## inlineThisFunctionForEfficiency +## return float(stringWidth(word, self.fontName, self.fontSize)) + + def fitLine(self, program, totalLength): + "fit words (and other things) onto a line" + # assuming word lengths and spaces have not been yet added + # fit words onto a line up to maxlength, adding spaces and respecting extra space + from reportlab.pdfbase.pdfmetrics import stringWidth + usedIndent = self.indent + maxLength = totalLength - usedIndent - self.rightIndent + done = 0 + line = [] + cursor = 0 + lineIsFull = 0 + currentLength = 0 + maxcursor = len(program) + needspace = 0 + first = 1 + terminated = None + fontName = self.fontName + fontSize = self.fontSize + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + justStrings = 1 + while not done and cursormaxLength and not first: # always do at least one thing + # this word won't fit + #if debug: + # print "WORD", opcode, "wont fit, width", width, "fullwidth", fullwidth + # print " currentLength", currentLength, "newlength", newlength, "maxLength", maxLength + done = 1 + lineIsFull = 1 + else: + # fit the word: add a space then the word + if lastneedspace: + line.append( spacewidth ) # expandable space: positive + if opcode: + line.append( opcode ) + if abs(width)>TOOSMALLSPACE: + line.append( -width ) # non expanding space: negative + currentLength = newlength + #print line + #stop + first = 0 + elif topcode is FloatType: + justStrings = 0 + aopcode = abs(opcode) # negative means non expanding + if aopcode>TOOSMALLSPACE: + nextLength = currentLength+aopcode + if nextLength>maxLength and not first: # always do at least one thing + #if debug: print "EXPLICIT spacer won't fit", maxLength, nextLength, opcode + done = 1 + else: + if aopcode>TOOSMALLSPACE: + currentLength = nextLength + line.append(opcode) + first = 0 + elif topcode is TupleType: + justStrings = 0 + indicator = opcode[0] + #line.append(opcode) + if indicator=="nextLine": + # advance to nextLine + #(i, endallmarks) = opcode + line.append(opcode) + cursor = cursor+1 # consume this element + terminated = done = 1 + #if debug: + # print "nextLine encountered" + elif indicator=="color": + # change fill color + oldcolor = self.fontColor + (i, colorname) = opcode + #print "opcode", opcode + if type(colorname) in (StringType, UnicodeType): + color = self.fontColor = getattr(colors, colorname) + else: + color = self.fontColor = colorname # assume its something sensible :) + line.append(opcode) + elif indicator=="face": + # change font face + (i, fontname) = opcode + fontName = self.fontName = fontname + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + line.append(opcode) + elif indicator=="size": + # change font size + (i, fontsize) = opcode + size = abs(float(fontsize)) + if type(fontsize) in (StringType, UnicodeType): + if fontsize[:1]=="+": + fontSize = self.fontSize = self.fontSize + size + elif fontsize[:1]=="-": + fontSize = self.fontSize = self.fontSize - size + else: + fontSize = self.fontSize = size + else: + fontSize = self.fontSize = size + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + line.append(opcode) + elif indicator=="leading": + # change font leading + (i, leading) = opcode + self.leading = leading + line.append(opcode) + elif indicator=="indent": + # increase the indent + (i, increment) = opcode + indent = self.indent = self.indent + increment + if first: + usedIndent = max(indent, usedIndent) + maxLength = totalLength - usedIndent - self.rightIndent + line.append(opcode) + elif indicator=="push": + self.pushTextState() + line.append(opcode) + elif indicator=="pop": + try: + self.popTextState() + except: +## print "stack fault near", cursor +## for i in program[max(0, cursor-10):cursor+10]: +## if i==cursor: +## print "***>>>", +## print i + raise + fontName = self.fontName + fontSize = self.fontSize + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + line.append(opcode) + elif indicator=="bullet": + (i, bullet, indent, font, size) = opcode + # adjust for base indent (only at format time -- only execute once) + indent = indent + self.baseindent + opcode = (i, bullet, indent, font, size) + if not first: + raise ValueError, "bullet not at beginning of line" + bulletwidth = float(stringWidth(bullet, font, size)) + spacewidth = float(stringWidth(" ", font, size)) + bulletmin = indent+spacewidth+bulletwidth + # decrease the line size to allow bullet + usedIndent = max(bulletmin, usedIndent) + if first: + maxLength = totalLength - usedIndent - self.rightIndent + line.append(opcode) + elif indicator=="rightIndent": + # increase the right indent + (i, increment) = opcode + self.rightIndent = self.rightIndent+increment + if first: + maxLength = totalLength - usedIndent - self.rightIndent + line.append(opcode) + elif indicator=="rise": + (i, rise) = opcode + newrise = self.rise = self.rise+rise + line.append(opcode) + elif indicator=="align": + (i, alignment) = opcode + #if debug: + # print "SETTING ALIGNMENT", alignment + self.alignment = alignment + line.append(opcode) + elif indicator=="lineOperation": + (i, handler) = opcode + line.append(opcode) + self.lineOpHandlers = self.lineOpHandlers + [handler] # fresh copy + elif indicator=="endLineOperation": + (i, handler) = opcode + h = self.lineOpHandlers[:] # fresh copy + h.remove(handler) + self.lineOpHandlers = h + line.append(opcode) + + else: + raise ValueError, "at format time don't understand indicator "+repr(indicator) + else: + raise ValueError, "op must be string, float, instance, or tuple "+repr(opcode) + if not done: + cursor = cursor+1 + #first = 0 +## if debug: +## if done: +## print "DONE FLAG IS SET" +## if cursor>=maxcursor: +## print "AT END OF PROGRAM" + if not terminated: + line.append( ("nextLine", 0) ) + #print "fitline", line + return (lineIsFull, line, cursor, currentLength, usedIndent, maxLength, justStrings) + + def centerAlign(self, line, lineLength, maxLength): + diff = maxLength-lineLength + shift = diff/2.0 + if shift>TOOSMALLSPACE: + return self.insertShift(line, shift) + return line + + def rightAlign(self, line, lineLength, maxLength): + shift = maxLength-lineLength + #die + if shift>TOOSMALLSPACE: + return self.insertShift(line, shift) + return line + + def insertShift(self, line, shift): + # insert shift just before first visible element in line + result = [] + first = 1 + for e in line: + te = type(e) + if first and (te in (StringType, UnicodeType, InstanceType)): + result.append(shift) + first = 0 + result.append(e) + return result + + def justifyAlign(self, line, lineLength, maxLength): + diff = maxLength-lineLength + # count EXPANDABLE SPACES AFTER THE FIRST VISIBLE + spacecount = 0 + visible = 0 + for e in line: + te = type(e) + if te is FloatType and e>TOOSMALLSPACE and visible: + spacecount = spacecount+1 + elif te in (StringType, UnicodeType, InstanceType): + visible = 1 + #if debug: print "diff is", diff, "wordcount", wordcount #; die + if spacecount<1: + return line + shift = diff/float(spacecount) + if shift<=TOOSMALLSPACE: + #if debug: print "shift too small", shift + return line + first = 1 + visible = 0 + result = [] + cursor = 0 + nline = len(line) + while cursorTOOSMALLSPACE and visible: + expanded = e+shift + result[-1] = expanded + cursor = cursor+1 + return result + +## if not first: +## #if debug: print "shifting", shift, e +## #result.append(shift) +## # add the shift in result before any start markers before e +## insertplace = len(result)-1 +## done = 0 +## myshift = shift +## while insertplace>0 and not done: +## beforeplace = insertplace-1 +## beforething = result[beforeplace] +## thingtype = type(beforething) +## if thingtype is TupleType: +## indicator = beforething[0] +## if indicator=="endLineOperation": +## done = 1 +## elif debug: +## print "adding shift before", beforething +## elif thingtype is FloatType: +## myshift = myshift + beforething +## del result[beforeplace] +## else: +## done = 1 +## if not done: +## insertplace = beforeplace +## result.insert(insertplace, myshift) +## first = 0 +## cursor = cursor+1 +## return result + + def shrinkWrap(self, line): + # for non justified text, collapse adjacent text/shift's into single operations + result = [] + index = 0 + maxindex = len(line) + while index0: + thefloats = -thefloats + if nexte<0 and thefloats>0: + nexte = -nexte + thefloats = thefloats + nexte + elif tnexte in (StringType, UnicodeType): + thestrings.append(nexte) + index = index+1 + if index0: + last = -last + if e<0 and last>0: + e = -e + last = float(last)+e + else: + if abs(last)>TOOSMALLSPACE: + result.append(last) + result.append(e) + last = 0 + if last: + result.append(last) + # now go backwards and delete all floats occurring after all visible elements +## count = len(result)-1 +## done = 0 +## while count>0 and not done: +## e = result[count] +## te = type(e) +## if te is StringType or te is InstanceType or te is TupleType: +## done = 1 +## elif te is FloatType: +## del result[count] +## count = count-1 + # move end operations left and start operations left up to visibles + change = 1 + rline = range(len(result)-1) + while change: + #print line + change = 0 + for index in rline: + nextindex = index+1 + this = result[index] + next = result[nextindex] + doswap = 0 + tthis = type(this) + tnext = type(next) + # don't swap visibles + if tthis in (StringType, UnicodeType) or \ + tnext in (StringType, UnicodeType) or \ + this is InstanceType or tnext is InstanceType: + doswap = 0 + # only swap two tuples if the second one is an end operation and the first is something else + elif tthis is TupleType: + thisindicator = this[0] + if tnext is TupleType: + nextindicator = next[0] + doswap = 0 + if (nextindicator=="endLineOperation" and thisindicator!="endLineOperation" + and thisindicator!="lineOperation"): + doswap = 1 # swap nonend<>end + elif tnext==FloatType: + if thisindicator=="lineOperation": + doswap = 1 # begin <> space + if doswap: + #print "swap", line[index],line[nextindex] + result[index] = next + result[nextindex] = this + change = 1 + return result + + def runOpCodes(self, program, canvas, textobject): + "render the line(s)" + + escape = canvas._escape + code = textobject._code + startstate = self.__dict__.copy() + font = None + size = None + # be sure to set them before using them (done lazily below) + #textobject.setFont(self.fontName, self.fontSize) + textobject.setFillColor(self.fontColor) + xstart = self.x + thislineindent = self.indent + thislinerightIndent = self.rightIndent + indented = 0 + for opcode in program: + topcode = type(opcode) + if topcode in (StringType, UnicodeType, InstanceType): + if not indented: + if abs(thislineindent)>TOOSMALLSPACE: + #if debug: print "INDENTING", thislineindent + #textobject.moveCursor(thislineindent, 0) + code.append('%s Td' % fp_str(thislineindent, 0)) + self.x = self.x + thislineindent + for handler in self.lineOpHandlers: + #handler.end_at(x, y, self, canvas, textobject) # finish, eg, underlining this line + handler.start_at(self.x, self.y, self, canvas, textobject) # start underlining the next + indented = 1 + # lazily set font (don't do it again if not needed) + if font!=self.fontName or size!=self.fontSize: + font = self.fontName + size = self.fontSize + textobject.setFont(font, size) + if topcode in (StringType, UnicodeType): + #textobject.textOut(opcode) + text = escape(opcode) + code.append('(%s) Tj' % text) + else: + # drawable thing + opcode.execute(self, textobject, canvas) + elif topcode is FloatType: + # use abs value (ignore expandable marking) + opcode = abs(opcode) + if opcode>TOOSMALLSPACE: + #textobject.moveCursor(opcode, 0) + code.append('%s Td' % fp_str(opcode, 0)) + self.x = self.x + opcode + elif topcode is TupleType: + indicator = opcode[0] + if indicator=="nextLine": + # advance to nextLine + (i, endallmarks) = opcode + x = self.x + y = self.y + newy = self.y = self.y-self.leading + newx = self.x = xstart + thislineindent = self.indent + thislinerightIndent = self.rightIndent + indented = 0 + for handler in self.lineOpHandlers: + handler.end_at(x, y, self, canvas, textobject) # finish, eg, underlining this line + #handler.start_at(newx, newy, self, canvas, textobject)) # start underlining the next + textobject.setTextOrigin(newx, newy) + elif indicator=="color": + # change fill color + oldcolor = self.fontColor + (i, colorname) = opcode + #print "opcode", opcode + if type(colorname) in (StringType, UnicodeType): + color = self.fontColor = getattr(colors, colorname) + else: + color = self.fontColor = colorname # assume its something sensible :) + #if debug: + # print color.red, color.green, color.blue + # print dir(color) + #print "color is", color + #from reportlab.lib.colors import green + #if color is green: print "color is green" + if color!=oldcolor: + textobject.setFillColor(color) + elif indicator=="face": + # change font face + (i, fontname) = opcode + self.fontName = fontname + #textobject.setFont(self.fontName, self.fontSize) + elif indicator=="size": + # change font size + (i, fontsize) = opcode + size = abs(float(fontsize)) + if type(fontsize) in (StringType, UnicodeType): + if fontsize[:1]=="+": + fontSize = self.fontSize = self.fontSize + size + elif fontsize[:1]=="-": + fontSize = self.fontSize = self.fontSize - size + else: + fontSize = self.fontSize = size + else: + fontSize = self.fontSize = size + #(i, fontsize) = opcode + self.fontSize = fontSize + textobject.setFont(self.fontName, self.fontSize) + elif indicator=="leading": + # change font leading + (i, leading) = opcode + self.leading = leading + elif indicator=="indent": + # increase the indent + (i, increment) = opcode + indent = self.indent = self.indent + increment + thislineindent = max(thislineindent, indent) + elif indicator=="push": + self.pushTextState() + elif indicator=="pop": + oldcolor = self.fontColor + oldfont = self.fontName + oldsize = self.fontSize + self.popTextState() + #if CAUSEERROR or oldfont!=self.fontName or oldsize!=self.fontSize: + # textobject.setFont(self.fontName, self.fontSize) + if oldcolor!=self.fontColor: + textobject.setFillColor(self.fontColor) + elif indicator=="wordSpacing": + (i, ws) = opcode + textobject.setWordSpace(ws) + elif indicator=="bullet": + (i, bullet, indent, font, size) = opcode + if abs(self.x-xstart)>TOOSMALLSPACE: + raise ValueError, "bullet not at beginning of line" + bulletwidth = float(stringWidth(bullet, font, size)) + spacewidth = float(stringWidth(" ", font, size)) + bulletmin = indent+spacewidth+bulletwidth + # decrease the line size to allow bullet as needed + if bulletmin > thislineindent: + #if debug: print "BULLET IS BIG", bullet, bulletmin, thislineindent + thislineindent = bulletmin + textobject.moveCursor(indent, 0) + textobject.setFont(font, size) + textobject.textOut(bullet) + textobject.moveCursor(-indent, 0) + #textobject.textOut("M") + textobject.setFont(self.fontName, self.fontSize) + elif indicator=="rightIndent": + # increase the right indent + (i, increment) = opcode + self.rightIndent = self.rightIndent+increment + elif indicator=="rise": + (i, rise) = opcode + newrise = self.rise = self.rise+rise + textobject.setRise(newrise) + elif indicator=="align": + (i, alignment) = opcode + self.alignment = alignment + elif indicator=="lineOperation": + (i, handler) = opcode + handler.start_at(self.x, self.y, self, canvas, textobject) + #self.lineOpHandlers.append(handler) + #if debug: print "adding", handler, self.lineOpHandlers + self.lineOpHandlers = self.lineOpHandlers + [handler] # fresh copy! + elif indicator=="endLineOperation": + (i, handler) = opcode + handler.end_at(self.x, self.y, self, canvas, textobject) + newh = self.lineOpHandlers = self.lineOpHandlers[:] # fresh copy + #if debug: print "removing", handler, self.lineOpHandlers + if handler in newh: + self.lineOpHandlers.remove(handler) + else: + pass + #print "WARNING: HANDLER", handler, "NOT IN", newh + else: + raise ValueError, "don't understand indicator "+repr(indicator) + else: + raise ValueError, "op must be string float or tuple "+repr(opcode) + laststate = self.__dict__.copy() + #self.resetState(startstate) + self.__dict__.update(startstate) + return laststate + +def stringLine(line, length): + "simple case: line with just strings and spacings which can be ignored" + + strings = [] + for x in line: + if type(x) in (StringType, UnicodeType): + strings.append(x) + text = ' '.join(strings) + result = [text, float(length)] + nextlinemark = ("nextLine", 0) + if line and line[-1]==nextlinemark: + result.append( nextlinemark ) + return result + +def simpleJustifyAlign(line, currentLength, maxLength): + "simple justification with only strings" + + strings = [] + for x in line[:-1]: + if type(x) in (StringType, UnicodeType): + strings.append(x) + nspaces = len(strings)-1 + slack = maxLength-currentLength + text = ' '.join(strings) + if nspaces>0 and slack>0: + wordspacing = slack/float(nspaces) + result = [("wordSpacing", wordspacing), text, maxLength, ("wordSpacing", 0)] + else: + result = [text, currentLength, ("nextLine", 0)] + nextlinemark = ("nextLine", 0) + if line and line[-1]==nextlinemark: + result.append( nextlinemark ) + return result + +from reportlab.lib.colors import black + +def readBool(text): + if text.upper() in ("Y", "YES", "TRUE", "1"): + return 1 + elif text.upper() in ("N", "NO", "FALSE", "0"): + return 0 + else: + raise RMLError, "true/false attribute has illegal value '%s'" % text + +def readAlignment(text): + up = text.upper() + if up == 'LEFT': + return TA_LEFT + elif up == 'RIGHT': + return TA_RIGHT + elif up in ['CENTER', 'CENTRE']: + return TA_CENTER + elif up == 'JUSTIFY': + return TA_JUSTIFY + +def readLength(text): + """Read a dimension measurement: accept "3in", "5cm", + "72 pt" and so on.""" + text = text.strip() + try: + return float(text) + except ValueError: + text = text.lower() + numberText, units = text[:-2],text[-2:] + numberText = numberText.strip() + try: + number = float(numberText) + except ValueError: + raise ValueError, "invalid length attribute '%s'" % text + try: + multiplier = { + 'in':72, + 'cm':28.3464566929, #72/2.54; is this accurate? + 'mm':2.83464566929, + 'pt':1 + }[units] + except KeyError: + raise RMLError, "invalid length attribute '%s'" % text + + return number * multiplier + +def lengthSequence(s, converter=readLength): + """from "(2, 1)" or "2,1" return [2,1], for example""" + s = s.strip() + if s[:1]=="(" and s[-1:]==")": + s = s[1:-1] + sl = s.split(',') + sl = [s.strip() for s in sl] + sl = [converter(s) for s in sl] + return sl + + +def readColor(text): + """Read color names or tuples, RGB or CMYK, and return a Color object.""" + if not text: + return None + from reportlab.lib import colors + from string import letters + if text[0] in letters: + return colors.__dict__[text] + tup = lengthSequence(text) + + msg = "Color tuple must have 3 (or 4) elements for RGB (or CMYC)." + assert 3 <= len(tup) <= 4, msg + msg = "Color tuple must have all elements <= 1.0." + for i in range(len(tup)): + assert tup[i] <= 1.0, msg + + if len(tup) == 3: + colClass = colors.Color + elif len(tup) == 4: + colClass = colors.CMYKColor + return apply(colClass, tup) + +class StyleAttributeConverters: + fontSize=[readLength] + leading=[readLength] + leftIndent=[readLength] + rightIndent=[readLength] + firstLineIndent=[readLength] + alignment=[readAlignment] + spaceBefore=[readLength] + spaceAfter=[readLength] + bulletFontSize=[readLength] + bulletIndent=[readLength] + textColor=[readColor] + backColor=[readColor] + +class SimpleStyle: + "simplified paragraph style without all the fancy stuff" + name = "basic" + fontName='Times-Roman' + fontSize=10 + leading=12 + leftIndent=0 + rightIndent=0 + firstLineIndent=0 + alignment=TA_LEFT + spaceBefore=0 + spaceAfter=0 + bulletFontName='Times-Roman' + bulletFontSize=10 + bulletIndent=0 + textColor=black + backColor=None + + def __init__(self, name, parent=None, **kw): + mydict = self.__dict__ + if parent: + for (a,b) in parent.__dict__.items(): + mydict[a]=b + for (a,b) in kw.items(): + mydict[a] = b + + def addAttributes(self, dictionary): + for key in dictionary.keys(): + value = dictionary[key] + if value is not None: + if hasattr(StyleAttributeConverters, key): + converter = getattr(StyleAttributeConverters, key)[0] + value = converter(value) + setattr(self, key, value) + + +DEFAULT_ALIASES = { + "h1.defaultStyle": "Heading1", + "h2.defaultStyle": "Heading2", + "h3.defaultStyle": "Heading3", + "h4.defaultStyle": "Heading4", + "h5.defaultStyle": "Heading5", + "h6.defaultStyle": "Heading6", + "title.defaultStyle": "Title", + "subtitle.defaultStyle": "SubTitle", + "para.defaultStyle": "Normal", + "pre.defaultStyle": "Code", + "ul.defaultStyle": "UnorderedList", + "ol.defaultStyle": "OrderedList", + "li.defaultStyle": "Definition", + } + +class FastPara(Flowable): + "paragraph with no special features (not even a single ampersand!)" + + def __init__(self, style, simpletext): + #if debug: + # print "FAST", id(self) + if "&" in simpletext: + raise ValueError, "no ampersands please!" + self.style = style + self.simpletext = simpletext + self.lines = None + + def wrap(self, availableWidth, availableHeight): + simpletext = self.simpletext + self.availableWidth = availableWidth + style = self.style + text = self.simpletext + rightIndent = style.rightIndent + leftIndent = style.leftIndent + leading = style.leading + font = style.fontName + size = style.fontSize + firstindent = style.firstLineIndent + #textcolor = style.textColor + words = simpletext.split() + lines = [] + from reportlab.pdfbase.pdfmetrics import stringWidth + spacewidth = stringWidth(" ", font, size) + currentline = [] + currentlength = 0 + firstmaxlength = availableWidth - rightIndent - firstindent + maxlength = availableWidth - rightIndent - leftIndent + if maxlengthavailableHeight: + done = 1 + if currentlength and not done: + lines.append( (' '.join(currentline), currentlength, len(currentline) )) + heightused = heightused+leading + self.lines = lines + self.height = heightused + remainder = self.remainder = ' '.join(words[cursor:]) + #print "lines", lines + #print "remainder is", remainder + else: + remainder = None + heightused = self.height + lines = self.lines + if remainder: + result = (availableWidth, availableHeight+leading) # need to split + else: + result = (availableWidth, heightused) + #if debug: print "wrap is", (availableWidth, availableHeight), result, len(lines) + return result + + def split(self, availableWidth, availableHeight): + style = self.style + leading = style.leading + if availableHeight1: + # patch from doug@pennatus.com, 9 Nov 2002, no extraspace on last line + textobject.setWordSpace((basicWidth-length)/(nwords-1.0)) + else: + textobject.setWordSpace(0.0) + textobject.setTextOrigin(x,y) + text = escape(text) + code.append('(%s) Tj' % text) + #textobject.textOut(text) + y = y-leading + c.drawText(textobject) + + def getSpaceBefore(self): + #if debug: + # print "got space before", self.spaceBefore + return self.style.spaceBefore + + def getSpaceAfter(self): + #print "got space after", self.spaceAfter + return self.style.spaceAfter + +def defaultContext(): + result = {} + from reportlab.lib.styles import getSampleStyleSheet + styles = getSampleStyleSheet() + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + result[stylenamekey] = styles[stylenamevalue] + return result + +def buildContext(stylesheet=None): + result = {} + from reportlab.lib.styles import getSampleStyleSheet + if stylesheet is not None: + # Copy styles with the same name as aliases + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + if stylesheet.has_key(stylenamekey): + result[stylenamekey] = stylesheet[stylenamekey] + # Then make aliases + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + if stylesheet.has_key(stylenamevalue): + result[stylenamekey] = stylesheet[stylenamevalue] + + styles = getSampleStyleSheet() + # Then, fill in defaults if they were not filled yet. + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + if not result.has_key(stylenamekey) and styles.has_key(stylenamevalue): + result[stylenamekey] = styles[stylenamevalue] + return result + +class Para(Flowable): + + spaceBefore = 0 + spaceAfter = 0 + + def __init__(self, style, parsedText=None, bulletText=None, state=None, context=None, baseindent=0): + #print id(self), "para", parsedText + self.baseindent = baseindent + self.context = buildContext(context) + self.parsedText = parsedText + self.bulletText = bulletText + self.style1 = style # make sure Flowable doesn't use this unless wanted! call it style1 NOT style + #self.spaceBefore = self.spaceAfter = 0 + self.program = [] # program before layout + self.formattedProgram = [] # after layout + self.remainder = None # follow on paragraph if any + self.state = state # initial formatting state (for completions) + if not state: + self.spaceBefore = style.spaceBefore + self.spaceAfter = style.spaceAfter + #self.spaceBefore = "invalid value" + #if hasattr(self, "spaceBefore") and debug: + # print "spaceBefore is", self.spaceBefore, self.parsedText + self.bold = 0 + self.italic = 0 + self.face = style.fontName + self.size = style.fontSize + + def getSpaceBefore(self): + #if debug: + # print "got space before", self.spaceBefore + return self.spaceBefore + + def getSpaceAfter(self): + #print "got space after", self.spaceAfter + return self.spaceAfter + + def wrap(self, availableWidth, availableHeight): + if debug: + print "WRAPPING", id(self), availableWidth, availableHeight + print " ", self.formattedProgram + print " ", self.program + self.availableHeight = availableHeight + self.myengine = p = paragraphEngine() + p.baseindent = self.baseindent # for shifting bullets as needed + parsedText = self.parsedText + formattedProgram = self.formattedProgram + state = self.state + if state: + leading = state["leading"] + else: + leading = self.style1.leading + program = self.program + self.cansplit = 1 # until proven otherwise + if state: + p.resetState(state) + p.x = 0 + p.y = 0 + needatleast = state["leading"] + else: + needatleast = self.style1.leading + if availableHeight<=needatleast: + self.cansplit = 0 + #if debug: + # print "CANNOT COMPILE, NEED AT LEAST", needatleast, 'AVAILABLE', availableHeight + return (availableHeight+1, availableWidth) # cannot split + if parsedText is None and program is None: + raise ValueError, "need parsedText for formatting" + if not program: + self.program = program = self.compileProgram(parsedText) + if not self.formattedProgram: + (formattedProgram, remainder, \ + laststate, heightused) = p.format(availableWidth, availableHeight, program, leading) + self.formattedProgram = formattedProgram + self.height = heightused + self.laststate = laststate + self.remainderProgram = remainder + else: + heightused = self.height + remainder = None + # too big if there is a remainder + if remainder: + # lie about the height: it must be split anyway + #if debug: + # print "I need to split", self.formattedProgram + # print "heightused", heightused, "available", availableHeight, "remainder", len(remainder) + height = availableHeight + 1 + #print "laststate is", laststate + #print "saving remainder", remainder + self.remainder = Para(self.style1, parsedText=None, bulletText=None, \ + state=laststate, context=self.context) + self.remainder.program = remainder + self.remainder.spaceAfter = self.spaceAfter + self.spaceAfter = 0 + else: + self.remainder = None # no extra + height = heightused + if height>availableHeight: + height = availableHeight-0.1 + #if debug: + # print "giving height", height, "of", availableHeight, self.parsedText + result = (availableWidth, height) + if debug: + (w, h) = result + if abs(availableHeight-h)<0.2: + print "exact match???" + repr(availableHeight, h) + print "wrap is", (availableWidth, availableHeight), result + return result + + def split(self, availableWidth, availableHeight): + #if debug: + # print "SPLITTING", id(self), availableWidth, availableHeight + if availableHeight<=0 or not self.cansplit: + #if debug: + # print "cannot split", availableWidth, "too small" + return [] # wrap failed to find a split + self.availableHeight = availableHeight + formattedProgram = self.formattedProgram + #print "formattedProgram is", formattedProgram + if formattedProgram is None: + raise ValueError, "must call wrap before split" + elif not formattedProgram: + # no first line in self: fail to split + return [] + remainder = self.remainder + if remainder: + #print "SPLITTING" + result= [self, remainder] + else: + result= [self] + #if debug: print "split is", result + return result + + def draw(self): + p = self.myengine #paragraphEngine() + formattedProgram = self.formattedProgram + if formattedProgram is None: + raise ValueError, "must call wrap before draw" + state = self.state + laststate = self.laststate + if state: + p.resetState(state) + p.x = 0 + p.y = 0 + c = self.canv + #if debug: + # print id(self), "page number", c.getPageNumber() + height = self.height + if state: + leading = state["leading"] + else: + leading = self.style1.leading + #if debug: + # c.rect(0,0,-1, height-self.size, fill=1, stroke=1) + c.translate(0, height-self.size) + t = c.beginText() + #t.setTextOrigin(0,0) + if DUMPPROGRAM or debug: + print "="*44, "now running program" + for x in formattedProgram: + print x + print "-"*44 + laststate = p.runOpCodes(formattedProgram, c, t) + #print laststate["x"], laststate["y"] + c.drawText(t) + + def compileProgram(self, parsedText, program=None): + style = self.style1 + # standard parameters + #program = self.program + if program is None: + program = [] + a = program.append + fn = style.fontName + # add style information if there was no initial state + a( ("face", fn ) ) + from reportlab.lib.fonts import ps2tt + (self.face, self.bold, self.italic) = ps2tt(fn) + a( ("size", style.fontSize ) ) + self.size = style.fontSize + a( ("align", style.alignment ) ) + a( ("indent", style.leftIndent ) ) + if style.firstLineIndent: + a( ("indent", style.firstLineIndent ) ) # must be undone later + a( ("rightIndent", style.rightIndent ) ) + a( ("leading", style.leading) ) + if style.textColor: + a( ("color", style.textColor) ) + #a( ("nextLine", 0) ) # clear for next line + if self.bulletText: + self.do_bullet(self.bulletText, program) + self.compileComponent(parsedText, program) + # now look for a place where to insert the unindent after the first line + if style.firstLineIndent: + count = 0 + for x in program: + count = count+1 + tx = type(x) + if tx in (StringType, UnicodeType, InstanceType): + break + program.insert( count, ("indent", -style.firstLineIndent ) ) # defaults to end if no visibles + #print "="*8, id(self), "program is" + #for x in program: + # print x +## print "="*11 +## # check pushes and pops +## stackcount = 0 +## dump = 0 +## for x in program: +## if dump: +## print "dump:", x +## if type(x) is TupleType: +## i = x[0] +## if i=="push": +## stackcount = stackcount+1 +## print " "*stackcount, "push", stackcount +## if i=="pop": +## stackcount = stackcount-1 +## print " "*stackcount, "pop", stackcount +## if stackcount<0: +## dump=1 +## print "STACK UNDERFLOW!" +## if dump: stop + return program + + def linearize(self, program = None, parsedText=None): + #print "LINEARIZING", self + #program = self.program = [] + if parsedText is None: + parsedText = self.parsedText + style = self.style1 + if program is None: + program = [] + program.append( ("push",) ) + if style.spaceBefore: + program.append( ("leading", style.spaceBefore+style.leading) ) + else: + program.append( ("leading", style.leading) ) + program.append( ("nextLine", 0) ) + program = self.compileProgram(parsedText, program=program) + program.append( ("pop",) ) + # go to old margin + program.append( ("push",) ) + if style.spaceAfter: + program.append( ("leading", style.spaceAfter) ) + else: + program.append( ("leading", 0) ) + program.append( ("nextLine", 0) ) + program.append( ("pop",) ) + + def compileComponent(self, parsedText, program): + import types + ttext = type(parsedText) + #program = self.program + if ttext in (StringType, UnicodeType): + # handle special characters here... + # short cut + if parsedText: + stext = parsedText.strip() + if not stext: + program.append(" ") # contract whitespace to single space + else: + handleSpecialCharacters(self, parsedText, program) + elif ttext is ListType: + for e in parsedText: + self.compileComponent(e, program) + elif ttext is TupleType: + (tagname, attdict, content, extra) = parsedText + if not attdict: + attdict = {} + compilername = "compile_"+tagname + compiler = getattr(self, compilername, None) + if compiler is not None: + compiler(attdict, content, extra, program) + else: + # just pass the tag through + if debug: + L = [ "<" + tagname ] + a = L.append + if not attdict: attdict = {} + for (k, v) in attdict.items(): + a(" %s=%s" % (k,v)) + if content: + a(">") + a(str(content)) + a("" % tagname) + else: + a("/>") + t = ''.join(L) + handleSpecialCharacters(self, t, program) + else: + raise ValueError, "don't know how to handle tag " + repr(tagname) + + def shiftfont(self, program, face=None, bold=None, italic=None): + oldface = self.face + oldbold = self.bold + olditalic = self.italic + oldfontinfo = (oldface, oldbold, olditalic) + if face is None: face = oldface + if bold is None: bold = oldbold + if italic is None: italic = olditalic + self.face = face + self.bold = bold + self.italic = italic + from reportlab.lib.fonts import tt2ps + font = tt2ps(face,bold,italic) + oldfont = tt2ps(oldface,oldbold,olditalic) + if font!=oldfont: + program.append( ("face", font ) ) + return oldfontinfo + + def compile_(self, attdict, content, extra, program): + # "anonymous" tag: just do the content + for e in content: + self.compileComponent(e, program) + #compile_para = compile_ # at least for now... + + def compile_pageNumber(self, attdict, content, extra, program): + program.append(PageNumberObject()) + + def compile_b(self, attdict, content, extra, program): + (f,b,i) = self.shiftfont(program, bold=1) + for e in content: + self.compileComponent(e, program) + self.shiftfont(program, bold=b) + + def compile_i(self, attdict, content, extra, program): + (f,b,i) = self.shiftfont(program, italic=1) + for e in content: + self.compileComponent(e, program) + self.shiftfont(program, italic=i) + + def compile_u(self, attdict, content, extra, program): + # XXXX must eventually add things like alternative colors + #program = self.program + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + program.append( ('endLineOperation', UNDERLINE) ) + + def compile_sub(self, attdict, content, extra, program): + size = self.size + self.size = newsize = size * 0.7 + rise = size*0.5 + #program = self.program + program.append( ('size', newsize) ) + self.size = size + program.append( ('rise', -rise) ) + for e in content: + self.compileComponent(e, program) + program.append( ('size', size) ) + program.append( ('rise', rise) ) + + def compile_ul(self, attdict, content, extra, program, tagname="ul"): + # by transformation + #print "compile", tagname, attdict + atts = attdict.copy() + bulletmaker = bulletMaker(tagname, atts, self.context) + # now do each element as a separate paragraph + for e in content: + te = type(e) + if te in (StringType, UnicodeType): + if e.strip(): + raise ValueError, "don't expect CDATA between list elements" + elif te is TupleType: + (tagname, attdict1, content1, extra) = e + if tagname!="li": + raise ValueError, "don't expect %s inside list" % repr(tagname) + newatts = atts.copy() + if attdict1: + newatts.update(attdict1) + bulletmaker.makeBullet(newatts) + self.compile_para(newatts, content1, extra, program) + + def compile_ol(self, attdict, content, extra, program): + return self.compile_ul(attdict, content, extra, program, tagname="ol") + + def compile_dl(self, attdict, content, extra, program): + # by transformation + #print "compile", tagname, attdict + atts = attdict.copy() + # by transformation + #print "compile", tagname, attdict + atts = attdict.copy() + bulletmaker = bulletMaker("dl", atts, self.context) + # now do each element as a separate paragraph + contentcopy = list(content) # copy for destruction + bullet = "" + while contentcopy: + e = contentcopy[0] + del contentcopy[0] + te = type(e) + if te in (StringType, UnicodeType): + if e.strip(): + raise ValueError, "don't expect CDATA between list elements" + elif not contentcopy: + break # done at ending whitespace + else: + continue # ignore intermediate whitespace + elif te is TupleType: + (tagname, attdict1, content1, extra) = e + if tagname!="dd" and tagname!="dt": + raise ValueError, "don't expect %s here inside list, expect 'dd' or 'dt'" % \ + repr(tagname) + if tagname=="dt": + if bullet: + raise ValueError, "dt will not be displayed unless followed by a dd: "+repr(bullet) + if content1: + self.compile_para(attdict1, content1, extra, program) + # raise ValueError, \ + # "only simple strings supported in dd content currently: "+repr(content1) + elif tagname=="dd": + newatts = atts.copy() + if attdict1: + newatts.update(attdict1) + bulletmaker.makeBullet(newatts, bl=bullet) + self.compile_para(newatts, content1, extra, program) + bullet = "" # don't use this bullet again + if bullet: + raise ValueError, "dt will not be displayed unless followed by a dd"+repr(bullet) + + def compile_super(self, attdict, content, extra, program): + size = self.size + self.size = newsize = size * 0.7 + rise = size*0.5 + #program = self.program + program.append( ('size', newsize) ) + program.append( ('rise', rise) ) + for e in content: + self.compileComponent(e, program) + program.append( ('size', size) ) + self.size = size + program.append( ('rise', -rise) ) + + def compile_font(self, attdict, content, extra, program): + #program = self.program + program.append( ("push",) ) # store current data + if attdict.has_key("face"): + face = attdict["face"] + from reportlab.lib.fonts import tt2ps + try: + font = tt2ps(face,self.bold,self.italic) + except: + font = face # better work! + program.append( ("face", font ) ) + if attdict.has_key("color"): + colorname = attdict["color"] + program.append( ("color", colorname) ) + if attdict.has_key("size"): + #size = float(attdict["size"]) # really should convert int, cm etc here! + size = attdict["size"] + program.append( ("size", size) ) + for e in content: + self.compileComponent(e, program) + program.append( ("pop",) ) # restore as before + + def compile_a(self, attdict, content, extra, program): + url = attdict["href"] + colorname = attdict.get("color", "blue") + #program = self.program + Link = HotLink(url) + program.append( ("push",) ) # store current data + program.append( ("color", colorname) ) + program.append( ('lineOperation', Link) ) + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + program.append( ('endLineOperation', UNDERLINE) ) + program.append( ('endLineOperation', Link) ) + program.append( ("pop",) ) # restore as before + + def compile_link(self, attdict, content, extra, program): + dest = attdict["destination"] + colorname = attdict.get("color", None) + #program = self.program + Link = InternalLink(dest) + program.append( ("push",) ) # store current data + if colorname: + program.append( ("color", colorname) ) + program.append( ('lineOperation', Link) ) + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + program.append( ('endLineOperation', UNDERLINE) ) + program.append( ('endLineOperation', Link) ) + program.append( ("pop",) ) # restore as before + + def compile_setLink(self, attdict, content, extra, program): + dest = attdict["destination"] + colorname = attdict.get("color", "blue") + #program = self.program + Link = DefDestination(dest) + program.append( ("push",) ) # store current data + if colorname: + program.append( ("color", colorname) ) + program.append( ('lineOperation', Link) ) + if colorname: + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + if colorname: + program.append( ('endLineOperation', UNDERLINE) ) + program.append( ('endLineOperation', Link) ) + program.append( ("pop",) ) # restore as before + + #def compile_p(self, attdict, content, extra, program): + # # have to be careful about base indent here! + # not finished + + def compile_bullet(self, attdict, content, extra, program): + ### eventually should allow things like images and graphics in bullets too XXXX + if len(content)!=1 or type(content[0]) not in (StringType, UnicodeType): + raise ValueError, "content for bullet must be a single string" + text = content[0] + self.do_bullet(text, program) + + def do_bullet(self, text, program): + style = self.style1 + #program = self.program + indent = style.bulletIndent + self.baseindent + font = style.bulletFontName + size = style.bulletFontSize + program.append( ("bullet", text, indent, font, size) ) + + def compile_tt(self, attdict, content, extra, program): + (f,b,i) = self.shiftfont(program, face="Courier") + for e in content: + self.compileComponent(e, program) + self.shiftfont(program, face=f) + + def compile_greek(self, attdict, content, extra, program): + self.compile_font({"face": "symbol"}, content, extra, program) + + def compile_evalString(self, attdict, content, extra, program): + program.append( EvalStringObject(attdict, content, extra, self.context) ) + + def compile_name(self, attdict, content, extra, program): + program.append( NameObject(attdict, content, extra, self.context) ) + + def compile_getName(self, attdict, content, extra, program): + program.append( GetNameObject(attdict, content, extra, self.context) ) + + def compile_seq(self, attdict, content, extra, program): + program.append( SeqObject(attdict, content, extra, self.context) ) + + def compile_seqReset(self, attdict, content, extra, program): + program.append( SeqResetObject(attdict, content, extra, self.context) ) + + def compile_seqDefault(self, attdict, content, extra, program): + program.append( SeqDefaultObject(attdict, content, extra, self.context) ) + + def compile_para(self, attdict, content, extra, program, stylename = "para.defaultStyle"): + if attdict is None: + attdict = {} + context = self.context + stylename = attdict.get("style", stylename) + style = context[stylename] + newstyle = SimpleStyle(name="rml2pdf internal embedded style", parent=style) + newstyle.addAttributes(attdict) + bulletText = attdict.get("bulletText", None) + mystyle = self.style1 + thepara = Para(newstyle, content, context=context, bulletText=bulletText) + # possible ref loop on context, break later + # now compile it and add it to the program + mybaseindent = self.baseindent + self.baseindent = thepara.baseindent = mystyle.leftIndent + self.baseindent + thepara.linearize(program=program) + program.append( ("nextLine", 0) ) + self.baseindent = mybaseindent + +class bulletMaker: + def __init__(self, tagname, atts, context): + self.tagname = tagname + #print "context is", context + style = "li.defaultStyle" + self.style = style = atts.get("style", style) + typ = {"ul": "disc", "ol": "1", "dl": None}[tagname] + #print tagname, "bulletmaker type is", typ + self.typ =typ = atts.get("type", typ) + #print tagname, "bulletmaker type is", typ + if not atts.has_key("leftIndent"): + # get the style so you can choose an indent length + thestyle = context[style] + from reportlab.pdfbase.pdfmetrics import stringWidth + size = thestyle.fontSize + indent = stringWidth("XXX", "Courier", size) + atts["leftIndent"] = str(indent) + self.count = 0 + + def makeBullet(self, atts, bl=None): + count = self.count = self.count+1 + typ = self.typ + tagname = self.tagname + #print "makeBullet", tagname, typ, count + # forget space before for non-first elements + if count>1: + atts["spaceBefore"] = "0" + if bl is None: + if tagname=="ul": + if typ=="disc": bl = chr(109) + elif typ=="circle": bl = chr(108) + elif typ=="square": bl = chr(110) + else: + raise ValueError, "unordered list type %s not implemented" % repr(typ) + if not atts.has_key("bulletFontName"): + atts["bulletFontName"] = "ZapfDingbats" + elif tagname=="ol": + if typ=="1": bl = repr(count) + elif typ=="a": + theord = ord("a")+count-1 + bl = chr(theord) + elif typ=="A": + theord = ord("A")+count-1 + bl = chr(theord) + else: + raise ValueError, "ordered bullet type %s not implemented" % repr(typ) + else: + raise ValueError, "bad tagname "+repr(tagname) + if not atts.has_key("bulletText"): + atts["bulletText"] = bl + if not atts.has_key("style"): + atts["style"] = self.style + +class EvalStringObject: + "this will only work if rml2pdf is present" + + tagname = "evalString" + + def __init__(self, attdict, content, extra, context): + if not attdict: + attdict = {} + self.attdict = attdict + self.content = content + self.context = context + self.extra = extra + + def getOp(self, tuple, engine): + from rlextra.rml2pdf.rml2pdf import Controller + #print "tuple", tuple + op = self.op = Controller.processTuple(tuple, self.context, {}) + return op + + def width(self, engine): + from reportlab.pdfbase.pdfmetrics import stringWidth + content = self.content + if not content: + content = [] + tuple = (self.tagname, self.attdict, content, self.extra) + op = self.op = self.getOp(tuple, engine) + #print op.__class__ + #print op.pcontent + #print self + s = str(op) + return stringWidth(s, engine.fontName, engine.fontSize) + + def execute(self, engine, textobject, canvas): + textobject.textOut(str(self.op)) + +class SeqObject(EvalStringObject): + + def getOp(self, tuple, engine): + from reportlab.lib.sequencer import getSequencer + globalsequencer = getSequencer() + attr = self.attdict + #if it has a template, use that; otherwise try for id; + #otherwise take default sequence + if attr.has_key('template'): + templ = attr['template'] + op = self.op = templ % globalsequencer + return op + elif attr.has_key('id'): + id = attr['id'] + else: + id = None + op = self.op = globalsequencer.nextf(id) + return op + +class NameObject(EvalStringObject): + tagname = "name" + def execute(self, engine, textobject, canvas): + pass # name doesn't produce any output + +class SeqDefaultObject(NameObject): + + def getOp(self, tuple, engine): + from reportlab.lib.sequencer import getSequencer + globalsequencer = getSequencer() + attr = self.attdict + try: + default = attr['id'] + except KeyError: + default = None + globalsequencer.setDefaultCounter(default) + self.op = "" + return "" + +class SeqResetObject(NameObject): + + def getOp(self, tuple, engine): + from reportlab.lib.sequencer import getSequencer + import math + globalsequencer = getSequencer() + attr = self.attdict + try: + id = attr['id'] + except KeyError: + id = None + try: + base = math.atoi(attr['base']) + except: + base=0 + globalsequencer.reset(id, base) + self.op = "" + return "" + +class GetNameObject(EvalStringObject): + tagname = "getName" + +class PageNumberObject: + + def __init__(self, example="XXX"): + self.example = example # XXX SHOULD ADD THE ABILITY TO PASS IN EXAMPLES + + def width(self, engine): + from reportlab.pdfbase.pdfmetrics import stringWidth + return stringWidth(self.example, engine.fontName, engine.fontSize) + + def execute(self, engine, textobject, canvas): + n = canvas.getPageNumber() + textobject.textOut(str(n)) + +### this should be moved into rml2pdf +def EmbedInRml2pdf(): + "make the para the default para implementation in rml2pdf" + from rlextra.rml2pdf.rml2pdf import MapNode, Controller # may not need to use superclass? + global paraMapper, theParaMapper, ulMapper + + class paraMapper(MapNode): + #stylename = "para.defaultStyle" + def translate(self, nodetuple, controller, context, overrides): + (tagname, attdict, content, extra) = nodetuple + stylename = tagname+".defaultStyle" + stylename = attdict.get("style", stylename) + style = context[stylename] + mystyle = SimpleStyle(name="rml2pdf internal style", parent=style) + mystyle.addAttributes(attdict) + bulletText = attdict.get("bulletText", None) + # can we use the fast implementation? + import types + result = None + if not bulletText and len(content)==1: + text = content[0] + if type(text) in (StringType, UnicodeType) and "&" not in text: + result = FastPara(mystyle, text) + if result is None: + result = Para(mystyle, content, context=context, bulletText=bulletText) # possible ref loop on context, break later + return result + + theParaMapper = paraMapper() + + class ulMapper(MapNode): + # wrap in a default para and let the para do it + def translate(self, nodetuple, controller, context, overrides): + thepara = ("para", {}, [nodetuple], None) + return theParaMapper.translate(thepara, controller, context, overrides) + + # override rml2pdf interpreters (should be moved to rml2pdf) + theListMapper = ulMapper() + Controller["ul"] = theListMapper + Controller["ol"] = theListMapper + Controller["dl"] = theListMapper + Controller["para"] = theParaMapper + Controller["h1"] = theParaMapper + Controller["h2"] = theParaMapper + Controller["h3"] = theParaMapper + Controller["title"] = theParaMapper + +def handleSpecialCharacters(engine, text, program=None): + from paraparser import greeks, symenc + from string import whitespace, atoi, atoi_error + standard={'lt':'<', 'gt':'>', 'amp':'&'} + # add space prefix if space here + if text[0:1] in whitespace: + program.append(" ") + #print "handling", repr(text) + # shortcut + if 0 and "&" not in text: + result = [] + for x in text.split(): + result.append(x+" ") + if result: + last = result[-1] + if text[-1:] not in whitespace: + result[-1] = last.strip() + program.extend(result) + return program + if program is None: + program = [] + amptext = text.split("&") + first = 1 + lastfrag = amptext[-1] + for fragment in amptext: + if not first: + # check for special chars + semi = fragment.find(";") + if semi>0: + name = fragment[:semi] + if name[0]=='#': + try: + if name[1] == 'x': + n = atoi(name[2:], 16) + else: + n = atoi(name[1:]) + except atoi_error: + n = -1 + if 0<=n<=255: fragment = chr(n)+fragment[semi+1:] + elif symenc.has_key(n): + fragment = fragment[semi+1:] + (f,b,i) = engine.shiftfont(program, face="symbol") + program.append(symenc[n]) + engine.shiftfont(program, face=f) + if fragment and fragment[0] in whitespace: + program.append(" ") # follow with a space + else: + fragment = "&"+fragment + elif standard.has_key(name): + fragment = standard[name]+fragment[semi+1:] + elif greeks.has_key(name): + fragment = fragment[semi+1:] + greeksub = greeks[name] + (f,b,i) = engine.shiftfont(program, face="symbol") + program.append(greeksub) + engine.shiftfont(program, face=f) + if fragment and fragment[0] in whitespace: + program.append(" ") # follow with a space + else: + # add back the & + fragment = "&"+fragment + else: + # add back the & + fragment = "&"+fragment + # add white separated components of fragment followed by space + sfragment = fragment.split() + for w in sfragment[:-1]: + program.append(w+" ") + # does the last one need a space? + if sfragment and fragment: + # reader 3 used to go nuts if you don't special case the last frag, but it's fixed? + if fragment[-1] in whitespace: # or fragment==lastfrag: + program.append( sfragment[-1]+" " ) + else: + last = sfragment[-1].strip() + if last: + #print "last is", repr(last) + program.append( last ) + first = 0 + #print "HANDLED", program + return program + +def Paragraph(text, style, bulletText=None, frags=None, context=None): + """ Paragraph(text, style, bulletText=None) + intended to be like a platypus Paragraph but better. + """ + # if there is no & or < in text then use the fast paragraph + if "&" not in text and "<" not in text: + return FastPara(style, simpletext=text) + else: + # use the fully featured one. + from reportlab.lib import rparsexml + parsedpara = rparsexml.parsexmlSimple(text,entityReplacer=None) + return Para(style, parsedText=parsedpara, bulletText=bulletText, state=None, context=context) + +class UnderLineHandler: + def __init__(self, color=None): + self.color = color + def start_at(self, x,y, para, canvas, textobject): + self.xStart = x + self.yStart = y + def end_at(self, x, y, para, canvas, textobject): + offset = para.fontSize/8.0 + canvas.saveState() + color = self.color + if self.color is None: + color = para.fontColor + canvas.setStrokeColor(color) + canvas.line(self.xStart, self.yStart-offset, x,y-offset) + canvas.restoreState() + +UNDERLINE = UnderLineHandler() + +class HotLink(UnderLineHandler): + + def __init__(self, url): + self.url = url + + def end_at(self, x, y, para, canvas, textobject): + fontsize = para.fontSize + rect = [self.xStart, self.yStart, x,y+fontsize] + if debug: + print "LINKING RECTANGLE", rect + #canvas.rect(self.xStart, self.yStart, x-self.xStart,y+fontsize-self.yStart, stroke=1) + self.link(rect, canvas) + + def link(self, rect, canvas): + canvas.linkURL(self.url, rect, relative=1) + +class InternalLink(HotLink): + + def link(self, rect, canvas): + destinationname = self.url + contents = "" + canvas.linkRect(contents, destinationname, rect, Border="[0 0 0]") + +class DefDestination(HotLink): + + defined = 0 + + def link(self, rect, canvas): + destinationname = self.url + if not self.defined: + [x, y, x1, y1] = rect + canvas.bookmarkHorizontal(destinationname, x, y1) # use the upper y + self.defined = 1 + +def splitspace(text): + # split on spacing but include spaces at element ends + stext = text.split() + result = [] + for e in stext: + result.append(e+" ") + return result + + +testparagraph = """ +This is Text. +This is bold text. +This is Text. +This is italic text. + +
                +
              • this is an element at 1 +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text monospaced and back to normal + +
                  +
                • this is an element at 2 + +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                    +
                  • this is an element at 3 + +more text and even more text and on and on and so forth + + +
                    +
                    frogs
                    Little green slimy things. Delicious with garlic
                    +
                    kittens
                    cute, furry, not edible
                    +
                    bunnies
                    cute, furry,. Delicious with garlic
                    +
                    + +more text and even more text and on and on and so forth + +
                      +
                    • this is an element at 4 + +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                    • +
                    + +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                  • +
                  +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                • + +
                +UNDERLINED more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                  +
                1. first element of the alpha list + +
                    +
                  • first element of the square unnumberred list
                  • + +
                  • second element of the unnumberred list
                  • + +
                  • third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list +
                  • + +
                  • fourth element of the unnumberred list
                  • + +
                  + +
                2. + +
                3. second element of the alpha list
                4. + +
                5. third element of the alpha list + third element of the unnumberred list &#33; --> ! + third element of the unnumberred list &#8704; --> ∀ + third element of the unnumberred list &exist; --> ∃ + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list +
                6. + +
                7. fourth element of the alpha list
                8. + +
                + + +
              • +
              +""" + +testparagraph1 = """ +goto www.reportlab.com. + + + +Red letter. thisisareallylongword andsoisthis andthisislonger +justified text paragraph example +justified text paragraph example +justified text paragraph example + + + +Green letter. +centered text paragraph example +centered text paragraph example +centered text paragraph example + + +Blue letter. +right justified text paragraph example +right justified text paragraph example +right justified text paragraph example + + +Yellow letter. +left justified text paragraph example +left justified text paragraph example +left justified text paragraph example + + +""" + +def test2(canv,testpara): + #print test_program; return + from reportlab.lib.units import inch + from reportlab.lib.styles import ParagraphStyle + from reportlab.lib import rparsexml + parsedpara = rparsexml.parsexmlSimple(testpara,entityReplacer=None) + S = ParagraphStyle("Normal", None) + P = Para(S, parsedpara) + (w, h) = P.wrap(5*inch, 10*inch) + print "wrapped as", (h,w) + canv.saveState() + canv.translate(1*inch, 1*inch) + canv.rect(0,0,5*inch,10*inch, fill=0, stroke=1) + P.canv = canv + canv.saveState() + P.draw() + canv.restoreState() + canv.setStrokeColorRGB(1, 0, 0) + #canv.translate(0, 3*inch) + canv.rect(0,0,w,h, fill=0, stroke=1) + canv.restoreState() + canv.showPage() + +testlink = HotLink("http://www.reportlab.com") + +test_program = [ + ('push',), + ('indent', 100), + ('rightIndent', 200), + ('bullet', 'very long bullet', 50, 'Courier', 14), + ('align', TA_CENTER), + ('face', "Times-Roman"), + ('size', 12), + ('leading', 14), + ] + splitspace("This is the first segment of the first paragraph.") + [ + ('lineOperation', testlink), + ]+splitspace("HOTLINK This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [ + ('endLineOperation', testlink), + ('nextLine', 0), + ('align', TA_LEFT), + ('bullet', 'Bullet', 10, 'Courier', 8), + ('face', "Times-Roman"), + ('size', 12), + ('leading', 14), + ] + splitspace("This is the SECOND!!! segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [ + ('nextLine', 0), + ('align', TA_JUSTIFY), + ('bullet', 'Bullet not quite as long this time', 50, 'Courier', 8), + ('face', "Helvetica-Oblique"), + ('size', 12), + ('leading', 14), + ('push',), + ('color', 'red'), + ] + splitspace("This is the THIRD!!! segment of the first paragraph." + ) + [ + ('lineOperation', UNDERLINE), + ] + splitspace("This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [ + ('endLineOperation', UNDERLINE), + ('rise', 5), + "raised ", "text ", + ('rise', -10), + "lowered ", "text ", + ('rise', 5), + "normal ", "text ", + ('pop',), + ('indent', 100), + ('rightIndent', 50), + ('nextLine', 0), + ('align', TA_RIGHT), + ('bullet', 'O', 50, 'Courier', 14), + ('face', "Helvetica"), + ('size', 12), + ('leading', 14), + ] + splitspace("And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a ") + [ + ('pop',), + ('nextLine', 0),] + +def test(): + from pprint import pprint + #print test_program; return + from reportlab.pdfgen import canvas + from reportlab.lib.units import inch + fn = "paratest0.pdf" + c = canvas.Canvas(fn) + test2(c,testparagraph) + test2(c,testparagraph1) + if 1: + remainder = test_program + test_program + test_program + laststate = {} + while remainder: + print "NEW PAGE" + c.translate(inch, 8*inch) + t = c.beginText() + t.setTextOrigin(0,0) + p = paragraphEngine() + p.resetState(laststate) + p.x = 0 + p.y = 0 + maxwidth = 7*inch + maxheight = 500 + (formattedprogram, remainder, laststate, height) = p.format(maxwidth, maxheight, remainder) + if debug: + pprint( formattedprogram )#; return + laststate = p.runOpCodes(formattedprogram, c, t) + c.drawText(t) + c.showPage() + print "="*30, "x=", laststate["x"], "y=", laststate["y"] + c.save() + print fn + +if __name__=="__main__": + test() diff --git a/bin/reportlab/platypus/paragraph.py b/bin/reportlab/platypus/paragraph.py new file mode 100644 index 00000000000..ca84f4ddc05 --- /dev/null +++ b/bin/reportlab/platypus/paragraph.py @@ -0,0 +1,1082 @@ +#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/paragraph.py +__version__=''' $Id: paragraph.py 2857 2006-05-11 13:06:52Z rgbecker $ ''' +from string import join, whitespace, find +from operator import truth +from types import StringType, ListType +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.platypus.paraparser import ParaParser +from reportlab.platypus.flowables import Flowable +from reportlab.lib.colors import Color +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.utils import _className +from reportlab.lib.textsplit import wordSplit +from copy import deepcopy +from reportlab.lib.abag import ABag +import re + +#on UTF8 branch, split and strip must be unicode-safe! +def split(text, delim=' '): + if type(text) is str: text = text.decode('utf8') + if type(delim) is str: delim = delim.decode('utf8') + return [uword.encode('utf8') for uword in text.split(delim)] + +def strip(text): + if type(text) is str: text = text.decode('utf8') + return text.strip().encode('utf8') + +class ParaLines(ABag): + """ + class ParaLines contains the broken into lines representation of Paragraphs + kind=0 Simple + fontName, fontSize, textColor apply to whole Paragraph + lines [(extraSpace1,words1),....,(extraspaceN,wordsN)] + + + kind==1 Complex + lines [FragLine1,...,FragLineN] + """ + +class FragLine(ABag): + """class FragLine contains a styled line (ie a line with more than one style) + + extraSpace unused space for justification only + wordCount 1+spaces in line for justification purposes + words [ParaFrags] style text lumps to be concatenated together + fontSize maximum fontSize seen on the line; not used at present, + but could be used for line spacing. + """ + +#our one and only parser +# XXXXX if the parser has any internal state using only one is probably a BAD idea! +_parser=ParaParser() + +def _lineClean(L): + return join(filter(truth,split(strip(L)))) + +def cleanBlockQuotedText(text,joiner=' '): + """This is an internal utility which takes triple- + quoted text form within the document and returns + (hopefully) the paragraph the user intended originally.""" + L=filter(truth,map(_lineClean, split(text, '\n'))) + return join(L, joiner) + +def setXPos(tx,dx): + if dx>1e-6 or dx<-1e-6: + tx.setXPos(dx) + +def _leftDrawParaLine( tx, offset, extraspace, words, last=0): + setXPos(tx,offset) + tx._textOut(join(words),1) + setXPos(tx,-offset) + return offset + +def _centerDrawParaLine( tx, offset, extraspace, words, last=0): + m = offset + 0.5 * extraspace + setXPos(tx,m) + tx._textOut(join(words),1) + setXPos(tx,-m) + return m + +def _rightDrawParaLine( tx, offset, extraspace, words, last=0): + m = offset + extraspace + setXPos(tx,m) + tx._textOut(join(words),1) + setXPos(tx,-m) + return m + +def _justifyDrawParaLine( tx, offset, extraspace, words, last=0): + setXPos(tx,offset) + text = join(words) + if last: + #last one, left align + tx._textOut(text,1) + else: + nSpaces = len(words)-1 + if nSpaces: + tx.setWordSpace(extraspace / float(nSpaces)) + tx._textOut(text,1) + tx.setWordSpace(0) + else: + tx._textOut(text,1) + setXPos(tx,-offset) + return offset + +def _putFragLine(tx,words): + cur_x = 0 + xtraState = tx.XtraState + for f in words: + if hasattr(f,'cbDefn'): + func = getattr(tx._canvas,f.cbDefn.name,None) + if not func: + raise AttributeError, "Missing %s callback attribute '%s'" % (f.cbDefn.kind,f.cbDefn.name) + func(tx._canvas,f.cbDefn.kind,f.cbDefn.label) + if f is words[-1]: tx._textOut('',1) + else: + if (tx._fontname,tx._fontsize)!=(f.fontName,f.fontSize): + tx._setFont(f.fontName, f.fontSize) + if xtraState.textColor!=f.textColor: + xtraState.textColor = f.textColor + tx.setFillColor(f.textColor) + if xtraState.rise!=f.rise: + xtraState.rise=f.rise + tx.setRise(f.rise) + tx._textOut(f.text,f is words[-1]) # cheap textOut + txtlen = tx._canvas.stringWidth(f.text, tx._fontname, tx._fontsize) + if not xtraState.underline and f.underline: + xtraState.underline = 1 + xtraState.underline_x = cur_x + xtraState.underlineColor = f.textColor + elif xtraState.underline: + if not f.underline: + xtraState.underline = 0 + spacelen = tx._canvas.stringWidth(' ', tx._fontname, tx._fontsize) + xtraState.underlines.append( (xtraState.underline_x, cur_x-spacelen, xtraState.underlineColor) ) + xtraState.underlineColor = None + elif xtraState.textColor!=xtraState.underlineColor: + xtraState.underlines.append( (xtraState.underline_x, cur_x, xtraState.underlineColor) ) + xtraState.underlineColor = xtraState.textColor + xtraState.underline_x = cur_x + if not xtraState.link and f.link: + xtraState.link = f.link + xtraState.link_x = cur_x + elif xtraState.link and f.link is not xtraState.link: + spacelen = tx._canvas.stringWidth(' ', tx._fontname, tx._fontsize) + xtraState.links.append( (xtraState.link_x, cur_x-spacelen, xtraState.link) ) + xtraState.link = None + cur_x += txtlen + if xtraState.underline: + xtraState.underlines.append( (xtraState.underline_x, cur_x, xtraState.underlineColor) ) + if xtraState.link: + xtraState.links.append( (xtraState.link_x, cur_x, xtraState.link) ) + +def _leftDrawParaLineX( tx, offset, line, last=0): + setXPos(tx,offset) + _putFragLine(tx, line.words) + setXPos(tx,-offset) + return offset + +def _centerDrawParaLineX( tx, offset, line, last=0): + m = offset+0.5*line.extraSpace + setXPos(tx,m) + _putFragLine(tx, line.words) + setXPos(tx,-m) + return m + +def _rightDrawParaLineX( tx, offset, line, last=0): + m = offset+line.extraSpace + setXPos(tx,m) + _putFragLine(tx, line.words) + setXPos(tx,-m) + return m + +def _justifyDrawParaLineX( tx, offset, line, last=0): + setXPos(tx,offset) + if last: + #last one, left align + _putFragLine(tx, line.words) + else: + nSpaces = line.wordCount - 1 + if nSpaces: + tx.setWordSpace(line.extraSpace / float(nSpaces)) + _putFragLine(tx, line.words) + tx.setWordSpace(0) + else: + _putFragLine(tx, line.words) + setXPos(tx,-offset) + return offset + +try: + from _rl_accel import _sameFrag +except ImportError: + try: + from reportlab.lib._rl_accel import _sameFrag + except ImportError: + def _sameFrag(f,g): + 'returns 1 if two ParaFrags map out the same' + if hasattr(f,'cbDefn') or hasattr(g,'cbDefn'): return 0 + for a in ('fontName', 'fontSize', 'textColor', 'rise', 'underline', 'link'): + if getattr(f,a)!=getattr(g,a): return 0 + return 1 + +def _getFragWords(frags): + ''' given a Parafrag list return a list of fragwords + [[size, (f00,w00), ..., (f0n,w0n)],....,[size, (fm0,wm0), ..., (f0n,wmn)]] + each pair f,w represents a style and some string + each sublist represents a word + ''' + R = [] + W = [] + n = 0 + for f in frags: + text = f.text + #del f.text # we can't do this until we sort out splitting + # of paragraphs + if text!='': + S = split(text) + if S==[]: S = [''] + if W!=[] and text[0] in whitespace: + W.insert(0,n) + R.append(W) + W = [] + n = 0 + + for w in S[:-1]: + W.append((f,w)) + n += stringWidth(w, f.fontName, f.fontSize) + W.insert(0,n) + R.append(W) + W = [] + n = 0 + + w = S[-1] + W.append((f,w)) + n += stringWidth(w, f.fontName, f.fontSize) + if text[-1] in whitespace: + W.insert(0,n) + R.append(W) + W = [] + n = 0 + elif hasattr(f,'cbDefn'): + W.append((f,'')) + + if W!=[]: + W.insert(0,n) + R.append(W) + + return R + +def _split_blParaSimple(blPara,start,stop): + f = blPara.clone() + for a in ('lines', 'kind', 'text'): + if hasattr(f,a): delattr(f,a) + + f.words = [] + for l in blPara.lines[start:stop]: + for w in l[1]: + f.words.append(w) + return [f] + +def _split_blParaHard(blPara,start,stop): + f = [] + lines = blPara.lines[start:stop] + for l in lines: + for w in l.words: + f.append(w) + if l is not lines[-1]: + i = len(f)-1 + while hasattr(f[i],'cbDefn'): i = i-1 + g = f[i] + if g.text and g.text[-1]!=' ': g.text += ' ' + return f + +def _drawBullet(canvas, offset, cur_y, bulletText, style): + '''draw a bullet text could be a simple string or a frag list''' + tx2 = canvas.beginText(style.bulletIndent, cur_y) + tx2.setFont(style.bulletFontName, style.bulletFontSize) + tx2.setFillColor(hasattr(style,'bulletColor') and style.bulletColor or style.textColor) + if type(bulletText) is StringType: + tx2.textOut(bulletText) + else: + for f in bulletText: + tx2.setFont(f.fontName, f.fontSize) + tx2.setFillColor(f.textColor) + tx2.textOut(f.text) + + canvas.drawText(tx2) + #AR making definition lists a bit less ugly + #bulletEnd = tx2.getX() + bulletEnd = tx2.getX() + style.bulletFontSize * 0.6 + offset = max(offset,bulletEnd - style.leftIndent) + return offset + +def _handleBulletWidth(bulletText,style,maxWidths): + '''work out bullet width and adjust maxWidths[0] if neccessary + ''' + if bulletText <> None: + if type(bulletText) is StringType: + bulletWidth = stringWidth( bulletText, style.bulletFontName, style.bulletFontSize) + else: + #it's a list of fragments + bulletWidth = 0 + for f in bulletText: + bulletWidth = bulletWidth + stringWidth(f.text, f.fontName, f.fontSize) + bulletRight = style.bulletIndent + bulletWidth + 0.6 * style.bulletFontSize + indent = style.leftIndent+style.firstLineIndent + if bulletRight > indent: + #..then it overruns, and we have less space available on line 1 + maxWidths[0] = maxWidths[0] - (bulletRight - indent) + +def splitLines0(frags,widths): + ''' + given a list of ParaFrags we return a list of ParaLines + + each ParaLine has + 1) ExtraSpace + 2) blankCount + 3) [textDefns....] + each text definition is a (ParaFrag, start, limit) triplet + ''' + #initialise the algorithm + lines = [] + lineNum = 0 + maxW = widths[lineNum] + i = -1 + l = len(frags) + lim = start = 0 + while 1: + #find a non whitespace character + while imaxW and line!=[]: + cLen = cLen-w + #this is the end of the line + while g.text[lim]==' ': + lim = lim - 1 + nSpaces = nSpaces-1 + break + if j<0: j = lim + if g[0] is f: g[2] = j #extend + else: + g = (f,start,j) + line.append(g) + if j==lim: + i += 1 + +def _do_under_line(i, t_off, tx): + y = tx.XtraState.cur_y - i*tx.XtraState.style.leading - tx.XtraState.f.fontSize/8.0 # 8.0 factor copied from para.py + text = join(tx.XtraState.lines[i][1]) + textlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize) + tx._canvas.line(t_off, y, t_off+textlen, y) + +def _do_under(i, t_off, tx): + xtraState = tx.XtraState + y = xtraState.cur_y - i*xtraState.style.leading - xtraState.f.fontSize/8.0 # 8.0 factor copied from para.py + ulc = None + for x1,x2,c in xtraState.underlines: + if c!=ulc: + tx._canvas.setStrokeColor(c) + ulc = c + tx._canvas.line(t_off+x1, y, t_off+x2, y) + xtraState.underlines = [] + xtraState.underline=0 + xtraState.underlineColor=None + +_scheme_re = re.compile('^[a-zA-Z][-+a-zA-Z0-9]+$') +def _doLink(tx,link,rect): + if type(link) is unicode: + link = unicode.encode('utf8') + parts = link.split(':',1) + scheme = len(parts)==2 and parts[0].lower() or '' + if _scheme_re.match(scheme) and scheme!='document': + kind=scheme.lower()=='pdf' and 'GoToR' or 'URI' + if kind=='GoToR': link = parts[1] + tx._canvas.linkURL(link, rect, relative=1, kind=kind) + else: + tx._canvas.linkRect("", scheme!='document' and link or parts[1], rect, relative=1) + +def _do_link_line(i, t_off, tx): + xs = tx.XtraState + leading = xs.style.leading + y = xs.cur_y - i*leading - xs.f.fontSize/8.0 # 8.0 factor copied from para.py + text = join(xs.lines[i][1]) + textlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize) + _doLink(tx, xs.link, (t_off, y, t_off+textlen, y+leading)) + +def _do_link(i, t_off, tx): + xs = tx.XtraState + leading = xs.style.leading + y = xs.cur_y - i*leading - xs.f.fontSize/8.0 # 8.0 factor copied from para.py + for x1,x2,link in xs.links: + tx._canvas.line(t_off+x1, y, t_off+x2, y) + _doLink(tx, link, (t_off+x1, y, t_off+x2, y+leading)) + xs.links = [] + xs.link=None + +class Paragraph(Flowable): + """ Paragraph(text, style, bulletText=None, caseSensitive=1) + text a string of stuff to go into the paragraph. + style is a style definition as in reportlab.lib.styles. + bulletText is an optional bullet defintion. + caseSensitive set this to 0 if you want the markup tags and their attributes to be case-insensitive. + + This class is a flowable that can format a block of text + into a paragraph with a given style. + + The paragraph Text can contain XML-like markup including the tags: + ... - bold + ... - italics + ... - underline + ... - superscript + ... - subscript + + + + The whole may be surrounded by tags + + It will also be able to handle any MathML specified Greek characters. + """ + def __init__(self, text, style, bulletText = None, frags=None, caseSensitive=1, encoding='utf8'): + self.caseSensitive = caseSensitive + self.encoding = encoding + self._setup(text, style, bulletText, frags, cleanBlockQuotedText) + + def __repr__(self): + import string + n = self.__class__.__name__ + L = [n+"("] + keys = self.__dict__.keys() + for k in keys: + v = getattr(self, k) + rk = repr(k) + rv = repr(v) + rk = " "+string.replace(rk, "\n", "\n ") + rv = " "+string.replace(rv, "\n", "\n ") + L.append(rk) + L.append(rv) + L.append(") #"+n) + return string.join(L, "\n") + + def _setup(self, text, style, bulletText, frags, cleaner): + if frags is None: + text = cleaner(text) + _parser.caseSensitive = self.caseSensitive + style, frags, bulletTextFrags = _parser.parse(text,style) + if frags is None: + raise "xml parser error (%s) in paragraph beginning\n'%s'"\ + % (_parser.errors[0],text[:min(30,len(text))]) + if bulletTextFrags: bulletText = bulletTextFrags + + #AR hack + self.text = text + self.frags = frags + self.style = style + self.bulletText = bulletText + self.debug = 0 #turn this on to see a pretty one with all the margins etc. + + def wrap(self, availWidth, availHeight): + # work out widths array for breaking + self.width = availWidth + leftIndent = self.style.leftIndent + first_line_width = availWidth - (leftIndent+self.style.firstLineIndent) - self.style.rightIndent + later_widths = availWidth - leftIndent - self.style.rightIndent + + if self.style.wordWrap == 'CJK': + #use Asian text wrap algorithm to break characters + self.blPara = self.breakLinesCJK([first_line_width, later_widths]) + else: + self.blPara = self.breakLines([first_line_width, later_widths]) + self.height = len(self.blPara.lines) * self.style.leading + return (self.width, self.height) + + def minWidth(self): + 'Attempt to determine a minimum sensible width' + frags = self.frags + nFrags= len(frags) + if not nFrags: return 0 + if nFrags==1: + f = frags[0] + fS = f.fontSize + fN = f.fontName + words = hasattr(f,'text') and split(f.text, ' ') or f.words + func = lambda w, fS=fS, fN=fN: stringWidth(w,fN,fS) + else: + words = _getFragWords(frags) + func = lambda x: x[0] + return max(map(func,words)) + + def _get_split_blParaFunc(self): + return self.blPara.kind==0 and _split_blParaSimple or _split_blParaHard + + def split(self,availWidth, availHeight): + if len(self.frags)<=0: return [] + + #the split information is all inside self.blPara + if not hasattr(self,'blPara'): + self.wrap(availWidth,availHeight) + blPara = self.blPara + style = self.style + leading = style.leading + lines = blPara.lines + n = len(lines) + s = int(availHeight/leading) + if s<=1: + del self.blPara + return [] + if n<=s: return [self] + func = self._get_split_blParaFunc() + + P1=self.__class__(None,style,bulletText=self.bulletText,frags=func(blPara,0,s)) + #this is a major hack + P1.blPara = ParaLines(kind=1,lines=blPara.lines[0:s],aH=availHeight,aW=availWidth) + P1._JustifyLast = 1 + P1._splitpara = 1 + P1.height = s*leading + P1.width = availWidth + if style.firstLineIndent != 0: + style = deepcopy(style) + style.firstLineIndent = 0 + P2=self.__class__(None,style,bulletText=None,frags=func(blPara,s,n)) + return [P1,P2] + + def draw(self): + #call another method for historical reasons. Besides, I + #suspect I will be playing with alternate drawing routines + #so not doing it here makes it easier to switch. + self.drawPara(self.debug) + + def breakLines(self, width): + """ + Returns a broken line structure. There are two cases + + A) For the simple case of a single formatting input fragment the output is + A fragment specifier with + kind = 0 + fontName, fontSize, leading, textColor + lines= A list of lines + Each line has two items. + 1) unused width in points + 2) word list + + B) When there is more than one input formatting fragment the output is + A fragment specifier with + kind = 1 + lines= A list of fragments each having fields + extraspace (needed for justified) + fontSize + words=word list + each word is itself a fragment with + various settings + + This structure can be used to easily draw paragraphs with the various alignments. + You can supply either a single width or a list of widths; the latter will have its + last item repeated until necessary. A 2-element list is useful when there is a + different first line indent; a longer list could be created to facilitate custom wraps + around irregular objects.""" + + if type(width) <> ListType: maxWidths = [width] + else: maxWidths = width + lines = [] + lineno = 0 + style = self.style + fFontSize = float(style.fontSize) + + #for bullets, work out width and ensure we wrap the right amount onto line one + _handleBulletWidth(self.bulletText,style,maxWidths) + + maxWidth = maxWidths[0] + + self.height = 0 + frags = self.frags + nFrags= len(frags) + if nFrags==1: + f = frags[0] + fontSize = f.fontSize + fontName = f.fontName + words = hasattr(f,'text') and split(f.text, ' ') or f.words + spaceWidth = stringWidth(' ', fontName, fontSize, self.encoding) + cLine = [] + currentWidth = - spaceWidth # hack to get around extra space for word 1 + for word in words: + #this underscores my feeling that Unicode throughout would be easier! + wordWidth = stringWidth(word, fontName, fontSize, self.encoding) + newWidth = currentWidth + spaceWidth + wordWidth + if newWidth <= maxWidth or len(cLine) == 0: + # fit one more on this line + cLine.append(word) + currentWidth = newWidth + else: + if currentWidth > self.width: self.width = currentWidth + #end of line + lines.append((maxWidth - currentWidth, cLine)) + cLine = [word] + currentWidth = wordWidth + lineno += 1 + try: + maxWidth = maxWidths[lineno] + except IndexError: + maxWidth = maxWidths[-1] # use the last one + + #deal with any leftovers on the final line + if cLine!=[]: + if currentWidth>self.width: self.width = currentWidth + lines.append((maxWidth - currentWidth, cLine)) + + return f.clone(kind=0, lines=lines) + elif nFrags<=0: + return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName, + textColor=style.textColor, lines=[]) + else: + if hasattr(self,'blPara') and getattr(self,'_splitpara',0): + #NB this is an utter hack that awaits the proper information + #preserving splitting algorithm + return self.blPara + n = 0 + nSp = 0 + for w in _getFragWords(frags): + spaceWidth = stringWidth(' ',w[-1][0].fontName, w[-1][0].fontSize) + + if n==0: + currentWidth = -spaceWidth # hack to get around extra space for word 1 + words = [] + maxSize = 0 + + wordWidth = w[0] + f = w[1][0] + if wordWidth>0: + newWidth = currentWidth + spaceWidth + wordWidth + else: + newWidth = currentWidth + if newWidth<=maxWidth or n==0: + # fit one more on this line + n += 1 + maxSize = max(maxSize,f.fontSize) + nText = w[1][1] + if words==[]: + g = f.clone() + words = [g] + g.text = nText + elif not _sameFrag(g,f): + if currentWidth>0 and ((nText!='' and nText[0]!=' ') or hasattr(f,'cbDefn')): + if hasattr(g,'cbDefn'): + i = len(words)-1 + while hasattr(words[i],'cbDefn'): i = i-1 + words[i].text += ' ' + else: + g.text += ' ' + nSp += 1 + g = f.clone() + words.append(g) + g.text = nText + else: + if nText!='' and nText[0]!=' ': + g.text += ' ' + nText + + for i in w[2:]: + g = i[0].clone() + g.text=i[1] + words.append(g) + maxSize = max(maxSize,g.fontSize) + + currentWidth = newWidth + else: + if currentWidth>self.width: self.width = currentWidth + #end of line + lines.append(FragLine(extraSpace=(maxWidth - currentWidth),wordCount=n, + words=words, fontSize=maxSize)) + + #start new line + lineno += 1 + try: + maxWidth = maxWidths[lineno] + except IndexError: + maxWidth = maxWidths[-1] # use the last one + currentWidth = wordWidth + n = 1 + maxSize = f.fontSize + g = f.clone() + words = [g] + g.text = w[1][1] + + for i in w[2:]: + g = i[0].clone() + g.text=i[1] + words.append(g) + maxSize = max(maxSize,g.fontSize) + + #deal with any leftovers on the final line + if words<>[]: + if currentWidth>self.width: self.width = currentWidth + lines.append(ParaLines(extraSpace=(maxWidth - currentWidth),wordCount=n, + words=words, fontSize=maxSize)) + return ParaLines(kind=1, lines=lines) + + return lines + + + def breakLinesCJK(self, width): + """Initially, the dumbest possible wrapping algorithm. + Cannot handle font variations.""" + + style = self.style + #for now we only handle one fragment. Need to generalize this quickly. + if len(self.frags) > 1: + raise ValueError('CJK Wordwrap can only handle one fragment per paragraph for now') + elif len(self.frags) == 0: + return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName, + textColor=style.textColor, lines=[]) + f = self.frags[0] + if 1 and hasattr(self,'blPara') and getattr(self,'_splitpara',0): + #NB this is an utter hack that awaits the proper information + #preserving splitting algorithm + return f.clone(kind=0, lines=self.blPara.lines) + if type(width)!=ListType: maxWidths = [width] + else: maxWidths = width + lines = [] + lineno = 0 + fFontSize = float(style.fontSize) + + #for bullets, work out width and ensure we wrap the right amount onto line one + _handleBulletWidth(self.bulletText, style, maxWidths) + + maxWidth = maxWidths[0] + + self.height = 0 + + + f = self.frags[0] + + if hasattr(f,'text'): + text = f.text + else: + text = ''.join(getattr(f,'words',[])) + + from reportlab.lib.textsplit import wordSplit + lines = wordSplit(text, maxWidths[0], f.fontName, f.fontSize) + #the paragraph drawing routine assumes multiple frags per line, so we need an + #extra list like this + # [space, [text]] + # + wrappedLines = [(sp, [line]) for (sp, line) in lines] + return f.clone(kind=0, lines=wrappedLines) + + + def beginText(self, x, y): + return self.canv.beginText(x, y) + + def drawPara(self,debug=0): + """Draws a paragraph according to the given style. + Returns the final y position at the bottom. Not safe for + paragraphs without spaces e.g. Japanese; wrapping + algorithm will go infinite.""" + + #stash the key facts locally for speed + canvas = self.canv + style = self.style + blPara = self.blPara + lines = blPara.lines + + #work out the origin for line 1 + leftIndent = style.leftIndent + cur_x = leftIndent + + #if has a background, draw it + if style.backColor: + canvas.saveState() + canvas.setFillColor(style.backColor) + canvas.rect(leftIndent, + 0, + self.width - (leftIndent+style.rightIndent), + self.height, + fill=1, + stroke=0) + canvas.restoreState() + + if debug: + # This boxes and shades stuff to show how the paragraph + # uses its space. Useful for self-documentation so + # the debug code stays! + # box the lot + canvas.rect(0, 0, self.width, self.height) + #left and right margins + canvas.saveState() + canvas.setFillColor(Color(0.9,0.9,0.9)) + canvas.rect(0, 0, leftIndent, self.height) + canvas.rect(self.width - style.rightIndent, 0, style.rightIndent, self.height) + # shade above and below + canvas.setFillColor(Color(1.0,1.0,0.0)) + canvas.restoreState() + #self.drawLine(x + leftIndent, y, x + leftIndent, cur_y) + + + nLines = len(lines) + bulletText = self.bulletText + if nLines > 0: + _offsets = getattr(self,'_offsets',[0]) + _offsets += (nLines-len(_offsets))*[_offsets[-1]] + canvas.saveState() + #canvas.addLiteral('%% %s.drawPara' % _className(self)) + alignment = style.alignment + offset = style.firstLineIndent+_offsets[0] + lim = nLines-1 + noJustifyLast = not (hasattr(self,'_JustifyLast') and self._JustifyLast) + + if blPara.kind==0: + if alignment == TA_LEFT: + dpl = _leftDrawParaLine + elif alignment == TA_CENTER: + dpl = _centerDrawParaLine + elif self.style.alignment == TA_RIGHT: + dpl = _rightDrawParaLine + elif self.style.alignment == TA_JUSTIFY: + dpl = _justifyDrawParaLine + f = blPara + cur_y = self.height - f.fontSize + if bulletText <> None: + offset = _drawBullet(canvas,offset,cur_y,bulletText,style) + + #set up the font etc. + canvas.setFillColor(f.textColor) + + tx = self.beginText(cur_x, cur_y) + + #now the font for the rest of the paragraph + tx.setFont(f.fontName, f.fontSize, style.leading) + t_off = dpl( tx, offset, lines[0][0], lines[0][1], noJustifyLast and nLines==1) + if f.underline or f.link: + xs = tx.XtraState=ABag() + xs.cur_y = cur_y + xs.f = f + xs.style = style + xs.lines = lines + xs.underlines=[] + xs.underlineColor=None + xs.links=[] + xs.link=f.link + canvas.setStrokeColor(f.textColor) + if f.underline: _do_under_line(0, t_off+leftIndent, tx) + if f.link: _do_link_line(0, t_off+leftIndent, tx) + + #now the middle of the paragraph, aligned with the left margin which is our origin. + for i in xrange(1, nLines): + t_off = dpl( tx, _offsets[i], lines[i][0], lines[i][1], noJustifyLast and i==lim) + if f.underline: _do_under_line(i, t_off+leftIndent, tx) + if f.link: _do_link_line(i, t_off+leftIndent, tx) + else: + for i in xrange(1, nLines): + dpl( tx, _offsets[i], lines[i][0], lines[i][1], noJustifyLast and i==lim) + else: + f = lines[0] + cur_y = self.height - f.fontSize + # default? + dpl = _leftDrawParaLineX + if bulletText <> None: + offset = _drawBullet(canvas,offset,cur_y,bulletText,style) + if alignment == TA_LEFT: + dpl = _leftDrawParaLineX + elif alignment == TA_CENTER: + dpl = _centerDrawParaLineX + elif self.style.alignment == TA_RIGHT: + dpl = _rightDrawParaLineX + elif self.style.alignment == TA_JUSTIFY: + dpl = _justifyDrawParaLineX + else: + raise ValueError, "bad align %s" % repr(alignment) + + #set up the font etc. + tx = self.beginText(cur_x, cur_y) + xs = tx.XtraState=ABag() + xs.textColor=None + xs.rise=0 + xs.underline=0 + xs.underlines=[] + xs.underlineColor=None + xs.links=[] + xs.link=None + tx.setLeading(style.leading) + xs.cur_y = cur_y + xs.f = f + xs.style = style + #f = lines[0].words[0] + #tx._setFont(f.fontName, f.fontSize) + + + tx._fontname,tx._fontsize = None, None + t_off = dpl( tx, offset, lines[0], noJustifyLast and nLines==1) + _do_under(0, t_off+leftIndent, tx) + _do_link(0, t_off+leftIndent, tx) + + #now the middle of the paragraph, aligned with the left margin which is our origin. + for i in range(1, nLines): + f = lines[i] + t_off = dpl( tx, _offsets[i], f, noJustifyLast and i==lim) + _do_under(i, t_off+leftIndent, tx) + _do_link(i, t_off+leftIndent, tx) + + canvas.drawText(tx) + canvas.restoreState() + + def getPlainText(self,identify=None): + """Convenience function for templates which want access + to the raw text, without XML tags. """ + frags = getattr(self,'frags',None) + if frags: + plains = [] + for frag in frags: + if hasattr(frag, 'text'): + plains.append(frag.text) + return join(plains, '') + elif identify: + text = getattr(self,'text',None) + if text is None: text = repr(self) + return text + else: + return '' + + def getActualLineWidths0(self): + """Convenience function; tells you how wide each line + actually is. For justified styles, this will be + the same as the wrap width; for others it might be + useful for seeing if paragraphs will fit in spaces.""" + assert hasattr(self, 'width'), "Cannot call this method before wrap()" + if self.blPara.kind: + func = lambda frag, w=self.width: w - frag.extraSpace + else: + func = lambda frag, w=self.width: w - frag[0] + return map(func,self.blPara.lines) + +if __name__=='__main__': #NORUNTESTS + def dumpParagraphLines(P): + print 'dumpParagraphLines()' % id(P) + lines = P.blPara.lines + n =len(lines) + for l in range(n): + line = lines[l] + if hasattr(line,'words'): + words = line.words + else: + words = line[1] + nwords = len(words) + print 'line%d: %d(%s)\n ' % (l,nwords,str(getattr(line,'wordCount','Unknown'))), + for w in range(nwords): + print "%d:'%s'"%(w,getattr(words[w],'text',words[w])), + print + + def dumpParagraphFrags(P): + print 'dumpParagraphFrags() minWidth() = %.2f' % (id(P), P.minWidth()) + frags = P.frags + n =len(frags) + for l in range(n): + print "frag%d: '%s'" % (l, frags[l].text) + + l = 0 + cum = 0 + for W in _getFragWords(frags): + cum += W[0] + print "fragword%d: cum=%3d size=%d" % (l, cum, W[0]), + for w in W[1:]: + print "'%s'" % w[1], + print + l += 1 + + + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + import sys + TESTS = sys.argv[1:] + if TESTS==[]: TESTS=['4'] + def flagged(i,TESTS=TESTS): + return 'all' in TESTS or '*' in TESTS or str(i) in TESTS + + styleSheet = getSampleStyleSheet() + B = styleSheet['BodyText'] + style = ParagraphStyle("discussiontext", parent=B) + style.fontName= 'Helvetica' + if flagged(1): + text='''The CMYK or subtractive method follows the way a printer +mixes three pigments (cyan, magenta, and yellow) to form colors. +Because mixing chemicals is more difficult than combining light there +is a fourth parameter for darkness. For example a chemical +combination of the CMY pigments generally never makes a perfect +black -- instead producing a muddy color -- so, to get black printers +don't use the CMY pigments but use a direct black ink. Because +CMYK maps more directly to the way printer hardware works it may +be the case that &| & | colors specified in CMYK will provide better fidelity +and better control when printed. +''' + P=Paragraph(text,style) + dumpParagraphFrags(P) + aW, aH = 456.0, 42.8 + w,h = P.wrap(aW, aH) + dumpParagraphLines(P) + S = P.split(aW,aH) + for s in S: + s.wrap(aW,aH) + dumpParagraphLines(s) + aH = 500 + + if flagged(2): + P=Paragraph("""Price*""", styleSheet['Normal']) + dumpParagraphFrags(P) + w,h = P.wrap(24, 200) + dumpParagraphLines(P) + + if flagged(3): + text = """Dieses Kapitel bietet eine schnelle Programme :: starten + +Eingabeaufforderung :: (>>>) + +>>> (Eingabeaufforderung) + +Einführung in Python Python :: Einführung +. +Das Ziel ist, die grundlegenden Eigenschaften von Python darzustellen, ohne +sich zu sehr in speziellen Regeln oder Details zu verstricken. Dazu behandelt +dieses Kapitel kurz die wesentlichen Konzepte wie Variablen, Ausdrücke, +Kontrollfluss, Funktionen sowie Ein- und Ausgabe. Es erhebt nicht den Anspruch, +umfassend zu sein.""" + P=Paragraph(text, styleSheet['Code']) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) + + if flagged(4): + text='''Die eingebaute Funktion range(i, j [, stride]) erzeugt eine Liste von Ganzzahlen und füllt sie mit Werten k, für die gilt: i <= k < j. Man kann auch eine optionale Schrittweite angeben. Die eingebaute Funktion xrange() erfüllt einen ähnlichen Zweck, gibt aber eine unveränderliche Sequenz vom Typ XRangeType zurück. Anstatt alle Werte in der Liste abzuspeichern, berechnet diese Liste ihre Werte, wann immer sie angefordert werden. Das ist sehr viel speicherschonender, wenn mit sehr langen Listen von Ganzzahlen gearbeitet wird. XRangeType kennt eine einzige Methode, s.tolist(), die seine Werte in eine Liste umwandelt.''' + aW = 420 + aH = 64.4 + P=Paragraph(text, B) + dumpParagraphFrags(P) + w,h = P.wrap(aW,aH) + print 'After initial wrap',w,h + dumpParagraphLines(P) + S = P.split(aW,aH) + dumpParagraphFrags(S[0]) + w0,h0 = S[0].wrap(aW,aH) + print 'After split wrap',w0,h0 + dumpParagraphLines(S[0]) + + if flagged(5): + text = ' %s & %s < >]]>' % (chr(163),chr(163)) + P=Paragraph(text, styleSheet['Code']) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) + + if flagged(6): + for text in ['''Here comes Helvetica 14 with strong emphasis.''', + '''Here comes Helvetica 14 with strong emphasis.''', + '''Here comes Courier 3cm and normal again.''', + ]: + P=Paragraph(text, styleSheet['Normal'], caseSensitive=0) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) + + if flagged(7): + text = """Generated by:Dilbert""" + P=Paragraph(text, styleSheet['Code']) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) diff --git a/bin/reportlab/platypus/paraparser.py b/bin/reportlab/platypus/paraparser.py new file mode 100644 index 00000000000..bb6936683b2 --- /dev/null +++ b/bin/reportlab/platypus/paraparser.py @@ -0,0 +1,898 @@ +#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/paraparser.py +__version__=''' $Id: paraparser.py 2853 2006-05-10 12:56:39Z rgbecker $ ''' +import string +import re +from types import TupleType, UnicodeType, StringType +import sys +import os +import copy + +import reportlab.lib.sequencer +from reportlab.lib.abag import ABag + +from reportlab.lib import xmllib + +from reportlab.lib.colors import toColor, white, black, red, Color +from reportlab.lib.fonts import tt2ps, ps2tt +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.units import inch,mm,cm,pica +_re_para = re.compile(r'^\s*<\s*para(?:\s+|>|/>)') + +sizeDelta = 2 # amount to reduce font size by for super and sub script +subFraction = 0.5 # fraction of font size that a sub script should be lowered +superFraction = 0.5 # fraction of font size that a super script should be raised + +def _num(s, unit=1): + """Convert a string like '10cm' to an int or float (in points). + The default unit is point, but optionally you can use other + default units like mm. + """ + if s[-2:]=='cm': + unit=cm + s = s[:-2] + if s[-2:]=='in': + unit=inch + s = s[:-2] + if s[-2:]=='pt': + unit=1 + s = s[:-2] + if s[-1:]=='i': + unit=inch + s = s[:-1] + if s[-2:]=='mm': + unit=mm + s = s[:-2] + if s[-4:]=='pica': + unit=pica + s = s[:-4] + if s[0] in ['+','-']: + try: + return ('relative',int(s)*unit) + except ValueError: + return ('relative',float(s)*unit) + else: + try: + return int(s)*unit + except ValueError: + return float(s)*unit + +def _align(s): + s = string.lower(s) + if s=='left': return TA_LEFT + elif s=='right': return TA_RIGHT + elif s=='justify': return TA_JUSTIFY + elif s in ('centre','center'): return TA_CENTER + else: raise ValueError + +_paraAttrMap = {'font': ('fontName', None), + 'face': ('fontName', None), + 'fontsize': ('fontSize', _num), + 'size': ('fontSize', _num), + 'leading': ('leading', _num), + 'lindent': ('leftIndent', _num), + 'rindent': ('rightIndent', _num), + 'findent': ('firstLineIndent', _num), + 'align': ('alignment', _align), + 'spaceb': ('spaceBefore', _num), + 'spacea': ('spaceAfter', _num), + 'bfont': ('bulletFontName', None), + 'bfontsize': ('bulletFontSize',_num), + 'bindent': ('bulletIndent',_num), + 'bcolor': ('bulletColor',toColor), + 'color':('textColor',toColor), + 'backcolor':('backColor',toColor), + 'bgcolor':('backColor',toColor), + 'bg':('backColor',toColor), + 'fg': ('textColor',toColor), + } + +_bulletAttrMap = { + 'font': ('bulletFontName', None), + 'face': ('bulletFontName', None), + 'size': ('bulletFontSize',_num), + 'fontsize': ('bulletFontSize',_num), + 'indent': ('bulletIndent',_num), + 'color': ('bulletColor',toColor), + 'fg': ('bulletColor',toColor), + } + +#things which are valid font attributes +_fontAttrMap = {'size': ('fontSize', _num), + 'face': ('fontName', None), + 'name': ('fontName', None), + 'fg': ('textColor', toColor), + 'color':('textColor', toColor), + 'backcolor':('backColor',toColor), + 'bgcolor':('backColor',toColor), + } +#things which are valid font attributes +_linkAttrMap = {'size': ('fontSize', _num), + 'face': ('fontName', None), + 'name': ('fontName', None), + 'fg': ('textColor', toColor), + 'color':('textColor', toColor), + 'backcolor':('backColor',toColor), + 'bgcolor':('backColor',toColor), + 'dest': ('link', None), + 'destination': ('link', None), + 'target': ('link', None), + 'href': ('link', None), + } + +def _addAttributeNames(m): + K = m.keys() + for k in K: + n = m[k][0] + if not m.has_key(n): m[n] = m[k] + n = string.lower(n) + if not m.has_key(n): m[n] = m[k] + +_addAttributeNames(_paraAttrMap) +_addAttributeNames(_fontAttrMap) +_addAttributeNames(_bulletAttrMap) + +def _applyAttributes(obj, attr): + for k, v in attr.items(): + if type(v) is TupleType and v[0]=='relative': + #AR 20/5/2000 - remove 1.5.2-ism + #v = v[1]+getattr(obj,k,0) + if hasattr(obj, k): + v = v[1]+getattr(obj,k) + else: + v = v[1] + setattr(obj,k,v) + +#Named character entities intended to be supported from the special font +#with additions suggested by Christoph Zwerschke who also suggested the +#numeric entity names that follow. +greeks = { + 'pound': '\xc2\xa3', + 'nbsp': '\xc2\xa0', + 'alefsym': '\xe2\x84\xb5', + 'Alpha': '\xce\x91', + 'alpha': '\xce\xb1', + 'and': '\xe2\x88\xa7', + 'ang': '\xe2\x88\xa0', + 'asymp': '\xe2\x89\x88', + 'Beta': '\xce\x92', + 'beta': '\xce\xb2', + 'bull': '\xe2\x80\xa2', + 'cap': '\xe2\x88\xa9', + 'Chi': '\xce\xa7', + 'chi': '\xcf\x87', + 'clubs': '\xe2\x99\xa3', + 'cong': '\xe2\x89\x85', + 'cup': '\xe2\x88\xaa', + 'darr': '\xe2\x86\x93', + 'dArr': '\xe2\x87\x93', + 'delta': '\xce\xb4', + 'Delta': '\xe2\x88\x86', + 'diams': '\xe2\x99\xa6', + 'empty': '\xe2\x88\x85', + 'Epsilon': '\xce\x95', + 'epsilon': '\xce\xb5', + 'epsiv': '\xce\xb5', + 'equiv': '\xe2\x89\xa1', + 'Eta': '\xce\x97', + 'eta': '\xce\xb7', + 'euro': '\xe2\x82\xac', + 'exist': '\xe2\x88\x83', + 'forall': '\xe2\x88\x80', + 'frasl': '\xe2\x81\x84', + 'Gamma': '\xce\x93', + 'gamma': '\xce\xb3', + 'ge': '\xe2\x89\xa5', + 'harr': '\xe2\x86\x94', + 'hArr': '\xe2\x87\x94', + 'hearts': '\xe2\x99\xa5', + 'hellip': '\xe2\x80\xa6', + 'image': '\xe2\x84\x91', + 'infin': '\xe2\x88\x9e', + 'int': '\xe2\x88\xab', + 'Iota': '\xce\x99', + 'iota': '\xce\xb9', + 'isin': '\xe2\x88\x88', + 'Kappa': '\xce\x9a', + 'kappa': '\xce\xba', + 'Lambda': '\xce\x9b', + 'lambda': '\xce\xbb', + 'lang': '\xe2\x8c\xa9', + 'larr': '\xe2\x86\x90', + 'lArr': '\xe2\x87\x90', + 'lceil': '\xef\xa3\xae', + 'le': '\xe2\x89\xa4', + 'lfloor': '\xef\xa3\xb0', + 'lowast': '\xe2\x88\x97', + 'loz': '\xe2\x97\x8a', + 'minus': '\xe2\x88\x92', + 'mu': '\xc2\xb5', + 'Mu': '\xce\x9c', + 'nabla': '\xe2\x88\x87', + 'ne': '\xe2\x89\xa0', + 'ni': '\xe2\x88\x8b', + 'notin': '\xe2\x88\x89', + 'nsub': '\xe2\x8a\x84', + 'Nu': '\xce\x9d', + 'nu': '\xce\xbd', + 'oline': '\xef\xa3\xa5', + 'omega': '\xcf\x89', + 'Omega': '\xe2\x84\xa6', + 'Omicron': '\xce\x9f', + 'omicron': '\xce\xbf', + 'oplus': '\xe2\x8a\x95', + 'or': '\xe2\x88\xa8', + 'otimes': '\xe2\x8a\x97', + 'part': '\xe2\x88\x82', + 'perp': '\xe2\x8a\xa5', + 'Phi': '\xce\xa6', + 'phi': '\xcf\x95', + 'phis': '\xcf\x86', + 'Pi': '\xce\xa0', + 'pi': '\xcf\x80', + 'piv': '\xcf\x96', + 'prime': '\xe2\x80\xb2', + 'prod': '\xe2\x88\x8f', + 'prop': '\xe2\x88\x9d', + 'Psi': '\xce\xa8', + 'psi': '\xcf\x88', + 'radic': '\xe2\x88\x9a', + 'rang': '\xe2\x8c\xaa', + 'rarr': '\xe2\x86\x92', + 'rArr': '\xe2\x87\x92', + 'rceil': '\xef\xa3\xb9', + 'real': '\xe2\x84\x9c', + 'rfloor': '\xef\xa3\xbb', + 'Rho': '\xce\xa1', + 'rho': '\xcf\x81', + 'sdot': '\xe2\x8b\x85', + 'Sigma': '\xce\xa3', + 'sigma': '\xcf\x83', + 'sigmaf': '\xcf\x82', + 'sigmav': '\xcf\x82', + 'sim': '\xe2\x88\xbc', + 'spades': '\xe2\x99\xa0', + 'sub': '\xe2\x8a\x82', + 'sube': '\xe2\x8a\x86', + 'sum': '\xe2\x88\x91', + 'sup': '\xe2\x8a\x83', + 'supe': '\xe2\x8a\x87', + 'Tau': '\xce\xa4', + 'tau': '\xcf\x84', + 'there4': '\xe2\x88\xb4', + 'Theta': '\xce\x98', + 'theta': '\xce\xb8', + 'thetasym': '\xcf\x91', + 'thetav': '\xcf\x91', + 'trade': '\xef\xa3\xaa', + 'uarr': '\xe2\x86\x91', + 'uArr': '\xe2\x87\x91', + 'upsih': '\xcf\x92', + 'Upsilon': '\xce\xa5', + 'upsilon': '\xcf\x85', + 'weierp': '\xe2\x84\x98', + 'Xi': '\xce\x9e', + 'xi': '\xce\xbe', + 'Zeta': '\xce\x96', + 'zeta': '\xce\xb6', + } + +#------------------------------------------------------------------------ +class ParaFrag(ABag): + """class ParaFrag contains the intermediate representation of string + segments as they are being parsed by the XMLParser. + fontname, fontSize, rise, textColor, cbDefn + """ + + +_greek2Utf8=None +def _greekConvert(data): + global _greek2Utf8 + if not _greek2Utf8: + from reportlab.pdfbase.rl_codecs import RL_Codecs + import codecs + dm = decoding_map = codecs.make_identity_dict(xrange(32,256)) + for k in xrange(0,32): + dm[k] = None + dm.update(RL_Codecs._RL_Codecs__rl_codecs_data['symbol'][0]) + _greek2Utf8 = {} + for k,v in dm.iteritems(): + if not v: + u = '\0' + else: + u = unichr(v).encode('utf8') + _greek2Utf8[chr(k)] = u + return ''.join(map(_greek2Utf8.__getitem__,data)) + +#------------------------------------------------------------------ +# !!! NOTE !!! THIS TEXT IS NOW REPLICATED IN PARAGRAPH.PY !!! +# The ParaFormatter will be able to format the following +# tags: +# < /b > - bold +# < /i > - italics +# < u > < /u > - underline +# < super > < /super > - superscript +# < sup > < /sup > - superscript +# < sub > < /sub > - subscript +# +# < bullet > - bullet text (at head of para only) +# +# +# +# - +# +# The whole may be surrounded by tags +# +# It will also be able to handle any MathML specified Greek characters. +#------------------------------------------------------------------ +class ParaParser(xmllib.XMLParser): + + #---------------------------------------------------------- + # First we will define all of the xml tag handler functions. + # + # start_(attributes) + # end_() + # + # While parsing the xml ParaFormatter will call these + # functions to handle the string formatting tags. + # At the start of each tag the corresponding field will + # be set to 1 and at the end tag the corresponding field will + # be set to 0. Then when handle_data is called the options + # for that data will be aparent by the current settings. + #---------------------------------------------------------- + + def __getattr__( self, attrName ): + """This way we can handle the same way as (ignoring case).""" + if attrName!=attrName.lower() and attrName!="caseSensitive" and not self.caseSensitive and \ + (attrName.startswith("start_") or attrName.startswith("end_")): + return getattr(self,attrName.lower()) + raise AttributeError, attrName + + #### bold + def start_b( self, attributes ): + self._push(bold=1) + + def end_b( self ): + self._pop(bold=1) + + def start_strong( self, attributes ): + self._push(bold=1) + + def end_strong( self ): + self._pop(bold=1) + + #### italics + def start_i( self, attributes ): + self._push(italic=1) + + def end_i( self ): + self._pop(italic=1) + + def start_em( self, attributes ): + self._push(italic=1) + + def end_em( self ): + self._pop(italic=1) + + #### underline + def start_u( self, attributes ): + self._push(underline=1) + + def end_u( self ): + self._pop(underline=1) + + #### link + def start_link(self, attributes): + self._push(**self.getAttributes(attributes,_linkAttrMap)) + + def end_link(self): + frag = self._stack[-1] + del self._stack[-1] + assert frag.link!=None + + #### super script + def start_super( self, attributes ): + self._push(super=1) + + def end_super( self ): + self._pop(super=1) + + start_sup = start_super + end_sup = end_super + + #### sub script + def start_sub( self, attributes ): + self._push(sub=1) + + def end_sub( self ): + self._pop(sub=1) + + #### greek script + #### add symbol encoding + def handle_charref(self, name): + try: + if name[0]=='x': + n = int(name[1:],16) + else: + n = int(name) + except ValueError: + self.unknown_charref(name) + return + self.handle_data(unichr(n).encode('utf8')) + + def handle_entityref(self,name): + if greeks.has_key(name): + self.handle_data(greeks[name]) + else: + xmllib.XMLParser.handle_entityref(self,name) + + def syntax_error(self,lineno,message): + self._syntax_error(message) + + def _syntax_error(self,message): + if message[:10]=="attribute " and message[-17:]==" value not quoted": return + self.errors.append(message) + + def start_greek(self, attr): + self._push(greek=1) + + def end_greek(self): + self._pop(greek=1) + + def start_unichar(self, attr): + if attr.has_key('name'): + if attr.has_key('code'): + self._syntax_error(' invalid with both name and code attributes') + try: + v = unicodedata.lookup(attr['name']).encode('utf8') + except KeyError: + self._syntax_error(' invalid name attribute\n"%s"' % name) + v = '\0' + elif attr.has_key('code'): + try: + v = unichr(int(eval(attr['code']))).encode('utf8') + except: + self._syntax_error(' invalid code attribute %s' % attr['code']) + v = '\0' + else: + v = None + if attr: + self._syntax_error(' invalid attribute %s' % attr.keys()[0]) + + if v is not None: + self.handle_data(v) + self._push(_selfClosingTag='unichar') + + def end_unichar(self): + self._pop() + + def start_font(self,attr): + self._push(**self.getAttributes(attr,_fontAttrMap)) + + def end_font(self): + self._pop() + + def _initial_frag(self,attr,attrMap,bullet=0): + style = self._style + if attr!={}: + style = copy.deepcopy(style) + _applyAttributes(style,self.getAttributes(attr,attrMap)) + self._style = style + + # initialize semantic values + frag = ParaFrag() + frag.sub = 0 + frag.super = 0 + frag.rise = 0 + frag.underline = 0 + frag.greek = 0 + frag.link = None + if bullet: + frag.fontName, frag.bold, frag.italic = ps2tt(style.bulletFontName) + frag.fontSize = style.bulletFontSize + frag.textColor = hasattr(style,'bulletColor') and style.bulletColor or style.textColor + else: + frag.fontName, frag.bold, frag.italic = ps2tt(style.fontName) + frag.fontSize = style.fontSize + frag.textColor = style.textColor + return frag + + def start_para(self,attr): + self._stack = [self._initial_frag(attr,_paraAttrMap)] + + def end_para(self): + self._pop() + + def start_bullet(self,attr): + if hasattr(self,'bFragList'): + self._syntax_error('only one tag allowed') + self.bFragList = [] + frag = self._initial_frag(attr,_bulletAttrMap,1) + frag.isBullet = 1 + self._stack.append(frag) + + def end_bullet(self): + self._pop() + + #--------------------------------------------------------------- + def start_seqdefault(self, attr): + try: + default = attr['id'] + except KeyError: + default = None + self._seq.setDefaultCounter(default) + + def end_seqdefault(self): + pass + + def start_seqreset(self, attr): + try: + id = attr['id'] + except KeyError: + id = None + try: + base = int(attr['base']) + except: + base=0 + self._seq.reset(id, base) + + def end_seqreset(self): + pass + + def start_seqchain(self, attr): + try: + order = attr['order'] + except KeyError: + order = '' + order = order.split() + seq = self._seq + for p,c in zip(order[:-1],order[1:]): + seq.chain(p, c) + end_seqchain = end_seqreset + + def start_seqformat(self, attr): + try: + id = attr['id'] + except KeyError: + id = None + try: + value = attr['value'] + except KeyError: + value = '1' + self._seq.setFormat(id,value) + end_seqformat = end_seqreset + + # AR hacking in aliases to allow the proper casing for RML. + # the above ones should be deprecated over time. 2001-03-22 + start_seqDefault = start_seqdefault + end_seqDefault = end_seqdefault + start_seqReset = start_seqreset + end_seqReset = end_seqreset + start_seqChain = start_seqchain + end_seqChain = end_seqchain + start_seqFormat = start_seqformat + end_seqFormat = end_seqformat + + def start_seq(self, attr): + #if it has a template, use that; otherwise try for id; + #otherwise take default sequence + if attr.has_key('template'): + templ = attr['template'] + self.handle_data(templ % self._seq) + return + elif attr.has_key('id'): + id = attr['id'] + else: + id = None + output = self._seq.nextf(id) + self.handle_data(output) + + def end_seq(self): + pass + + def start_onDraw(self,attr): + defn = ABag() + if attr.has_key('name'): defn.name = attr['name'] + else: self._syntax_error(' needs at least a name attribute') + + if attr.has_key('label'): defn.label = attr['label'] + defn.kind='onDraw' + self._push(cbDefn=defn) + self.handle_data('') + self._pop() + + #--------------------------------------------------------------- + def _push(self,**attr): + frag = copy.copy(self._stack[-1]) + _applyAttributes(frag,attr) + self._stack.append(frag) + + def _pop(self,**kw): + frag = self._stack[-1] + del self._stack[-1] + for k, v in kw.items(): + assert getattr(frag,k)==v + return frag + + def getAttributes(self,attr,attrMap): + A = {} + for k, v in attr.items(): + if not self.caseSensitive: + k = string.lower(k) + if k in attrMap.keys(): + j = attrMap[k] + func = j[1] + try: + A[j[0]] = (func is None) and v or func(v) + except: + self._syntax_error('%s: invalid value %s'%(k,v)) + else: + self._syntax_error('invalid attribute name %s'%k) + return A + + #---------------------------------------------------------------- + + def __init__(self,verbose=0): + self.caseSensitive = 0 + xmllib.XMLParser.__init__(self,verbose=verbose) + + def _iReset(self): + self.fragList = [] + if hasattr(self, 'bFragList'): delattr(self,'bFragList') + + def _reset(self, style): + '''reset the parser''' + xmllib.XMLParser.reset(self) + + # initialize list of string segments to empty + self.errors = [] + self._style = style + self._iReset() + + #---------------------------------------------------------------- + def handle_data(self,data): + "Creates an intermediate representation of string segments." + + frag = copy.copy(self._stack[-1]) + if hasattr(frag,'cbDefn'): + if data!='': syntax_error('Only tag allowed') + elif hasattr(frag,'_selfClosingTag'): + if data!='': syntax_error('No content allowed in %s tag' % frag._selfClosingTag) + return + else: + # if sub and super are both on they will cancel each other out + if frag.sub == 1 and frag.super == 1: + frag.sub = 0 + frag.super = 0 + + if frag.sub: + frag.rise = -frag.fontSize*subFraction + frag.fontSize = max(frag.fontSize-sizeDelta,3) + elif frag.super: + frag.rise = frag.fontSize*superFraction + frag.fontSize = max(frag.fontSize-sizeDelta,3) + + if frag.greek: + frag.fontName = 'symbol' + data = _greekConvert(data) + + # bold, italic, and underline + x = frag.fontName = tt2ps(frag.fontName,frag.bold,frag.italic) + + #save our data + frag.text = data + + if hasattr(frag,'isBullet'): + delattr(frag,'isBullet') + self.bFragList.append(frag) + else: + self.fragList.append(frag) + + def handle_cdata(self,data): + self.handle_data(data) + + def _setup_for_parse(self,style): + self._seq = reportlab.lib.sequencer.getSequencer() + self._reset(style) # reinitialise the parser + + def parse(self, text, style): + """Given a formatted string will return a list of + ParaFrag objects with their calculated widths. + If errors occur None will be returned and the + self.errors holds a list of the error messages. + """ + # AR 20040612 - when we feed Unicode strings in, sgmlop + # tries to coerce to ASCII. Must intercept, coerce to + # any 8-bit encoding which defines most of 256 points, + # and revert at end. Yuk. Preliminary step prior to + # removal of parser altogether. + enc = self._enc = 'cp1252' #our legacy default + self._UNI = type(text) is UnicodeType + if self._UNI: + text = text.encode(enc) + + self._setup_for_parse(style) + # the xmlparser requires that all text be surrounded by xml + # tags, therefore we must throw some unused flags around the + # given string + if not(len(text)>=6 and text[0]=='<' and _re_para.match(text)): + text = ""+text+"" + self.feed(text) + self.close() # force parsing to complete + return self._complete_parse() + + def _complete_parse(self): + del self._seq + style = self._style + del self._style + if len(self.errors)==0: + fragList = self.fragList + bFragList = hasattr(self,'bFragList') and self.bFragList or None + self._iReset() + else: + fragList = bFragList = None + + if self._UNI: + #reconvert to unicode + if fragList: + for frag in fragList: + frag.text = unicode(frag.text, self._enc) + if bFragList: + for frag in bFragList: + frag.text = unicode(frag.text, self._enc) + + return style, fragList, bFragList + + def _tt_parse(self,tt): + tag = tt[0] + try: + start = getattr(self,'start_'+tag) + end = getattr(self,'end_'+tag) + except AttributeError: + raise ValueError('Invalid tag "%s"' % tag) + start(tt[1] or {}) + C = tt[2] + if C: + M = self._tt_handlers + for c in C: + M[type(c) is TupleType](c) + end() + + def tt_parse(self,tt,style): + '''parse from tupletree form''' + self._setup_for_parse(style) + self._tt_handlers = self.handle_data,self._tt_parse + self._tt_parse(tt) + return self._complete_parse() + +if __name__=='__main__': + from reportlab.platypus import cleanBlockQuotedText + _parser=ParaParser() + def check_text(text,p=_parser): + print '##########' + text = cleanBlockQuotedText(text) + l,rv,bv = p.parse(text,style) + if rv is None: + for l in _parser.errors: + print l + else: + print 'ParaStyle', l.fontName,l.fontSize,l.textColor + for l in rv: + print l.fontName,l.fontSize,l.textColor,l.bold, l.rise, '|%s|'%l.text[:25], + if hasattr(l,'cbDefn'): + print 'cbDefn',l.cbDefn.name,l.cbDefn.label,l.cbDefn.kind + else: print + + style=ParaFrag() + style.fontName='Times-Roman' + style.fontSize = 12 + style.textColor = black + style.bulletFontName = black + style.bulletFontName='Times-Roman' + style.bulletFontSize=12 + + text=''' + aDβ + + Tell me, O muse, of that ingenious hero who travelled far and wide + after he had sacked the famous town of Troy. Many cities did he visit, + and many were the nations with whose manners and customs he was acquainted; + moreover he suffered much by sea while trying to save his own life + and bring his men safely home; but do what he might he could not save + his men, for they perished through their own sheer folly in eating + the cattle of the Sun-god Hyperion; so the god prevented them from + ever reaching home. Tell me, too, about all these things, O daughter + of Jove, from whatsoever source you1 may know them. + ''' + check_text(text) + check_text(' ') + check_text('ReportLab -- Reporting for the Internet Age') + check_text(''' + τTell me, O muse, of that ingenious hero who travelled far and wide + after he had sacked the famous town of Troy. Many cities did he visit, + and many were the nations with whose manners and customs he was acquainted; + moreover he suffered much by sea while trying to save his own life + and bring his men safely home; but do what he might he could not save + his men, for they perished through their own sheer folly in eating + the cattle of the Sun-god Hyperion; so the god prevented them from + ever reaching home. Tell me, too, about all these things, O daughter + of Jove, from whatsoever source you may know them.''') + check_text(''' + Telemachus took this speech as of good omen and rose at once, for + he was bursting with what he had to say. He stood in the middle of + the assembly and the good herald Pisenor brought him his staff. Then, + turning to Aegyptius, "Sir," said he, "it is I, as you will shortly + learn, who have convened you, for it is I who am the most aggrieved. + I have not got wind of any host approaching about which I would warn + you, nor is there any matter of public moment on which I would speak. + My grieveance is purely personal, and turns on two great misfortunes + which have fallen upon my house. The first of these is the loss of + my excellent father, who was chief among all you here present, and + was like a father to every one of you; the second is much more serious, + and ere long will be the utter ruin of my estate. The sons of all + the chief men among you are pestering my mother to marry them against + her will. They are afraid to go to her father Icarius, asking him + to choose the one he likes best, and to provide marriage gifts for + his daughter, but day by day they keep hanging about my father's house, + sacrificing our oxen, sheep, and fat goats for their banquets, and + never giving so much as a thought to the quantity of wine they drink. + No estate can stand such recklessness; we have now no Ulysses to ward + off harm from our doors, and I cannot hold my own against them. I + shall never all my days be as good a man as he was, still I would + indeed defend myself if I had power to do so, for I cannot stand such + treatment any longer; my house is being disgraced and ruined. Have + respect, therefore, to your own consciences and to public opinion. + Fear, too, the wrath of heaven, lest the gods should be displeased + and turn upon you. I pray you by Jove and Themis, who is the beginning + and the end of councils, [do not] hold back, my friends, and leave + me singlehanded- unless it be that my brave father Ulysses did some + wrong to the Achaeans which you would now avenge on me, by aiding + and abetting these suitors. Moreover, if I am to be eaten out of house + and home at all, I had rather you did the eating yourselves, for I + could then take action against you to some purpose, and serve you + with notices from house to house till I got paid in full, whereas + now I have no remedy."''') + + check_text(''' +But as the sun was rising from the fair sea into the firmament of +heaven to shed light on mortals and immortals, they reached Pylos +the city of Neleus. Now the people of Pylos were gathered on the sea +shore to offer sacrifice of black bulls to Neptune lord of the Earthquake. +There were nine guilds with five hundred men in each, and there were +nine bulls to each guild. As they were eating the inward meats and +burning the thigh bones [on the embers] in the name of Neptune, Telemachus +and his crew arrived, furled their sails, brought their ship to anchor, +and went ashore. ''') + check_text(''' +So the neighbours and kinsmen of Menelaus were feasting and making +merry in his house. There was a bard also to sing to them and play +his lyre, while two tumblers went about performing in the midst of +them when the man struck up with his tune.]''') + check_text(''' +"When we had passed the [Wandering] rocks, with Scylla and terrible +Charybdis, we reached the noble island of the sun-god, where were +the goodly cattle and sheep belonging to the sun Hyperion. While still +at sea in my ship I could bear the cattle lowing as they came home +to the yards, and the sheep bleating. Then I remembered what the blind +Theban prophet Teiresias had told me, and how carefully Aeaean Circe +had warned me to shun the island of the blessed sun-god. So being +much troubled I said to the men, 'My men, I know you are hard pressed, +but listen while I tell you the prophecy that Teiresias made me, and +how carefully Aeaean Circe warned me to shun the island of the blessed +sun-god, for it was here, she said, that our worst danger would lie. +Head the ship, therefore, away from the island.''') + check_text('''A<B>C&D"E'F''') + check_text('''A< B> C& D" E' F''') + check_text('''&'"]]>''') + check_text('''+ +There was a bard also to sing to them and play +his lyre, while two tumblers went about performing in the midst of +them when the man struck up with his tune.]''') + check_text('''A paragraph''') + check_text('''B paragraph''') + # HVB, 30.05.2003: Test for new features + _parser.caseSensitive=0 + check_text('''Here comes Helvetica 14 with strong emphasis.''') + check_text('''Here comes Helvetica 14 with strong emphasis.''') + check_text('''Here comes Courier 3cm and normal again.''') diff --git a/bin/reportlab/platypus/tableofcontents.py b/bin/reportlab/platypus/tableofcontents.py new file mode 100644 index 00000000000..c1ad3d525db --- /dev/null +++ b/bin/reportlab/platypus/tableofcontents.py @@ -0,0 +1,329 @@ +#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/tableofcontents.py +""" +This module defines a single TableOfContents() class that can be used to +create automatically a table of tontents for Platypus documents like +this: + + story = [] + toc = TableOfContents() + story.append(toc) + # some heading paragraphs here... + doc = MyTemplate(path) + doc.multiBuild(story) + +The data needed to create the table is a list of (level, text, pageNum) +triplets, plus some paragraph styles for each level of the table itself. +The triplets will usually be created in a document template's method +like afterFlowable(), making notification calls using the notify() +method with appropriate data like this: + + (level, text, pageNum) = ... + self.notify('TOCEntry', (level, text, pageNum)) + +As the table of contents need at least two passes over the Platypus +story which is why the moultiBuild0() method must be called. + +The levelParaStyle variables are the paragraph styles used +to format the entries in the table of contents. Their indentation +is calculated like this: each entry starts at a multiple of some +constant named delta. If one entry spans more than one line, all +lines after the first are indented by the same constant named +epsilon. +""" +__version__=''' $Id: tableofcontents.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +import string + +from reportlab.lib import enums +from reportlab.lib.units import cm +from reportlab.lib.styles import ParagraphStyle +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.doctemplate import IndexingFlowable +from reportlab.platypus.tables import TableStyle, Table + + +# Default paragraph styles for tables of contents. +# (This could also be generated automatically or even +# on-demand if it is not known how many levels the +# TOC will finally need to display...) + +delta = 1*cm +epsilon = 0.5*cm + +levelZeroParaStyle = \ + ParagraphStyle(name='LevelZero', + fontName='Times-Roman', + fontSize=10, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 0*delta + epsilon) + +levelOneParaStyle = \ + ParagraphStyle(name='LevelOne', + parent = levelZeroParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 1*delta + epsilon) + +levelTwoParaStyle = \ + ParagraphStyle(name='LevelTwo', + parent = levelOneParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 2*delta + epsilon) + +levelThreeParaStyle = \ + ParagraphStyle(name='LevelThree', + parent = levelTwoParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 3*delta + epsilon) + +levelFourParaStyle = \ + ParagraphStyle(name='LevelFour', + parent = levelTwoParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 4*delta + epsilon) + +defaultTableStyle = \ + TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP')]) + + +class TableOfContents(IndexingFlowable): + """This creates a formatted table of contents. + + It presumes a correct block of data is passed in. + The data block contains a list of (level, text, pageNumber) + triplets. You can supply a paragraph style for each level + (starting at zero). + """ + + def __init__(self): + self.entries = [] + self.rightColumnWidth = 72 + self.levelStyles = [levelZeroParaStyle, + levelOneParaStyle, + levelTwoParaStyle, + levelThreeParaStyle, + levelFourParaStyle] + self.tableStyle = defaultTableStyle + self._table = None + self._entries = [] + self._lastEntries = [] + + + def beforeBuild(self): + # keep track of the last run + self._lastEntries = self._entries[:] + self.clearEntries() + + + def isIndexing(self): + return 1 + + + def isSatisfied(self): + return (self._entries == self._lastEntries) + + def notify(self, kind, stuff): + """The notification hook called to register all kinds of events. + + Here we are interested in 'TOCEntry' events only. + """ + if kind == 'TOCEntry': + (level, text, pageNum) = stuff + self.addEntry(level, text, pageNum) + + + def clearEntries(self): + self._entries = [] + + + def addEntry(self, level, text, pageNum): + """Adds one entry to the table of contents. + + This allows incremental buildup by a doctemplate. + Requires that enough styles are defined.""" + + assert type(level) == type(1), "Level must be an integer" + assert level < len(self.levelStyles), \ + "Table of contents must have a style defined " \ + "for paragraph level %d before you add an entry" % level + + self._entries.append((level, text, pageNum)) + + + def addEntries(self, listOfEntries): + """Bulk creation of entries in the table of contents. + + If you knew the titles but not the page numbers, you could + supply them to get sensible output on the first run.""" + + for (level, text, pageNum) in listOfEntries: + self.addEntry(level, text, pageNum) + + + def wrap(self, availWidth, availHeight): + "All table properties should be known by now." + + widths = (availWidth - self.rightColumnWidth, + self.rightColumnWidth) + + # makes an internal table which does all the work. + # we draw the LAST RUN's entries! If there are + # none, we make some dummy data to keep the table + # from complaining + if len(self._lastEntries) == 0: + _tempEntries = [(0,'Placeholder for table of contents',0)] + else: + _tempEntries = self._lastEntries + + tableData = [] + for (level, text, pageNum) in _tempEntries: + leftColStyle = self.levelStyles[level] + #right col style is right aligned + rightColStyle = ParagraphStyle(name='leftColLevel%d' % level, + parent=leftColStyle, + leftIndent=0, + alignment=enums.TA_RIGHT) + leftPara = Paragraph(text, leftColStyle) + rightPara = Paragraph(str(pageNum), rightColStyle) + tableData.append([leftPara, rightPara]) + + self._table = Table(tableData, colWidths=widths, + style=self.tableStyle) + + self.width, self.height = self._table.wrapOn(self.canv,availWidth, availHeight) + return (self.width, self.height) + + + def split(self, availWidth, availHeight): + """At this stage we do not care about splitting the entries, + we will just return a list of platypus tables. Presumably the + calling app has a pointer to the original TableOfContents object; + Platypus just sees tables. + """ + return self._table.splitOn(self.canv,availWidth, availHeight) + + + def drawOn(self, canvas, x, y, _sW=0): + """Don't do this at home! The standard calls for implementing + draw(); we are hooking this in order to delegate ALL the drawing + work to the embedded table object. + """ + self._table.drawOn(canvas, x, y, _sW) + + +class SimpleIndex(IndexingFlowable): + """This creates a very simple index. + + Entries have a string key, and appear with a page number on + the right. Prototype for more sophisticated multi-level index.""" + def __init__(self): + #keep stuff in a dictionary while building + self._entries = {} + self._lastEntries = {} + self._table = None + self.textStyle = ParagraphStyle(name='index', + fontName='Times-Roman', + fontSize=12) + def isIndexing(self): + return 1 + + def isSatisfied(self): + return (self._entries == self._lastEntries) + + def beforeBuild(self): + # keep track of the last run + self._lastEntries = self._entries.copy() + self.clearEntries() + + def clearEntries(self): + self._entries = {} + + def notify(self, kind, stuff): + """The notification hook called to register all kinds of events. + + Here we are interested in 'IndexEntry' events only. + """ + if kind == 'IndexEntry': + (text, pageNum) = stuff + self.addEntry(text, pageNum) + + def addEntry(self, text, pageNum): + """Allows incremental buildup""" + if self._entries.has_key(text): + self._entries[text].append(str(pageNum)) + else: + self._entries[text] = [pageNum] + + def split(self, availWidth, availHeight): + """At this stage we do not care about splitting the entries, + we will just return a list of platypus tables. Presumably the + calling app has a pointer to the original TableOfContents object; + Platypus just sees tables. + """ + return self._table.splitOn(self.canv,availWidth, availHeight) + + def wrap(self, availWidth, availHeight): + "All table properties should be known by now." + # makes an internal table which does all the work. + # we draw the LAST RUN's entries! If there are + # none, we make some dummy data to keep the table + # from complaining + if len(self._lastEntries) == 0: + _tempEntries = [('Placeholder for index',[0,1,2])] + else: + _tempEntries = self._lastEntries.items() + _tempEntries.sort() + + tableData = [] + for (text, pageNumbers) in _tempEntries: + #right col style is right aligned + allText = text + ': ' + string.join(map(str, pageNumbers), ', ') + para = Paragraph(allText, self.textStyle) + tableData.append([para]) + + self._table = Table(tableData, colWidths=[availWidth]) + + self.width, self.height = self._table.wrapOn(self.canv,availWidth, availHeight) + return (self.width, self.height) + + def drawOn(self, canvas, x, y, _sW=0): + """Don't do this at home! The standard calls for implementing + draw(); we are hooking this in order to delegate ALL the drawing + work to the embedded table object. + """ + self._table.drawOn(canvas, x, y, _sW) + +class ReferenceText(IndexingFlowable): + """Fakery to illustrate how a reference would work if we could + put it in a paragraph.""" + def __init__(self, textPattern, targetKey): + self.textPattern = textPattern + self.target = targetKey + self.paraStyle = ParagraphStyle('tmp') + self._lastPageNum = None + self._pageNum = -999 + self._para = None + + def beforeBuild(self): + self._lastPageNum = self._pageNum + + def notify(self, kind, stuff): + if kind == 'Target': + (key, pageNum) = stuff + if key == self.target: + self._pageNum = pageNum + + def wrap(self, availWidth, availHeight): + text = self.textPattern % self._lastPageNum + self._para = Paragraph(text, self.paraStyle) + return self._para.wrap(availWidth, availHeight) + + def drawOn(self, canvas, x, y, _sW=0): + self._para.drawOn(canvas, x, y, _sW) + + diff --git a/bin/reportlab/platypus/tables.py b/bin/reportlab/platypus/tables.py new file mode 100755 index 00000000000..62fefbd8d77 --- /dev/null +++ b/bin/reportlab/platypus/tables.py @@ -0,0 +1,1384 @@ +#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/tables.py +__version__=''' $Id: tables.py 2849 2006-05-06 08:25:23Z andy $ ''' + +__doc__=""" +Tables are created by passing the constructor a tuple of column widths, a tuple of row heights and the data in +row order. Drawing of the table can be controlled by using a TableStyle instance. This allows control of the +color and weight of the lines (if any), and the font, alignment and padding of the text. + +None values in the sequence of row heights or column widths, mean that the corresponding rows +or columns should be automatically sized. + +All the cell values should be convertible to strings; embedded newline '\\n' characters +cause the value to wrap (ie are like a traditional linefeed). + +See the test output from running this module as a script for a discussion of the method for constructing +tables and table styles. +""" +from reportlab.platypus.flowables import Flowable, Preformatted +from reportlab import rl_config +from reportlab.lib.styles import PropertySet, ParagraphStyle +from reportlab.lib import colors +from reportlab.lib.utils import fp_str +from reportlab.pdfbase import pdfmetrics +import operator, string +from types import TupleType, ListType, StringType, FloatType, IntType + +class CellStyle(PropertySet): + defaults = { + 'fontname':'Times-Roman', + 'fontsize':10, + 'leading':12, + 'leftPadding':6, + 'rightPadding':6, + 'topPadding':3, + 'bottomPadding':3, + 'firstLineIndent':0, + 'color':colors.black, + 'alignment': 'LEFT', + 'background': (1,1,1), + 'valign': 'BOTTOM', + 'href': None, + 'destination':None, + } + +LINECAPS={None: None, 'butt':0,'round':1,'projecting':2,'squared':2} +LINEJOINS={None: None, 'miter':0, 'mitre':0, 'round':1,'bevel':2} + +# experimental replacement +class CellStyle1(PropertySet): + fontname = "Times-Roman" + fontsize = 10 + leading = 12 + leftPadding = 6 + rightPadding = 6 + topPadding = 3 + bottomPadding = 3 + firstLineIndent = 0 + color = colors.black + alignment = 'LEFT' + background = (1,1,1) + valign = "BOTTOM" + href = None + destination = None + def __init__(self, name, parent=None): + self.name = name + if parent is not None: + parent.copy(self) + def copy(self, result=None): + if result is None: + result = CellStyle1() + for name in dir(self): + setattr(result, name, gettattr(self, name)) + return result +CellStyle = CellStyle1 + +class TableStyle: + def __init__(self, cmds=None, parent=None, **kw): + #handle inheritance from parent first. + commands = [] + if parent: + # copy the parents list at construction time + commands = commands + parent.getCommands() + self._opts = parent._opts + for a in ('spaceBefore','spaceAfter'): + if hasattr(parent,a): + setattr(self,a,getattr(parent,a)) + if cmds: + commands = commands + list(cmds) + self._cmds = commands + self._opts={} + self._opts.update(kw) + + def add(self, *cmd): + self._cmds.append(cmd) + def __repr__(self): + L = map(repr, self._cmds) + import string + L = string.join(L, " \n") + return "TableStyle(\n%s\n) # end TableStyle" % L + def getCommands(self): + return self._cmds + +TableStyleType = type(TableStyle()) +_SeqTypes = (TupleType, ListType) + +def _rowLen(x): + return type(x) not in _SeqTypes and 1 or len(x) + +def _calc_pc(V,avail): + '''check list V for percentage or * values + 1) absolute values go through unchanged + 2) percentages are used as weights for unconsumed space + 3) if no None values were seen '*' weights are + set equally with unclaimed space + otherwise * weights are assigned as None''' + R = [] + r = R.append + I = [] + i = I.append + J = [] + j = J.append + s = avail + w = n = 0. + for v in V: + if type(v) is type(""): + v = v.strip() + if not v: + v = None + n += 1 + elif v.endswith('%'): + v = float(v[:-1]) + w += v + i(len(R)) + elif v=='*': + j(len(R)) + else: + v = float(v) + s -= v + elif v is None: + n += 1 + else: + s -= v + r(v) + s = max(0.,s) + f = s/max(100.,w) + for i in I: + R[i] *= f + s -= R[i] + s = max(0.,s) + m = len(J) + if m: + v = n==0 and s/m or None + for j in J: + R[j] = v + return R + +def _hLine(canvLine, scp, ecp, y, hBlocks, FUZZ=rl_config._FUZZ): + ''' + Draw horizontal lines; do not draw through regions specified in hBlocks + This also serves for vertical lines with a suitable canvLine + ''' + if hBlocks: hBlocks = hBlocks.get(y,None) + if not hBlocks or scp>=hBlocks[-1][1]-FUZZ or ecp<=hBlocks[0][0]+FUZZ: + canvLine(scp,y,ecp,y) + else: + i = 0 + n = len(hBlocks) + while scp=ecp-FUZZ: + i += 1 + continue + i0 = max(scp,x0) + i1 = min(ecp,x1) + if i0>scp: canvLine(scp,y,i0,y) + scp = i1 + if scp%s" % (self.__class__.__name__, id(self), nr, nc, vx) + + def _listCellGeom(self, V,w,s,W=None,H=None,aH=72000): + if not V: return 0,0 + aW = w-s.leftPadding-s.rightPadding + aH = aH - s.topPadding - s.bottomPadding + t = 0 + w = 0 + canv = getattr(self,'canv',None) + for v in V: + vw, vh = v.wrapOn(canv,aW, aH) + if W is not None: W.append(vw) + if H is not None: H.append(vh) + w = max(w,vw) + t = t + vh + v.getSpaceBefore()+v.getSpaceAfter() + return w, t - V[0].getSpaceBefore()-V[-1].getSpaceAfter() + + def _calc_width(self,availWidth,W=None): + if getattr(self,'_width_calculated_once',None): return + #comments added by Andy to Robin's slightly terse variable names + if not W: W = _calc_pc(self._argW,availWidth) #widths array + if None in W: #some column widths are not given + canv = getattr(self,'canv',None) + saved = None + colSpanCells = self._spanCmds and self._colSpanCells or () + if W is self._argW: W = W[:] + while None in W: + j = W.index(None) #find first unspecified column + f = lambda x,j=j: operator.getitem(x,j) + V = map(f,self._cellvalues) #values for this column + S = map(f,self._cellStyles) #styles for this column + w = 0 + i = 0 + + for v, s in map(None, V, S): + #if the current cell is part of a spanned region, + #assume a zero size. + if (j, i) in colSpanCells: + t = 0.0 + else:#work out size + t = self._elementWidth(v,s) + if t is None: + raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width\n%s" % (v.identity(30),i,j,self.identity(30)) + t = t + s.leftPadding+s.rightPadding + if t>w: w = t #record a new maximum + i = i + 1 + + W[j] = w + + self._colWidths = W + width = 0 + self._colpositions = [0] #index -1 is right side boundary; we skip when processing cells + for w in W: + width = width + w + self._colpositions.append(width) + + self._width = width + self._width_calculated_once = 1 + + def _elementWidth(self,v,s): + t = type(v) + if t in _SeqTypes: + w = 0 + for e in v: + ew = self._elementWidth(e,s) + if ew is None: return None + w = max(w,ew) + return w + elif isinstance(v,Flowable) and v._fixedWidth: + if hasattr(v, 'width') and type(v.width) in (IntType,FloatType): return v.width + if hasattr(v, 'drawWidth') and type(v.drawWidth) in (IntType,FloatType): return v.drawWidth + # Even if something is fixedWidth, the attribute to check is not + # necessarily consistent (cf. Image.drawWidth). Therefore, we'll + # be extra-careful and fall through to this code if necessary. + if hasattr(v, 'minWidth'): + try: + w = v.minWidth() # should be all flowables + if type(w) in (FloatType,IntType): return w + except AttributeError: + pass + v = string.split(v is not None and str(v) or '', "\n") + return max(map(lambda a, b=s.fontname, c=s.fontsize,d=pdfmetrics.stringWidth: d(a,b,c), v)) + + def _calc_height(self, availHeight, availWidth, H=None, W=None): + + H = self._argH + if not W: W = _calc_pc(self._argW,availWidth) #widths array + + hmax = lim = len(H) + longTable = getattr(self,'_longTableOptimize',rl_config.longTableOptimize) + + if None in H: + canv = getattr(self,'canv',None) + saved = None + #get a handy list of any cells which span rows. should be ignored for sizing + if self._spanCmds: + rowSpanCells = self._rowSpanCells + colSpanCells = self._colSpanCells + spanRanges = self._spanRanges + colpositions = self._colpositions + else: + rowSpanCells = colSpanCells = () + if canv: saved = canv._fontname, canv._fontsize, canv._leading + H = H[:] #make a copy as we'll change it + self._rowHeights = H + while None in H: + i = H.index(None) + if longTable: + hmax = i + height = reduce(operator.add, H[:i], 0) + # we can stop if we have filled up all available room + if height > availHeight: break + V = self._cellvalues[i] # values for row i + S = self._cellStyles[i] # styles for row i + h = 0 + j = 0 + for v, s, w in map(None, V, S, W): # value, style, width (lengths must match) + ji = j,i + if ji in rowSpanCells: + t = 0.0 # don't count it, it's either occluded or unreliable + else: + t = type(v) + if t in _SeqTypes or isinstance(v,Flowable): + if not t in _SeqTypes: v = (v,) + if w is None: + raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width in\n%s" % (v[0].identity(30),i,j,self.identity(30)) + if canv: canv._fontname, canv._fontsize, canv._leading = s.fontname, s.fontsize, s.leading or 1.2*s.fontsize + if ji in colSpanCells: + t = spanRanges[ji] + w = max(colpositions[t[2]+1]-colpositions[t[0]],w) + dW,t = self._listCellGeom(v,w,s) + if canv: canv._fontname, canv._fontsize, canv._leading = saved + dW = dW + s.leftPadding + s.rightPadding + if not rl_config.allowTableBoundsErrors and dW>w: + raise "LayoutError", "Flowable %s (%sx%s points) too wide for cell(%d,%d) (%sx* points) in\n%s" % (v[0].identity(30),fp_str(dW),fp_str(t),i,j, fp_str(w), self.identity(30)) + else: + v = string.split(v is not None and str(v) or '', "\n") + t = s.leading*len(v) + t = t+s.bottomPadding+s.topPadding + if t>h: h = t #record a new maximum + j = j + 1 + H[i] = h + if None not in H: hmax = lim + + height = self._height = reduce(operator.add, H[:hmax], 0) + self._rowpositions = [height] # index 0 is actually topline; we skip when processing cells + for h in H[:hmax]: + height = height - h + self._rowpositions.append(height) + assert abs(height)<1e-8, 'Internal height error' + self._hmax = hmax + + def _calc(self, availWidth, availHeight): + #if hasattr(self,'_width'): return + + #in some cases there are unsizable things in + #cells. If so, apply a different algorithm + #and assign some withs in a less (thanks to Gary Poster) dumb way. + #this CHANGES the widths array. + if (None in self._colWidths or '*' in self._colWidths) and self._hasVariWidthElements(): + W = self._calcPreliminaryWidths(availWidth) #widths + else: + W = None + + # need to know which cells are part of spanned + # ranges, so _calc_height and _calc_width can ignore them + # in sizing + if self._spanCmds: + self._calcSpanRanges() + if None in self._argH: + self._calc_width(availWidth,W=W) + + # calculate the full table height + self._calc_height(availHeight,availWidth,W=W) + + # calculate the full table width + self._calc_width(availWidth,W=W) + + if self._spanCmds: + #now work out the actual rect for each spanned cell from the underlying grid + self._calcSpanRects() + + def _hasVariWidthElements(self, upToRow=None): + """Check for flowables in table cells and warn up front. + + Allow a couple which we know are fixed size such as + images and graphics.""" + bad = 0 + if upToRow is None: upToRow = self._nrows + for row in range(min(self._nrows, upToRow)): + for col in range(self._ncols): + value = self._cellvalues[row][col] + if not self._canGetWidth(value): + bad = 1 + #raise Exception('Unsizable elements found at row %d column %d in table with content:\n %s' % (row, col, value)) + return bad + + def _canGetWidth(self, thing): + "Can we work out the width quickly?" + if type(thing) in (ListType, TupleType): + for elem in thing: + if not self._canGetWidth(elem): + return 0 + return 1 + elif isinstance(thing, Flowable): + return thing._fixedWidth # must loosen this up + else: #string, number, None etc. + #anything else gets passed to str(...) + # so should be sizable + return 1 + + def _calcPreliminaryWidths(self, availWidth): + """Fallback algorithm for when main one fails. + + Where exact width info not given but things like + paragraphs might be present, do a preliminary scan + and assign some best-guess values.""" + + W = list(self._argW) # _calc_pc(self._argW,availWidth) + verbose = 0 + totalDefined = 0.0 + percentDefined = 0 + percentTotal = 0 + numberUndefined = 0 + numberGreedyUndefined = 0 + for w in W: + if w is None: + numberUndefined += 1 + elif w == '*': + numberUndefined += 1 + numberGreedyUndefined += 1 + elif _endswith(w,'%'): + percentDefined += 1 + percentTotal += float(w[:-1]) + else: + assert type(w) in (IntType, FloatType) + totalDefined = totalDefined + w + if verbose: print 'prelim width calculation. %d columns, %d undefined width, %0.2f units remain' % ( + self._ncols, numberUndefined, availWidth - totalDefined) + + #check columnwise in each None column to see if they are sizable. + given = [] + sizeable = [] + unsizeable = [] + minimums = {} + totalMinimum = 0 + elementWidth = self._elementWidth + for colNo in range(self._ncols): + w = W[colNo] + if w is None or w=='*' or _endswith(w,'%'): + siz = 1 + current = final = None + for rowNo in range(self._nrows): + value = self._cellvalues[rowNo][colNo] + style = self._cellStyles[rowNo][colNo] + new = elementWidth(value,style)+style.leftPadding+style.rightPadding + final = max(current, new) + current = new + siz = siz and self._canGetWidth(value) # irrelevant now? + if siz: + sizeable.append(colNo) + else: + unsizeable.append(colNo) + minimums[colNo] = final + totalMinimum += final + else: + given.append(colNo) + if len(given) == self._ncols: + return + if verbose: print 'predefined width: ',given + if verbose: print 'uncomputable width: ',unsizeable + if verbose: print 'computable width: ',sizeable + + # how much width is left: + remaining = availWidth - (totalMinimum + totalDefined) + if remaining > 0: + # we have some room left; fill it. + definedPercentage = (totalDefined/availWidth)*100 + percentTotal += definedPercentage + if numberUndefined and percentTotal < 100: + undefined = numberGreedyUndefined or numberUndefined + defaultWeight = (100-percentTotal)/undefined + percentTotal = 100 + defaultDesired = (defaultWeight/percentTotal)*availWidth + else: + defaultWeight = defaultDesired = 1 + # we now calculate how wide each column wanted to be, and then + # proportionately shrink that down to fit the remaining available + # space. A column may not shrink less than its minimum width, + # however, which makes this a bit more complicated. + desiredWidths = [] + totalDesired = 0 + effectiveRemaining = remaining + for colNo, minimum in minimums.items(): + w = W[colNo] + if _endswith(w,'%'): + desired = (float(w[:-1])/percentTotal)*availWidth + elif w == '*': + desired = defaultDesired + else: + desired = not numberGreedyUndefined and defaultDesired or 1 + if desired <= minimum: + W[colNo] = minimum + else: + desiredWidths.append( + (desired-minimum, minimum, desired, colNo)) + totalDesired += desired + effectiveRemaining += minimum + if desiredWidths: # else we're done + # let's say we have two variable columns. One wanted + # 88 points, and one wanted 264 points. The first has a + # minWidth of 66, and the second of 55. We have 71 points + # to divide up in addition to the totalMinimum (i.e., + # remaining==71). Our algorithm tries to keep the proportion + # of these variable columns. + # + # To do this, we add up the minimum widths of the variable + # columns and the remaining width. That's 192. We add up the + # totalDesired width. That's 352. That means we'll try to + # shrink the widths by a proportion of 192/352--.545454. + # That would make the first column 48 points, and the second + # 144 points--adding up to the desired 192. + # + # Unfortunately, that's too small for the first column. It + # must be 66 points. Therefore, we go ahead and save that + # column width as 88 points. That leaves (192-88==) 104 + # points remaining. The proportion to shrink the remaining + # column is (104/264), which, multiplied by the desired + # width of 264, is 104: the amount assigned to the remaining + # column. + proportion = effectiveRemaining/totalDesired + # we sort the desired widths by difference between desired and + # and minimum values, a value called "disappointment" in the + # code. This means that the columns with a bigger + # disappointment will have a better chance of getting more of + # the available space. + desiredWidths.sort() + finalSet = [] + for disappointment, minimum, desired, colNo in desiredWidths: + adjusted = proportion * desired + if adjusted < minimum: + W[colNo] = minimum + totalDesired -= desired + effectiveRemaining -= minimum + if totalDesired: + proportion = effectiveRemaining/totalDesired + else: + finalSet.append((minimum, desired, colNo)) + for minimum, desired, colNo in finalSet: + adjusted = proportion * desired + assert adjusted >= minimum + W[colNo] = adjusted + else: + for colNo, minimum in minimums.items(): + W[colNo] = minimum + if verbose: print 'new widths are:', W + self._argW = self._colWidths = W + return W + + def minWidth(self): + W = list(self._argW) + width = 0 + elementWidth = self._elementWidth + rowNos = xrange(self._nrows) + values = self._cellvalues + styles = self._cellStyles + for colNo in xrange(len(W)): + w = W[colNo] + if w is None or w=='*' or _endswith(w,'%'): + final = 0 + for rowNo in rowNos: + value = values[rowNo][colNo] + style = styles[rowNo][colNo] + new = (elementWidth(value,style)+ + style.leftPadding+style.rightPadding) + final = max(final, new) + width += final + else: + width += float(w) + return width # XXX + 1/2*(left and right border widths) + + def _calcSpanRanges(self): + """Work out rects for tables which do row and column spanning. + + This creates some mappings to let the later code determine + if a cell is part of a "spanned" range. + self._spanRanges shows the 'coords' in integers of each + 'cell range', or None if it was clobbered: + (col, row) -> (col0, row0, col1, row1) + + Any cell not in the key is not part of a spanned region + """ + self._spanRanges = spanRanges = {} + for x in xrange(self._ncols): + for y in xrange(self._nrows): + spanRanges[x,y] = (x, y, x, y) + self._colSpanCells = [] + self._rowSpanCells = [] + csa = self._colSpanCells.append + rsa = self._rowSpanCells.append + for (cmd, start, stop) in self._spanCmds: + x0, y0 = start + x1, y1 = stop + + #normalize + if x0 < 0: x0 = x0 + self._ncols + if x1 < 0: x1 = x1 + self._ncols + if y0 < 0: y0 = y0 + self._nrows + if y1 < 0: y1 = y1 + self._nrows + if x0 > x1: x0, x1 = x1, x0 + if y0 > y1: y0, y1 = y1, y0 + + if x0!=x1 or y0!=y1: + #column span + if x0!=x1: + for y in xrange(y0, y1+1): + for x in xrange(x0,x1+1): + csa((x,y)) + #row span + if y0!=y1: + for y in xrange(y0, y1+1): + for x in xrange(x0,x1+1): + rsa((x,y)) + + for y in xrange(y0, y1+1): + for x in xrange(x0,x1+1): + spanRanges[x,y] = None + # set the main entry + spanRanges[x0,y0] = (x0, y0, x1, y1) + + def _calcSpanRects(self): + """Work out rects for tables which do row and column spanning. + + Based on self._spanRanges, which is already known, + and the widths which were given or previously calculated, + self._spanRects shows the real coords for drawing: + (col, row) -> (x, y, width, height) + + for each cell. Any cell which 'does not exist' as another + has spanned over it will get a None entry on the right + """ + if getattr(self,'_spanRects',None): return + colpositions = self._colpositions + rowpositions = self._rowpositions + self._spanRects = spanRects = {} + self._vBlocks = vBlocks = {} + self._hBlocks = hBlocks = {} + for (coord, value) in self._spanRanges.items(): + if value is None: + spanRects[coord] = None + else: + col,row = coord + col0, row0, col1, row1 = value + if col1-col0>0: + for _ in xrange(col0+1,col1+1): + vBlocks.setdefault(colpositions[_],[]).append((rowpositions[row1+1],rowpositions[row0])) + if row1-row0>0: + for _ in xrange(row0+1,row1+1): + hBlocks.setdefault(rowpositions[_],[]).append((colpositions[col0],colpositions[col1+1])) + x = colpositions[col0] + y = rowpositions[row1+1] + width = colpositions[col1+1] - x + height = rowpositions[row0] - y + spanRects[coord] = (x, y, width, height) + + for _ in hBlocks, vBlocks: + for value in _.values(): + value.sort() + + def setStyle(self, tblstyle): + if type(tblstyle) is not TableStyleType: + tblstyle = TableStyle(tblstyle) + for cmd in tblstyle.getCommands(): + self._addCommand(cmd) + for k,v in tblstyle._opts.items(): + setattr(self,k,v) + for a in ('spaceBefore','spaceAfter'): + if not hasattr(self,a) and hasattr(tblstyle,a): + setattr(self,a,getattr(tblstyle,a)) + + def _addCommand(self,cmd): + if cmd[0] in ('BACKGROUND','ROWBACKGROUNDS','COLBACKGROUNDS'): + self._bkgrndcmds.append(cmd) + elif cmd[0] == 'SPAN': + self._spanCmds.append(cmd) + elif _isLineCommand(cmd): + # we expect op, start, stop, weight, colour, cap, dashes, join + cmd = list(cmd) + if len(cmd)<5: raise ValueError('bad line command '+str(cmd)) + + #determine line cap value at position 5. This can be string or numeric. + if len(cmd)<6: + cmd.append(1) + else: + cap = _convert2int(cmd[5], LINECAPS, 0, 2, 'cap', cmd) + cmd[5] = cap + + #dashes at index 6 - this is a dash array: + if len(cmd)<7: cmd.append(None) + + #join mode at index 7 - can be string or numeric, look up as for caps + if len(cmd)<8: cmd.append(1) + else: + join = _convert2int(cmd[7], LINEJOINS, 0, 2, 'join', cmd) + cmd[7] = join + + #linecount at index 8. Default is 1, set to 2 for double line. + if len(cmd)<9: cmd.append(1) + else: + lineCount = cmd[8] + if lineCount is None: + lineCount = 1 + cmd[8] = lineCount + assert lineCount >= 1 + #linespacing at index 9. Not applicable unless 2+ lines, defaults to line + #width so you get a visible gap between centres + if len(cmd)<10: cmd.append(cmd[3]) + else: + space = cmd[9] + if space is None: + space = cmd[3] + cmd[9] = space + assert len(cmd) == 10 + + self._linecmds.append(tuple(cmd)) + else: + (op, (sc, sr), (ec, er)), values = cmd[:3] , cmd[3:] + if sc < 0: sc = sc + self._ncols + if ec < 0: ec = ec + self._ncols + if sr < 0: sr = sr + self._nrows + if er < 0: er = er + self._nrows + for i in range(sr, er+1): + for j in range(sc, ec+1): + _setCellStyle(self._cellStyles, i, j, op, values) + + def _drawLines(self): + ccap, cdash, cjoin = None, None, None + self.canv.saveState() + for op, (sc,sr), (ec,er), weight, color, cap, dash, join, count, space in self._linecmds: + if type(sr) is type('') and sr.startswith('split'): continue + if sc < 0: sc = sc + self._ncols + if ec < 0: ec = ec + self._ncols + if sr < 0: sr = sr + self._nrows + if er < 0: er = er + self._nrows + if cap!=None and ccap!=cap: + self.canv.setLineCap(cap) + ccap = cap + if dash is None or dash == []: + if cdash is not None: + self.canv.setDash() + cdash = None + elif dash != cdash: + self.canv.setDash(dash) + cdash = dash + if join is not None and cjoin!=join: + self.canv.setLineJoin(join) + cjoin = join + getattr(self,_LineOpMap.get(op, '_drawUnknown' ))( (sc, sr), (ec, er), weight, color, count, space) + self.canv.restoreState() + self._curcolor = None + + def _drawUnknown(self, (sc, sr), (ec, er), weight, color, count, space): + raise ValueError, "Unknown line command '%s'" % op + + def _drawGrid(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawBox( (sc, sr), (ec, er), weight, color, count, space) + self._drawInnerGrid( (sc, sr), (ec, er), weight, color, count, space) + + def _drawBox(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawHLines((sc, sr), (ec, sr), weight, color, count, space) + self._drawHLines((sc, er+1), (ec, er+1), weight, color, count, space) + self._drawVLines((sc, sr), (sc, er), weight, color, count, space) + self._drawVLines((ec+1, sr), (ec+1, er), weight, color, count, space) + + def _drawInnerGrid(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawHLines((sc, sr+1), (ec, er), weight, color, count, space) + self._drawVLines((sc+1, sr), (ec, er), weight, color, count, space) + + def _prepLine(self, weight, color): + if color != self._curcolor: + self.canv.setStrokeColor(color) + self._curcolor = color + if weight != self._curweight: + self.canv.setLineWidth(weight) + self._curweight = weight + + def _drawHLines(self, (sc, sr), (ec, er), weight, color, count, space): + ecp = self._colpositions[sc:ec+2] + rp = self._rowpositions[sr:er+1] + if len(ecp)<=1 or len(rp)<1: return + self._prepLine(weight, color) + scp = ecp[0] + ecp = ecp[-1] + hBlocks = getattr(self,'_hBlocks',{}) + canvLine = self.canv.line + if count == 1: + for y in rp: + _hLine(canvLine, scp, ecp, y, hBlocks) + else: + lf = lambda x0,y0,x1,y1,canvLine=canvLine, ws=weight+space, count=count: _multiLine(x0,x1,y0,canvLine,ws,count) + for y in rp: + _hLine(lf, scp, ecp, y, hBlocks) + + def _drawHLinesB(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawHLines((sc, sr+1), (ec, er+1), weight, color, count, space) + + def _drawVLines(self, (sc, sr), (ec, er), weight, color, count, space): + erp = self._rowpositions[sr:er+2] + cp = self._colpositions[sc:ec+1] + if len(erp)<=1 or len(cp)<1: return + self._prepLine(weight, color) + srp = erp[0] + erp = erp[-1] + vBlocks = getattr(self,'_vBlocks',{}) + canvLine = lambda y0, x0, y1, x1, _line=self.canv.line: _line(x0,y0,x1,y1) + if count == 1: + for x in cp: + _hLine(canvLine, erp, srp, x, vBlocks) + else: + lf = lambda x0,y0,x1,y1,canvLine=canvLine, ws=weight+space, count=count: _multiLine(x0,x1,y0,canvLine,ws,count) + for x in cp: + _hLine(lf, erp, srp, x, vBlocks) + + def _drawVLinesA(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawVLines((sc+1, sr), (ec+1, er), weight, color, count, space) + + def wrap(self, availWidth, availHeight): + self._calc(availWidth, availHeight) + #nice and easy, since they are predetermined size + self.availWidth = availWidth + return (self._width, self._height) + + def onSplit(self,T,byRow=1): + ''' + This method will be called when the Table is split. + Special purpose tables can override to do special stuff. + ''' + pass + + def _cr_0(self,n,cmds): + for c in cmds: + c = tuple(c) + (sc,sr), (ec,er) = c[1:3] + if sr>=n: continue + if er>=n: er = n-1 + self._addCommand((c[0],)+((sc, sr), (ec, er))+c[3:]) + + def _cr_1_1(self,n,repeatRows, cmds): + for c in cmds: + c = tuple(c) + (sc,sr), (ec,er) = c[1:3] + if sr in ('splitfirst','splitlast'): self._addCommand(c) + else: + if sr>=0 and sr>=repeatRows and sr=0 and er=repeatRows and sr=repeatRows and sr>=n: sr=sr+repeatRows-n + if er>=repeatRows and er=repeatRows and er>=n: er=er+repeatRows-n + self._addCommand((c[0],)+((sc, sr), (ec, er))+c[3:]) + + def _cr_1_0(self,n,cmds): + for c in cmds: + c = tuple(c) + (sc,sr), (ec,er) = c[1:3] + if sr in ('splitfirst','splitlast'): self._addCommand(c) + else: + if er>=0 and er=0 and sr=n: sr = sr-n + if er>=n: er = er-n + self._addCommand((c[0],)+((sc, sr), (ec, er))+c[3:]) + + def _splitRows(self,availHeight): + n=self._getFirstPossibleSplitRowPosition(availHeight) + if n<=self.repeatRows: return [] + lim = len(self._rowHeights) + if n==lim: return [self] + + repeatRows = self.repeatRows + repeatCols = self.repeatCols + splitByRow = self.splitByRow + data = self._cellvalues + + #we're going to split into two superRows + #R0 = slelf.__class__( data[:n], self._argW, self._argH[:n], + R0 = self.__class__( data[:n], self._colWidths, self._argH[:n], + repeatRows=repeatRows, repeatCols=repeatCols, + splitByRow=splitByRow) + + #copy the styles and commands + R0._cellStyles = self._cellStyles[:n] + + A = [] + # hack up the line commands + for op, (sc,sr), (ec,er), weight, color, cap, dash, join, count, space in self._linecmds: + if type(sr)is type('') and sr.startswith('split'): + A.append((op,(sc,sr), (ec,sr), weight, color, cap, dash, join, count, space)) + if sr=='splitlast': + sr = er = n-1 + elif sr=='splitfirst': + sr = n + er = n + + if sc < 0: sc = sc + self._ncols + if ec < 0: ec = ec + self._ncols + if sr < 0: sr = sr + self._nrows + if er < 0: er = er + self._nrows + + if op in ('BOX','OUTLINE','GRID'): + if sr=n: + # we have to split the BOX + A.append(('LINEABOVE',(sc,sr), (ec,sr), weight, color, cap, dash, join, count, space)) + A.append(('LINEBEFORE',(sc,sr), (sc,er), weight, color, cap, dash, join, count, space)) + A.append(('LINEAFTER',(ec,sr), (ec,er), weight, color, cap, dash, join, count, space)) + A.append(('LINEBELOW',(sc,er), (ec,er), weight, color, cap, dash, join, count, space)) + if op=='GRID': + A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) + A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) + A.append(('INNERGRID',(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + else: + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + elif op in ('INNERGRID','LINEABOVE'): + if sr=n: + A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) + A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + elif op == 'LINEBELOW': + if sr=(n-1): + A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) + A.append((op,(sc,sr), (ec,er), weight, color)) + elif op == 'LINEABOVE': + if sr<=n and er>=n: + A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + else: + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + + R0._cr_0(n,A) + R0._cr_0(n,self._bkgrndcmds) + R0._cr_0(n,self._spanCmds) + + if repeatRows: + #R1 = slelf.__class__(data[:repeatRows]+data[n:],self._argW, + R1 = self.__class__(data[:repeatRows]+data[n:],self._colWidths, + self._argH[:repeatRows]+self._argH[n:], + repeatRows=repeatRows, repeatCols=repeatCols, + splitByRow=splitByRow) + R1._cellStyles = self._cellStyles[:repeatRows]+self._cellStyles[n:] + R1._cr_1_1(n,repeatRows,A) + R1._cr_1_1(n,repeatRows,self._bkgrndcmds) + R1._cr_1_1(n,repeatRows,self._spanCmds) + else: + #R1 = slelf.__class__(data[n:], self._argW, self._argH[n:], + R1 = self.__class__(data[n:], self._colWidths, self._argH[n:], + repeatRows=repeatRows, repeatCols=repeatCols, + splitByRow=splitByRow) + R1._cellStyles = self._cellStyles[n:] + R1._cr_1_0(n,A) + R1._cr_1_0(n,self._bkgrndcmds) + R1._cr_1_0(n,self._spanCmds) + + + R0.hAlign = R1.hAlign = self.hAlign + R0.vAlign = R1.vAlign = self.vAlign + self.onSplit(R0) + self.onSplit(R1) + return [R0,R1] + + def _getFirstPossibleSplitRowPosition(self,availHeight): + if self._spanCmds: + impossible={} + for xy in self._rowSpanCells: + r=self._spanRanges[xy] + if r!=None: + y1,y2=r[1],r[3] + if y1!=y2: + ymin=min(y1,y2) #normalize + ymax=max(y1,y2) #normalize + y=ymin+1 + while 1: + if y>ymax: break + impossible[y]=None #split at position y is impossible because of overlapping rowspan + y=y+1 + else: + impossible={} # any split possible because table does *not* have rowspans + h = 0 + n = 1 + split_at = 0 # from this point of view 0 is the first position where the table may *always* be splitted + for rh in self._rowHeights: + if h+rh>availHeight: + break + if not impossible.has_key(n): + split_at=n + h=h+rh + n=n+1 + return split_at + + def split(self, availWidth, availHeight): + self._calc(availWidth, availHeight) + if self.splitByRow: + if not rl_config.allowTableBoundsErrors and self._width>availWidth: return [] + return self._splitRows(availHeight) + else: + raise NotImplementedError + + def draw(self): + self._curweight = self._curcolor = self._curcellstyle = None + self._drawBkgrnd() + if self._spanCmds == []: + # old fashioned case, no spanning, steam on and do each cell + for row, rowstyle, rowpos, rowheight in map(None, self._cellvalues, self._cellStyles, self._rowpositions[1:], self._rowHeights): + for cellval, cellstyle, colpos, colwidth in map(None, row, rowstyle, self._colpositions[:-1], self._colWidths): + self._drawCell(cellval, cellstyle, (colpos, rowpos), (colwidth, rowheight)) + else: + # we have some row or col spans, need a more complex algorithm + # to find the rect for each + for rowNo in range(self._nrows): + for colNo in range(self._ncols): + cellRect = self._spanRects[colNo, rowNo] + if cellRect is not None: + (x, y, width, height) = cellRect + cellval = self._cellvalues[rowNo][colNo] + cellstyle = self._cellStyles[rowNo][colNo] + self._drawCell(cellval, cellstyle, (x, y), (width, height)) + self._drawLines() + + + def _drawBkgrnd(self): + nrows = self._nrows + ncols = self._ncols + for cmd, (sc, sr), (ec, er), arg in self._bkgrndcmds: + if sc < 0: sc = sc + ncols + if ec < 0: ec = ec + ncols + if sr < 0: sr = sr + nrows + if er < 0: er = er + nrows + x0 = self._colpositions[sc] + y0 = self._rowpositions[sr] + x1 = self._colpositions[min(ec+1,ncols)] + y1 = self._rowpositions[min(er+1,nrows)] + w, h = x1-x0, y1-y0 + canv = self.canv + if callable(arg): + apply(arg,(self,canv, x0, y0, w, h)) + elif cmd == 'ROWBACKGROUNDS': + #Need a list of colors to cycle through. The arguments + #might be already colours, or convertible to colors, or + # None, or the string 'None'. + #It's very common to alternate a pale shade with None. + colorCycle = map(colors.toColorOrNone, arg) + count = len(colorCycle) + rowCount = er - sr + 1 + for i in range(rowCount): + color = colorCycle[i%count] + h = self._rowHeights[sr + i] + if color: + canv.setFillColor(color) + canv.rect(x0, y0, w, -h, stroke=0,fill=1) + y0 = y0 - h + + elif cmd == 'COLBACKGROUNDS': + #cycle through colours columnwise + colorCycle = map(colors.toColorOrNone, arg) + count = len(colorCycle) + colCount = ec - sc + 1 + for i in range(colCount): + color = colorCycle[i%count] + w = self._colWidths[sc + i] + if color: + canv.setFillColor(color) + canv.rect(x0, y0, w, h, stroke=0,fill=1) + x0 = x0 +w + else: #cmd=='BACKGROUND' + canv.setFillColor(colors.toColor(arg)) + canv.rect(x0, y0, w, h, stroke=0,fill=1) + + def _drawCell(self, cellval, cellstyle, (colpos, rowpos), (colwidth, rowheight)): + if self._curcellstyle is not cellstyle: + cur = self._curcellstyle + if cur is None or cellstyle.color != cur.color: + self.canv.setFillColor(cellstyle.color) + if cur is None or cellstyle.leading != cur.leading or cellstyle.fontname != cur.fontname or cellstyle.fontsize != cur.fontsize: + self.canv.setFont(cellstyle.fontname, cellstyle.fontsize, cellstyle.leading) + self._curcellstyle = cellstyle + + just = cellstyle.alignment + valign = cellstyle.valign + n = type(cellval) + if n in _SeqTypes or isinstance(cellval,Flowable): + if not n in _SeqTypes: cellval = (cellval,) + # we assume it's a list of Flowables + W = [] + H = [] + w, h = self._listCellGeom(cellval,colwidth,cellstyle,W=W, H=H,aH=rowheight) + if valign=='TOP': + y = rowpos + rowheight - cellstyle.topPadding + elif valign=='BOTTOM': + y = rowpos+cellstyle.bottomPadding + h + else: + y = rowpos+(rowheight+cellstyle.bottomPadding-cellstyle.topPadding+h)/2.0 + if cellval: y += cellval[0].getSpaceBefore() + for v, w, h in map(None,cellval,W,H): + if just=='LEFT': x = colpos+cellstyle.leftPadding + elif just=='RIGHT': x = colpos+colwidth-cellstyle.rightPadding - w + elif just in ('CENTRE', 'CENTER'): + x = colpos+(colwidth+cellstyle.leftPadding-cellstyle.rightPadding-w)/2.0 + else: + raise ValueError, 'Invalid justification %s' % just + y -= v.getSpaceBefore() + y -= h + v.drawOn(self.canv,x,y) + y -= v.getSpaceAfter() + else: + if just == 'LEFT': + draw = self.canv.drawString + x = colpos + cellstyle.leftPadding + elif just in ('CENTRE', 'CENTER'): + draw = self.canv.drawCentredString + x = colpos + colwidth * 0.5 + elif just == 'RIGHT': + draw = self.canv.drawRightString + x = colpos + colwidth - cellstyle.rightPadding + elif just == 'DECIMAL': + draw = self.canv.drawAlignedString + x = colpos + colwidth - cellstyle.rightPadding + else: + raise ValueError, 'Invalid justification %s' % just + vals = string.split(str(cellval), "\n") + n = len(vals) + leading = cellstyle.leading + fontsize = cellstyle.fontsize + if valign=='BOTTOM': + y = rowpos + cellstyle.bottomPadding+n*leading-fontsize + elif valign=='TOP': + y = rowpos + rowheight - cellstyle.topPadding - fontsize + elif valign=='MIDDLE': + #tim roberts pointed out missing fontsize correction 2004-10-04 + y = rowpos + (cellstyle.bottomPadding + rowheight-cellstyle.topPadding+n*leading)/2.0 - fontsize + else: + raise ValueError, "Bad valign: '%s'" % str(valign) + + for v in vals: + draw(x, y, v) + y -= leading + + if cellstyle.href: + #external hyperlink + self.canv.linkURL(cellstyle.href, (colpos, rowpos, colpos + colwidth, rowpos + rowheight), relative=1) + if cellstyle.destination: + #external hyperlink + self.canv.linkRect("", cellstyle.destination, Rect=(colpos, rowpos, colpos + colwidth, rowpos + rowheight), relative=1) + +_LineOpMap = { 'GRID':'_drawGrid', + 'BOX':'_drawBox', + 'OUTLINE':'_drawBox', + 'INNERGRID':'_drawInnerGrid', + 'LINEBELOW':'_drawHLinesB', + 'LINEABOVE':'_drawHLines', + 'LINEBEFORE':'_drawVLines', + 'LINEAFTER':'_drawVLinesA', } + +class LongTable(Table): + '''Henning von Bargen's changes will be active''' + _longTableOptimize = 1 + +LINECOMMANDS = _LineOpMap.keys() + +def _isLineCommand(cmd): + return cmd[0] in LINECOMMANDS + +def _setCellStyle(cellStyles, i, j, op, values): + #new = CellStyle('<%d, %d>' % (i,j), cellStyles[i][j]) + #cellStyles[i][j] = new + ## modify in place!!! + new = cellStyles[i][j] + if op == 'FONT': + n = len(values) + new.fontname = values[0] + if n>1: + new.fontsize = values[1] + if n>2: + new.leading = values[2] + else: + new.leading = new.fontsize*1.2 + elif op in ('FONTNAME', 'FACE'): + new.fontname = values[0] + elif op in ('SIZE', 'FONTSIZE'): + new.fontsize = values[0] + elif op == 'LEADING': + new.leading = values[0] + elif op == 'TEXTCOLOR': + new.color = colors.toColor(values[0], colors.Color(0,0,0)) + elif op in ('ALIGN', 'ALIGNMENT'): + new.alignment = values[0] + elif op == 'VALIGN': + new.valign = values[0] + elif op == 'LEFTPADDING': + new.leftPadding = values[0] + elif op == 'RIGHTPADDING': + new.rightPadding = values[0] + elif op == 'TOPPADDING': + new.topPadding = values[0] + elif op == 'BOTTOMPADDING': + new.bottomPadding = values[0] + elif op == 'HREF': + new.href = values[0] + elif op == 'DESTINATION': + new.destination = values[0] + +GRID_STYLE = TableStyle( + [('GRID', (0,0), (-1,-1), 0.25, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +BOX_STYLE = TableStyle( + [('BOX', (0,0), (-1,-1), 0.50, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +LABELED_GRID_STYLE = TableStyle( + [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 2, colors.black), + ('LINEBELOW', (0,0), (-1,0), 2, colors.black), + ('LINEAFTER', (0,0), (0,-1), 2, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +COLORED_GRID_STYLE = TableStyle( + [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 2, colors.red), + ('LINEBELOW', (0,0), (-1,0), 2, colors.black), + ('LINEAFTER', (0,0), (0,-1), 2, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +LIST_STYLE = TableStyle( + [('LINEABOVE', (0,0), (-1,0), 2, colors.green), + ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 2, colors.green), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + + +# experimental iterator which can apply a sequence +# of colors e.g. Blue, None, Blue, None as you move +# down. + + +if __name__ == '__main__': + from reportlab.test.test_platypus_tables import old_tables_test + old_tables_test() diff --git a/bin/reportlab/platypus/xpreformatted.py b/bin/reportlab/platypus/xpreformatted.py new file mode 100644 index 00000000000..73ebe37687c --- /dev/null +++ b/bin/reportlab/platypus/xpreformatted.py @@ -0,0 +1,316 @@ +#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/xpreformatted.py +__version__=''' $Id: xpreformatted.py 2426 2004-09-02 11:52:56Z rgbecker $ ''' + +import string +from types import StringType, ListType + +from reportlab.lib import PyFontify +from paragraph import Paragraph, cleanBlockQuotedText, _handleBulletWidth, \ + ParaLines, _getFragWords, stringWidth, _sameFrag +from flowables import _dedenter + + +def _getFragLines(frags): + lines = [] + cline = [] + W = frags[:] + while W != []: + w = W[0] + t = w.text + del W[0] + i = string.find(t,'\n') + if i>=0: + tleft = t[i+1:] + cline.append(w.clone(text=t[:i])) + lines.append(cline) + cline = [] + if tleft!='': + W.insert(0,w.clone(text=tleft)) + else: + cline.append(w) + if cline!=[]: + lines.append(cline) + return lines + +def _split_blPara(blPara,start,stop): + f = blPara.clone() + for a in ('lines', 'text'): + if hasattr(f,a): delattr(f,a) + f.lines = blPara.lines[start:stop] + return [f] + +# Will be removed shortly. +def _countSpaces(text): + return string.count(text, ' ') +## i = 0 +## s = 0 +## while 1: +## j = string.find(text,' ',i) +## if j<0: return s +## s = s + 1 +## i = j + 1 + +def _getFragWord(frags): + ''' given a fragment list return a list of lists + [size, spaces, (f00,w00), ..., (f0n,w0n)] + each pair f,w represents a style and some string + ''' + W = [] + n = 0 + s = 0 + for f in frags: + text = f.text[:] + W.append((f,text)) + n = n + stringWidth(text, f.fontName, f.fontSize) + + #s = s + _countSpaces(text) + s = s + string.count(text, ' ') # much faster for many blanks + + #del f.text # we can't do this until we sort out splitting + # of paragraphs + return n, s, W + + +class XPreformatted(Paragraph): + def __init__(self, text, style, bulletText = None, frags=None, caseSensitive=1, dedent=0): + self.caseSensitive = caseSensitive + cleaner = lambda text, dedent=dedent: string.join(_dedenter(text or '',dedent),'\n') + self._setup(text, style, bulletText, frags, cleaner) + + def breakLines(self, width): + """ + Returns a broken line structure. There are two cases + + A) For the simple case of a single formatting input fragment the output is + A fragment specifier with + kind = 0 + fontName, fontSize, leading, textColor + lines= A list of lines + Each line has two items. + 1) unused width in points + 2) a list of words + + B) When there is more than one input formatting fragment the out put is + A fragment specifier with + kind = 1 + lines= A list of fragments each having fields + extraspace (needed for justified) + fontSize + words=word list + each word is itself a fragment with + various settings + + This structure can be used to easily draw paragraphs with the various alignments. + You can supply either a single width or a list of widths; the latter will have its + last item repeated until necessary. A 2-element list is useful when there is a + different first line indent; a longer list could be created to facilitate custom wraps + around irregular objects.""" + + if type(width) <> ListType: maxWidths = [width] + else: maxWidths = width + lines = [] + lineno = 0 + maxWidth = maxWidths[lineno] + style = self.style + fFontSize = float(style.fontSize) + requiredWidth = 0 + + #for bullets, work out width and ensure we wrap the right amount onto line one + _handleBulletWidth(self.bulletText,style,maxWidths) + + self.height = 0 + frags = self.frags + nFrags= len(frags) + if nFrags==1: + f = frags[0] + if hasattr(f,'text'): + fontSize = f.fontSize + fontName = f.fontName + kind = 0 + L=string.split(f.text, '\n') + for l in L: + currentWidth = stringWidth(l,fontName,fontSize) + requiredWidth = max(currentWidth,requiredWidth) + extraSpace = maxWidth-currentWidth + lines.append((extraSpace,string.split(l,' '),currentWidth)) + lineno = lineno+1 + maxWidth = lineno', ''), + 'keyword' : ('', ''), + 'parameter' : ('', ''), + 'identifier' : ('', ''), + 'string' : ('', '') } + + def __init__(self, text, style, bulletText = None, dedent=0, frags=None): + if text: + text = self.fontify(self.escapeHtml(text)) + apply(XPreformatted.__init__, + (self, text, style), + {'bulletText':bulletText, 'dedent':dedent, 'frags':frags}) + + def escapeHtml(self, text): + s = string.replace(text, '&', '&') + s = string.replace(s, '<', '<') + s = string.replace(s, '>', '>') + return s + + def fontify(self, code): + "Return a fontified version of some Python code." + + if code[0] == '\n': + code = code[1:] + + tags = PyFontify.fontify(code) + fontifiedCode = '' + pos = 0 + for k, i, j, dummy in tags: + fontifiedCode = fontifiedCode + code[pos:i] + s, e = self.formats[k] + fontifiedCode = fontifiedCode + s + code[i:j] + e + pos = j + + fontifiedCode = fontifiedCode + code[pos:] + + return fontifiedCode + + +if __name__=='__main__': #NORUNTESTS + def dumpXPreformattedLines(P): + print '\n############dumpXPreforemattedLines(%s)' % str(P) + lines = P.blPara.lines + n =len(lines) + for l in range(n): + line = lines[l] + words = line.words + nwords = len(words) + print 'line%d: %d(%d)\n ' % (l,nwords,line.wordCount), + for w in range(nwords): + print "%d:'%s'"%(w,words[w].text), + print + + def dumpXPreformattedFrags(P): + print '\n############dumpXPreforemattedFrags(%s)' % str(P) + frags = P.frags + n =len(frags) + for l in range(n): + print "frag%d: '%s'" % (l, frags[l].text) + + l = 0 + for L in _getFragLines(frags): + n=0 + for W in _getFragWords(L): + print "frag%d.%d: size=%d" % (l, n, W[0]), + n = n + 1 + for w in W[1:]: + print "'%s'" % w[1], + print + l = l + 1 + + def try_it(text,style,dedent,aW,aH): + P=XPreformatted(text,style,dedent=dedent) + dumpXPreformattedFrags(P) + w,h = P.wrap(aW, aH) + dumpXPreformattedLines(P) + S = P.split(aW,aH) + dumpXPreformattedLines(P) + for s in S: + s.wrap(aW,aH) + dumpXPreformattedLines(s) + aH = 500 + + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + styleSheet = getSampleStyleSheet() + B = styleSheet['BodyText'] + DTstyle = ParagraphStyle("discussiontext", parent=B) + DTstyle.fontName= 'Helvetica' + for (text,dedent,style, aW, aH, active) in [(''' + + +The CMYK or subtractive + +method follows the way a printer +mixes three pigments (cyan, magenta, and yellow) to form colors. +Because mixing chemicals is more difficult than combining light there +is a fourth parameter for darkness. For example a chemical +combination of the CMY pigments generally never makes a perfect + +black -- instead producing a muddy color -- so, to get black printers +don't use the CMY pigments but use a direct black ink. Because +CMYK maps more directly to the way printer hardware works it may +be the case that &| & | colors specified in CMYK will provide better fidelity +and better control when printed. + + +''',0,DTstyle, 456.0, 42.8, 0), +(''' + + This is a non rearranging form of the Paragraph class; + XML tags are allowed in text and have the same + + meanings as for the Paragraph class. + As for Preformatted, if dedent is non zero dedent + common leading spaces will be removed from the + front of each line. + +''',3, DTstyle, 456.0, 42.8, 0), +("""\ + class FastXMLParser: + # Nonsense method + def nonsense(self): + self.foo = 'bar' +""",0, styleSheet['Code'], 456.0, 4.8, 1), +]: + if active: try_it(text,style,dedent,aW,aH)