really merge para-measure-fix; version-->3.3.11
authorrobin
Mon, 06 Jun 2016 14:27:51 +0100
changeset 4277 838129322a55
parent 4276 e834b7475072
child 4278 5aa80e7cdba8
really merge para-measure-fix; version-->3.3.11
docs/userguide/ch5_paragraphs.py
src/reportlab/__init__.py
src/reportlab/lib/styles.py
src/reportlab/platypus/paragraph.py
src/reportlab/platypus/paraparser.py
src/reportlab/platypus/xpreformatted.py
src/reportlab/rl_settings.py
tests/test_platypus_breaking.py
--- a/docs/userguide/ch5_paragraphs.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/docs/userguide/ch5_paragraphs.py	Mon Jun 06 14:27:51 2016 +0100
@@ -234,7 +234,7 @@
 table below shows the allowed attributes and synonyms in the
 outermost paragraph tag.""")
 
-
+CPage(1)
 heading2("Intra-paragraph markup")
 disc("""<![CDATA[Within each paragraph, we use a basic set of XML tags
 to provide markup.  The most basic of these are bold (<b>...</b>),
@@ -385,7 +385,7 @@
 overrides the implied bullet style and ^bulletText^ specified in the  ^Paragraph^
 creation.
 """)
-parabox("""<bullet>\u2022</bullet>this is a bullet point.  Spam
+parabox("""<bullet>&bull;</bullet>this is a bullet point.  Spam
 spam spam spam spam spam spam spam spam spam spam spam
 spam spam spam spam spam spam spam spam spam spam """,
         styleSheet['Bullet'],
--- a/src/reportlab/__init__.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/src/reportlab/__init__.py	Mon Jun 06 14:27:51 2016 +0100
@@ -1,9 +1,9 @@
 #Copyright ReportLab Europe Ltd. 2000-2016
 #see license.txt for license details
 __doc__="""The Reportlab PDF generation library."""
-Version = "3.3.10"
+Version = "3.3.11"
 __version__=Version
-__date__='20160601'
+__date__='20160606'
 
 import sys, os
 
--- a/src/reportlab/lib/styles.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/src/reportlab/lib/styles.py	Mon Jun 06 14:27:51 2016 +0100
@@ -25,7 +25,9 @@
 from reportlab.lib.colors import white, black
 from reportlab.lib.enums import TA_LEFT, TA_CENTER
 from reportlab.lib.fonts import tt2ps
-from reportlab.rl_config import canvas_basefontname as _baseFontName, baseUnderlineProportion as _baseUnderlineProportion
+from reportlab.rl_config import canvas_basefontname as _baseFontName, \
+                                baseUnderlineProportion as _baseUnderlineProportion, \
+                                spaceShrinkage
 _baseFontNameB = tt2ps(_baseFontName,1,0)
 _baseFontNameI = tt2ps(_baseFontName,0,1)
 _baseFontNameBI = tt2ps(_baseFontName,1,1)
@@ -132,6 +134,7 @@
         'bulletAnchor': 'start',    #where the bullet is anchored ie start, middle, end or numeric
         'justifyLastLine': 0,   #n allow justification on the last line for more than n words 0 means don't bother
         'justifyBreaks': 0,     #justify lines broken with <br/>
+        'spaceShrinkage': spaceShrinkage,   #allow shrinkage of percentage of space to fit on line
         }
 
 class LineStyle(PropertySet):
--- a/src/reportlab/platypus/paragraph.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/src/reportlab/platypus/paragraph.py	Mon Jun 06 14:27:51 2016 +0100
@@ -363,24 +363,56 @@
         if dy: tx.setTextOrigin(tx._x0,xs.cur_y-dy)
 
 def _leftDrawParaLineX( tx, offset, line, last=0):
-    setXPos(tx,offset)
-    _putFragLine(offset, tx, line, last, 'left')
+    extraSpace = line.extraSpace
+    simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
+    if not simple:
+        nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
+        simple = not nSpaces
+    if simple:
+        setXPos(tx,offset)
+        _putFragLine(offset, tx, line, last, 'left')
+    else:
+        tx.setWordSpace(extraSpace / float(nSpaces))
+        _putFragLine(offset, tx, line, last, 'left')
+        tx.setWordSpace(0)
     setXPos(tx,-offset)
 
 def _centerDrawParaLineX( tx, offset, line, last=0):
     tx._dotsOffsetX = offset + tx._x0
     try:
-        m = offset+0.5*line.extraSpace
-        setXPos(tx,m)
-        _putFragLine(m,tx, line, last,'center')
+        extraSpace = line.extraSpace
+        simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
+        if not simple:
+            nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
+            simple = not nSpaces
+        if simple:
+            m = offset+0.5*line.extraSpace
+            setXPos(tx,m)
+            _putFragLine(m, tx, line, last,'center')
+        else:
+            m = offset
+            tx.setWordSpace(extraSpace / float(nSpaces))
+            _putFragLine(m, tx, line, last, 'center')
+            tx.setWordSpace(0)
         setXPos(tx,-m)
     finally:
         del tx._dotsOffsetX
 
 def _rightDrawParaLineX( tx, offset, line, last=0):
-    m = offset+line.extraSpace
-    setXPos(tx,m)
-    _putFragLine(m,tx, line, last, 'right')
+    extraSpace = line.extraSpace
+    simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
+    if not simple:
+        nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
+        simple = not nSpaces
+    if simple:
+        m = offset+line.extraSpace
+        setXPos(tx,m)
+        _putFragLine(m,tx, line, last, 'right')
+    else:
+        m = offset
+        tx.setWordSpace(extraSpace / float(nSpaces))
+        _putFragLine(m, tx, line, last, 'right')
+        tx.setWordSpace(0)
     setXPos(tx,-m)
 
 def _justifyDrawParaLineX( tx, offset, line, last=0):
@@ -402,6 +434,13 @@
     ws = _wsc_end_search(text)
     return tx._canvas.stringWidth(ws.group(), tx._fontname, tx._fontsize) if ws else 0
 
+class _HSWord(list):
+    pass
+
+_FK_TEXT = 0
+_FK_IMG = 1
+_FK_APPEND = 2
+_FK_BREAK = 3
 def _getFragWords(frags,maxWidth=None):
     ''' given a Parafrag list return a list of fragwords
         [[size, (f00,w00), ..., (f0n,w0n)],....,[size, (fm0,wm0), ..., (f0n,wmn)]]
@@ -410,38 +449,48 @@
     '''
     R = []
     W = []
+    hangingSpace = False
     n = 0
-    hangingStrip = False
+    hangingStrip = True
     for f in frags:
         text = f.text
-        #del f.text # we can't do this until we sort out splitting
-                    # of paragraphs
         if text!='':
+            f._fkind = _FK_TEXT
             if hangingStrip:
+                text = text.lstrip()
+                if not text: continue
                 hangingStrip = False
-                text = text.lstrip()
             S = split(text)
-            if S==[]: S = ['']
-            if W!=[] and text[0] in whitespace:
-                W.insert(0,n)
-                R.append(W)
-                W = []
-                n = 0
+            if text[0] in whitespace:
+                if W:
+                    W.insert(0,n)   #end preceding word
+                    R.append(W)
+                    whs = hangingSpace
+                    W = []
+                    hangingSpace = False
+                    n = 0
+                else:
+                    whs = R and isinstance(R[-1],_HSWord)
+                if not whs:
+                    S.insert(0,'')
+                elif not S:
+                    continue
 
             for w in S[:-1]:
                 W.append((f,w))
                 n += stringWidth(w, f.fontName, f.fontSize)
                 W.insert(0,n)
-                R.append(W)
+                R.append(_HSWord(W))
                 W = []
                 n = 0
 
+            hangingSpace = False
             w = S[-1]
             W.append((f,w))
             n += stringWidth(w, f.fontName, f.fontSize)
             if text and text[-1] in whitespace:
                 W.insert(0,n)
-                R.append(W)
+                R.append(_HSWord(W))
                 W = []
                 n = 0
         elif hasattr(f,'cbDefn'):
@@ -451,25 +500,34 @@
                 if hasattr(w,'normalizedValue'):
                     w._normalizer = maxWidth
                     w = w.normalizedValue(maxWidth)
-                if W!=[]:
+                if W:
                     W.insert(0,n)
-                    R.append(W)
+                    R.append(_HSWord(W) if hangingSpace else W)
                     W = []
+                    hangingSpace = False
                     n = 0
+                f._fkind = _FK_IMG
                 R.append([w,(f,'')])
+                hangingStrip = False
             else:
-                W.append((f,''))
+                f._fkind = _FK_APPEND
+                if not W and R and isinstance(R[-1],_HSWord):
+                    R[-1].append((f,''))
+                else:
+                    W.append((f,''))
         elif hasattr(f, 'lineBreak'):
             #pass the frag through.  The line breaker will scan for it.
-            if W!=[]:
+            if W:
                 W.insert(0,n)
                 R.append(W)
                 W = []
                 n = 0
+                hangingSpace = False
+            f._fkind = _FK_BREAK
             R.append([0,(f,'')])
             hangingStrip = True
 
-    if W!=[]:
+    if W:
         W.insert(0,n)
         R.append(W)
 
@@ -490,6 +548,9 @@
 class _SplitList(list):
     pass
 
+class _HSSplitList(_HSWord):
+    pass
+
 def _splitFragWord(w,maxWidth,maxWidths,lineno):
     '''given a frag word, w, as returned by getFragWords
     split it into frag words that fit in lines of length
@@ -529,7 +590,8 @@
         fragText += c
         lineWidth = newLineWidth
     W.append((f,fragText))
-    W = _SplitList([wordWidth]+W)
+    W = _HSSplitList([wordWidth]+W) if isinstance(w,_HSWord) else _SplitList([wordWidth]+W)
+
     R.append(W)
     return R
 
@@ -1123,12 +1185,9 @@
             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)
+            return max(stringWidth(w,fN,fS) for w in (split(f.text, ' ') if hasattr(f,'text') else f.words))
         else:
-            words = _getFragWords(frags)
-            func  = lambda x: x[0]
-        return max(list(map(func,words)))
+            return max(w[0] for w in _getFragWords(frags))
 
     def _get_split_blParaFunc(self):
         return self.blPara.kind==0 and _split_blParaSimple or _split_blParaHard
@@ -1256,6 +1315,7 @@
         self.height = lineno = 0
         maxlineno = len(maxWidths)-1
         style = self.style
+        spaceShrinkage = style.spaceShrinkage
         splitLongWords = style.splitLongWords
         self._splitLongWordCount = 0
 
@@ -1328,19 +1388,17 @@
                 #NB this is an utter hack that awaits the proper information
                 #preserving splitting algorithm
                 return self.blPara
-            n = 0
             njlbv = not style.justifyBreaks
             words = []
             _words = _getFragWords(frags,maxWidth)
             while _words:
                 w = _words.pop(0)
-                f=w[-1][0]
+                f = w[-1][0]
                 fontName = f.fontName
                 fontSize = f.fontSize
-                spaceWidth = stringWidth(' ',fontName, fontSize)
 
                 if not words:
-                    currentWidth = -spaceWidth   # hack to get around extra space for word 1
+                    n = space = spaceWidth = currentWidth = 0
                     maxSize = fontSize
                     maxAscent, minDescent = getAscentDescent(fontName,fontSize)
 
@@ -1353,24 +1411,23 @@
 
                 #test to see if this frag is a line break. If it is we will only act on it
                 #if the current width is non-negative or the previous thing was a deliberate lineBreak
-                lineBreak = hasattr(f,'lineBreak')
-                if not lineBreak and newWidth>maxWidth and not isinstance(w,_SplitList) and splitLongWords:
+                lineBreak = f._fkind==_FK_BREAK
+                if not lineBreak and newWidth>(maxWidth+space*spaceShrinkage) and not isinstance(w,_SplitList) and splitLongWords:
                     nmw = min(lineno,maxlineno)
                     if wordWidth>max(maxWidths[nmw:nmw+1]):
                         #a long word
                         _words[0:0] = _splitFragWord(w,maxWidth-spaceWidth-currentWidth,maxWidths,lineno)
                         self._splitLongWordCount += 1
                         continue
-                endLine = (newWidth>maxWidth and n>0) or lineBreak
+                endLine = (newWidth>(maxWidth+space*spaceShrinkage) and n>0) or lineBreak
                 if not endLine:
                     if lineBreak: continue      #throw it away
                     nText = w[1][1]
                     if nText: n += 1
                     fontSize = f.fontSize
                     if calcBounds:
-                        cbDefn = getattr(f,'cbDefn',None)
-                        if getattr(cbDefn,'width',0):
-                            descent,ascent = imgVRange(imgNormV(cbDefn.height,fontSize),cbDefn.valign,fontSize)
+                        if f._fkind==_FK_IMG:
+                            descent,ascent = imgVRange(imgNormV(f.cbDefn.height,fontSize),f.cbDefn.valign,fontSize)
                         else:
                             ascent, descent = getAscentDescent(f.fontName,fontSize)
                     else:
@@ -1383,28 +1440,29 @@
                         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 i>=0:
-                                    wi = words[i]
-                                    cbDefn = getattr(wi,'cbDefn',None)
-                                    if cbDefn:
-                                        if not getattr(cbDefn,'width',0):
-                                            i -= 1
-                                            continue
+                        if spaceWidth:
+                            i = len(words)-1
+                            while i>=0:
+                                wi = words[i]
+                                i -= 1
+                                if wi._fkind==_FK_TEXT:
                                     if not wi.text.endswith(' '):
                                         wi.text += ' '
+                                        space += spaceWidth
                                     break
-                            else:
-                                if not g.text.endswith(' '):
-                                    g.text += ' '
                         g = f.clone()
                         words.append(g)
                         g.text = nText
+                    elif spaceWidth:
+                        if not g.text.endswith(' '):
+                            g.text += ' ' + nText
+                            space += spaceWidth
+                        else:
+                            g.text += nText
                     else:
-                        if nText and nText[0]!=' ':
-                            g.text += ' ' + nText
+                        g.text += nText
+
+                    spaceWidth = stringWidth(' ',fontName,fontSize) if isinstance(w,_HSWord) else 0 #of the space following this word
 
                     ni = 0
                     for i in w[2:]:
@@ -1414,9 +1472,8 @@
                         words.append(g)
                         fontSize = g.fontSize
                         if calcBounds:
-                            cbDefn = getattr(g,'cbDefn',None)
-                            if getattr(cbDefn,'width',0):
-                                descent,ascent = imgVRange(imgNormV(cbDefn.height,fontSize),cbDefn.valign,fontSize)
+                            if g._fkind==_FK_IMG:
+                                descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
                             else:
                                 ascent, descent = getAscentDescent(g.fontName,fontSize)
                         else:
@@ -1445,18 +1502,18 @@
                     maxWidth = maxWidths[min(maxlineno,lineno)]
 
                     if lineBreak:
-                        n = 0
                         words = []
                         continue
 
+                    spaceWidth = stringWidth(' ',fontName,fontSize) if isinstance(w,_HSWord) else 0 #of the space following this word
                     currentWidth = wordWidth
                     n = 1
+                    space = 0
                     g = f.clone()
                     maxSize = g.fontSize
                     if calcBounds:
-                        cbDefn = getattr(g,'cbDefn',None)
-                        if getattr(cbDefn,'width',0):
-                            minDescent,maxAscent = imgVRange(imgNormV(cbDefn.height,fontSize),cbDefn.valign,maxSize)
+                        if g._fkind==_FK_IMG:
+                            descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
                         else:
                             maxAscent, minDescent = getAscentDescent(g.fontName,maxSize)
                     else:
@@ -1470,9 +1527,8 @@
                         words.append(g)
                         fontSize = g.fontSize
                         if calcBounds:
-                            cbDefn = getattr(g,'cbDefn',None)
-                            if getattr(cbDefn,'width',0):
-                                descent,ascent = imgVRange(imgNormV(cbDefn.height,fontSize),cbDefn.valign,fontSize)
+                            if g._fkind==_FK_IMG:
+                                descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
                             else:
                                 ascent, descent = getAscentDescent(g.fontName,fontSize)
                         else:
--- a/src/reportlab/platypus/paraparser.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/src/reportlab/platypus/paraparser.py	Mon Jun 06 14:27:51 2016 +0100
@@ -204,6 +204,7 @@
                 'texttransform':('textTransform',_textTransformConv),
                 'enddots':('endDots',None),
                 'underlineproportion':('underlineProportion',_num),
+                'spaceshrinkage':('spaceShrinkage',_num),
                 }
 
 _bulletAttrMap = {
@@ -1027,7 +1028,7 @@
         if 'name' in attr: defn.name = attr['name']
         else: self._syntax_error('<onDraw> needs at least a name attribute')
 
-        if 'label' in attr: defn.label = attr['label']
+        defn.label = attr.get('label',None)
         defn.kind='onDraw'
         self._push('ondraw',cbDefn=defn)
         self.handle_data('')
--- a/src/reportlab/platypus/xpreformatted.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/src/reportlab/platypus/xpreformatted.py	Mon Jun 06 14:27:51 2016 +0100
@@ -192,7 +192,7 @@
                 maxWidth = lineno<len(maxWidths) and maxWidths[lineno] or maxWidths[-1]
                 requiredWidth = max(currentWidth,requiredWidth)
                 extraSpace = maxWidth - currentWidth
-                lines.append(ParaLines(extraSpace=extraSpace,wordCount=n, words=words, fontSize=maxSize, ascent=maxAscent,descent=minDescent,currentWidth=currentWidth))
+                lines.append(ParaLines(extraSpace=extraSpace,wordCount=n, words=words, fontSize=maxSize, ascent=maxAscent,descent=minDescent,currentWidth=currentWidth,preformatted=True))
 
             self.width = max(self.width,requiredWidth)
             return ParaLines(kind=1, lines=lines)
--- a/src/reportlab/rl_settings.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/src/reportlab/rl_settings.py	Mon Jun 06 14:27:51 2016 +0100
@@ -52,7 +52,8 @@
 decimalSymbol
 errorOnDuplicatePageLabelPage
 autoGenerateMissingTTFName
-allowTTFSubsetting'''.split())
+allowTTFSubsetting
+spaceShrinkage'''.split())
 
 allowTableBoundsErrors =    1 # set to 0 to die on too large elements in tables in debug (recommend 1 for production use)
 shapeChecking =             1
@@ -114,6 +115,8 @@
                                                     #This flag could already be overcome by hacking the code.
                                                     #ReportLab takes no responsibility for the use of this setting.
 
+spaceShrinkage=0.05                                 #allowable space shrinkage to make lines fit
+
 
 # places to look for T1Font information
 T1SearchPath =  (
--- a/tests/test_platypus_breaking.py	Wed Jun 01 12:10:52 2016 +0100
+++ b/tests/test_platypus_breaking.py	Mon Jun 06 14:27:51 2016 +0100
@@ -5,7 +5,7 @@
 __version__='3.3.0'
 from reportlab.lib.testutils import setOutDir,makeSuiteForClasses, outputfile, printLocation
 setOutDir(__name__)
-import sys, os, time
+import sys, os, time, re
 from operator import truth
 import unittest
 from reportlab.platypus.flowables import Flowable
@@ -18,8 +18,9 @@
 from reportlab.lib.randomtext import randomText, PYTHON
 from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate, Indenter, SimpleDocTemplate
 from reportlab.platypus.paragraph import *
-from reportlab.rl_config import invariant
-
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.rl_config import invariant, paraFontSizeHeightOffset
+from reportlab.pdfbase.pdfmetrics import stringWidth
 
 def myMainPageFrame(canvas, doc):
     "The page frame used for all PDF documents."
@@ -30,7 +31,6 @@
     canvas.drawString(10*cm, cm, str(pageNumber))
     canvas.restoreState()
 
-
 class MyDocTemplate(BaseDocTemplate):
     _invalidInitArgs = ('pageTemplates',)
 
@@ -153,7 +153,6 @@
     doc = MyDocTemplate(outputfile('test_platypus_breaking.pdf'))
     doc.multiBuild(story)
 
-
 class BreakingTestCase(unittest.TestCase):
     "Test multi-page splitting of paragraphs (eyeball-test)."
     def test0(self):
@@ -187,8 +186,6 @@
         self.assertEqual(len(p.split(20,16)),2) #orphans allowed
 
     def test3(self):
-        from reportlab.pdfgen.canvas import Canvas
-
         aW=307
         styleSheet = getSampleStyleSheet()
         bt = styleSheet['BodyText']
@@ -210,16 +207,186 @@
         canv.restoreState()
         canv.showPage()
         canv.save()
-        from reportlab import rl_config
-        x = rl_config.paraFontSizeHeightOffset and '50' or '53.17'
+        x = paraFontSizeHeightOffset and '50' or '53.17'
         good = ['q', '1 0 0 1 0 0 cm', 'q', 'BT 1 0 0 1 0 '+x+' Tm 3.59 Tw 12 TL /F1 10 Tf 0 0 0 rg (Subsequent pages test pageBreakBefore, frameBreakBefore and) Tj T* 0 Tw .23 Tw (keepTogether attributes. Generated at 1111. The number in brackets) Tj T* 0 Tw .299167 Tw (at the end of each paragraph is its position in the story. llllllllllllllllllllllllll) Tj T* 0 Tw 66.9 Tw (bbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccc) Tj T* 0 Tw (ddddddddddddddddddddd eeeeyyy) Tj T* ET', 'Q', 'Q']
         ok= ParaCode==good
         assert ok, "\nParaCode=%r\nexpected=%r" % (ParaCode,good)
 
+    def test4(self):
+        styleSheet = getSampleStyleSheet()
+        bt = styleSheet['BodyText']
+        bfn = bt.fontName = 'Helvetica'
+        bfs = bt.fontSize
+        bfl = bt.leading
+        canv=Canvas(outputfile('test_platypus_paragraph_line_lengths.pdf'))
+        canv.setFont('Courier',bfs,bfl)
+        pageWidth, pageHeight = canv._pagesize
+        y = pageHeight - 15
+        x = stringWidth('999: ','Courier',bfs) + 5
+        aW = int(pageWidth)-2*x
+
+        def doPara(x,text,wc,ns,n,hrep=' ',crep=' ',hdw=0,cdw=0):
+            if '{H}' in text:
+                text = text.replace('{H}',hrep)
+                wc += hdw
+            if '{C}' in text:
+                text = text.replace('{C}',crep)
+                wc += cdw
+            p = Paragraph(text,bt)
+            w,h = p.wrap(aW,1000)
+            annotations[:] = []
+            if measuring:
+                ends[:] = []
+            p.drawOn(canv,x,y-h)
+            canv.saveState()
+            canv.setLineWidth(0.1)
+            canv.setStrokeColorRGB(1,0,0)
+            canv.rect(x,y-h,wc,h)
+
+            if n is not None:
+                canv.setFillColorRGB(0,1,0)
+                canv.drawRightString(x,y-h,'%3d: ' % n)
+
+            if annotations:
+                canv.setLineWidth(0.1)
+                canv.setStrokeColorRGB(0,1,0)
+                canv.setFillColorRGB(0,0,1)
+                canv.setFont('Helvetica',0.2)
+                for info in annotations:
+                    cur_x = info['cur_x']+x
+                    cur_y = info['cur_y']+y-h
+                    canv.drawCentredString(cur_x, cur_y+0.3,'%.2f' % (cur_x-x))
+                    canv.line(cur_x,cur_y,cur_x,cur_y+0.299)
+            if measuring:
+                if not ends:
+                    errors.append('Paragraph measurement failure no ends found for %s\n%r' % (ns,text))
+                elif len(ends)>1:
+                    errors.append('Paragraph measurement failure no len(ends)==%d for %s\n%r' % (len(ends),ns,text))
+                else:
+                    cur_x = ends[0]['cur_x']
+                    adiff = abs(wc-cur_x)
+                    length_errors.append(adiff)
+                    if adiff>1e-8:
+                        errors.append('Paragraph measurement error wc=%.4f measured=%.4f for %s\n%r' % (wc,cur_x,ns,text))
+            canv.restoreState()
+            return h
+        swc = lambda t: stringWidth(t,'Courier',bfs)
+        swcbo = lambda t: stringWidth(t,'Courier-BoldOblique',bfs)
+        swh = lambda t: stringWidth(t,'Helvetica',bfs)
+        swhbo = lambda t: stringWidth(t,'Helvetica-BoldOblique',bfs)
+        swt = lambda t: stringWidth(t,'Times-Roman',bfs)
+        swtb = lambda t: stringWidth(t,'Times-Bold',bfs)
+
+        apat = re.compile("(<a\\s+name='a\\d+'/>)")
+        argv = sys.argv[1:]
+        data = (
+            (0,"<span fontName='Courier'>Hello{C}</span> World.", swc('Hello ')+swh('World.')),
+            (1,"<span fontName='Courier'>Hello</span>{H}World.", swc('Hello')+swh(' World.')),
+            (2," <a name='a2'/><span fontName='Courier'>Hello{C}</span> World.", swc('Hello ')+swh('World.')),
+            (3," <a name='a3'/><span fontName='Courier'>Hello</span>{H}World.", swc('Hello')+swh(' World.')),
+            (4,"<span fontName='Courier'><a name='a4'/>Hello{C}</span> World.", swc('Hello ')+swh('World.')),
+            (5,"<span fontName='Courier'><a name='a5'/>Hello</span>{H}World.", swc('Hello')+swh(' World.')),
+            (6,"<span fontName='Courier'>Hello<a name='a6'/>{C}</span> World.", swc('Hello ')+swh('World.')),
+            (7,"<span fontName='Courier'>Hello<a name='a7'/></span>{H}World.", swc('Hello')+swh(' World.')),
+            (8,"<span fontName='Courier'>Hello{C}<a name='a8'/></span> World.", swc('Hello ')+swh('World.')),
+            (9,"<span fontName='Courier'>Hello</span><a name='a9'/>{H}World.", swc('Hello')+swh(' World.')),
+            (10,"<span fontName='Courier'>Hello{C}</span> <a name='a10'/>World.", swc('Hello ')+swh('World.')),
+            (11,"<span fontName='Courier'>Hello</span>{H}<a name='a11'/>World.", swc('Hello')+swh(' World.')),
+            (12,"<span fontName='Courier'>Hello{C}</span> World. <a name='a12'/>", swc('Hello ')+swh('World.')),
+            (13,"<span fontName='Courier'>Hello</span>{H}World. <a name='a13'/>", swc('Hello')+swh(' World.')),
+            (14," <a name='a2'/> <span fontName='Courier'>Hello{C}</span> World.", swc('Hello ')+swh('World.')),
+            (15," <a name='a3'/> <span fontName='Courier'>Hello</span>{H}World.", swc('Hello')+swh(' World.')),
+            (16," <a name='a2'/> <span fontName='Courier'>Hello{C}<a name='b'/> </span> <a name='b'/> World.", swc('Hello ')+swh('World.')),
+            (17," <a name='a3'/> <span fontName='Courier'>Hello</span>{H}<a name='b'/> World.", swc('Hello')+swh(' World.')),
+            (30,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span>{C}</span> World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (31,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span>{H}World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (32," <a name='a2'/><span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span>{C}</span> World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (33," <a name='a3'/><span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span>{H}World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (34,"<span fontName='Courier'><a name='a4'/>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span> </span> World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (35,"<span fontName='Courier'><a name='a5'/>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span>{H}World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (36,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span><a name='a6'/> </span> World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (37,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span><a name='a7'/></span>{H}World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (38,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span>{C}<a name='a8'/></span> World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (39,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span><a name='a9'/>{H}World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (40,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span>{C}</span> <a name='a10'/>World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (41,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span>{H}<a name='a11'/>World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (42,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span>{C}</span> World. <a name='a12'/>", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (43,"<span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span> World.{H}<a name='a13'/>", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (44," <a name='a2'/> <span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span>{C}</span> World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (45," <a name='a3'/> <span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span>{H}World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            (46," <a name='a2'/> <span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span>{C}<a name='b'/> </span> <a name='b'/> World.", swt('l')+swtb('lo')+swc('He ')+swh('World.')),
+            (47," <a name='a3'/> <span fontName='Courier'>He<span face='Times-Roman' color='red'>l</span><span face='Times-Bold' color='orange'>lo</span></span>{H}<a name='b'/> World.", swt('l')+swtb('lo')+swc('He')+swh(' World.')),
+            )
+        _exceptions = {
+                1:  {
+                    8: swh(' '),
+                    12: swh(' '),
+                    13: swh(' '),
+                    14: swh(' '),
+                    15: swh(' '),
+                    16: swh(' '),
+                    17: swh(' '),
+                    38: swh(' '),
+                    42: swh(' '),
+                    43: swh(' '),
+                    44: swh(' '),
+                    45: swh(' '),
+                    46: swh(' '),
+                    47: swh(' '),
+                    },
+                }
+        def gex(n,v):
+            return _exceptions[1].get(v,0)
+        x1 = x + max(_tmp[2] for _tmp in data) + 5
+        x2 = x1 + max(_tmp[2]+10+gex(1,_tmp[0]) for _tmp in data) + 5
+        x3 = x2 + max(_tmp[2]+10+gex(2,_tmp[0]) for _tmp in data) + 5
+        x4 = x3 + max(_tmp[2]+20+gex(3,_tmp[0]) for _tmp in data) + 5
+        annotations = []
+        ends = []
+        errors = []
+        measuring = True
+        length_errors = []
+        def _onDrawFunc(canv,name,label):
+            if measuring and label=='end':
+                ends.append(canv._curr_tx_info)
+            annotations.append(canv._curr_tx_info)
+        canv._onDrawFunc = _onDrawFunc
+
+        rep0 = '<ondraw name="_onDrawFunc"/>\\1'
+        for n,text,wc in data:
+            if argv and str(n) not in argv: continue
+            text0 = (apat.sub(rep0,text) if rep0 else text)+('<ondraw name="_onDrawFunc" label="end"/>' if measuring else '')
+            ns = str(n)
+            h = doPara(x,text0,wc,ns,n)
+            if '<a' in text:
+                text1 = apat.sub('<img width="10" height="5" src="pythonpowered.gif"/>',text0)
+                doPara(x1,text1,wc+10+gex(1,n),ns+'.11',None)
+                text2 = apat.sub('\\1<img width="10" height="5" src="pythonpowered.gif"/>',text0)
+                doPara(x2,text1,wc+10+gex(2,n),ns+'.12',None)
+                text3 = apat.sub('\\1<img width="10" height="5" src="pythonpowered.gif"/><img width="10" height="5" src="pythonpowered.gif"/>\\1',text0)
+                doPara(x3,text3,wc+20+gex(3,n),ns+'.13',None)
+                doPara(x4,text3,wc+20+gex(3,n),ns+'.14',None,
+                        hrep='<span face="Courier-BoldOblique"> </span>',
+                        crep='<span face="Helvetica-BoldOblique"> </span>',
+                        hdw = swcbo(' ') - swhbo(' '),
+                        cdw = swhbo(' ') - swcbo(' '),
+                        )
+            else:
+                doPara(x1,text0,wc,ns+'.21',None,
+                        hrep='<span face="Courier-BoldOblique"> </span>',
+                        crep='<span face="Helvetica-BoldOblique"> </span>',
+                        hdw = swcbo(' ') - swhbo(' '),
+                        cdw = swhbo(' ') - swcbo(' '),
+                        )
+            y -= h+1
+        canv.showPage()
+        canv.save()
+        if errors:
+            raise ValueError('\n'.join(errors))
+
 def makeSuite():
     return makeSuiteForClasses(BreakingTestCase)
 
-
 #noruntests
 if __name__ == "__main__": #NORUNTESTS
     if 'debug' in sys.argv: