"Fossies" - the Fresh Open Source Software Archive

Member "reportlab-3.5.23/src/reportlab/platypus/paragraph.py" (31 May 2019, 100127 Bytes) of package /linux/privat/reportlab-3.5.23.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "paragraph.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3.5.21_vs_3.5.23.

    1 #Copyright ReportLab Europe Ltd. 2000-2017
    2 #see license.txt for license details
    3 #history https://bitbucket.org/rptlab/reportlab/history-node/tip/src/reportlab/platypus/paragraph.py
    4 __all__=(
    5         'Paragraph',
    6         'cleanBlockQuotedText',
    7         'ParaLines',
    8         'FragLine',
    9         )
   10 __version__='3.5.20'
   11 __doc__='''The standard paragraph implementation'''
   12 from string import whitespace
   13 from operator import truth
   14 from unicodedata import category
   15 from reportlab.pdfbase.pdfmetrics import stringWidth, getFont, getAscentDescent
   16 from reportlab.platypus.paraparser import ParaParser, _PCT, _num as _parser_num, _re_us_value
   17 from reportlab.platypus.flowables import Flowable
   18 from reportlab.lib.colors import Color
   19 from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
   20 from reportlab.lib.geomutils import normalizeTRBL
   21 from reportlab.lib.textsplit import wordSplit, ALL_CANNOT_START
   22 from copy import deepcopy
   23 from reportlab.lib.abag import ABag
   24 from reportlab.rl_config import platypus_link_underline, decimalSymbol, _FUZZ,\
   25         paraFontSizeHeightOffset, hyphenationMinWordLength
   26 from reportlab.lib.utils import _className, isBytes, unicodeT, bytesT, isStr
   27 from reportlab.lib.rl_accel import sameFrag
   28 from reportlab import xrange
   29 import re
   30 from types import MethodType
   31 try:
   32     import pyphen
   33 except:
   34     pyphen = None
   35 
   36 #on UTF8/py33 branch, split and strip must be unicode-safe!
   37 #thanks to Dirk Holtwick for helpful discussions/insight
   38 #on this one
   39 _wsc = ''.join((
   40     u'\u0009',  # HORIZONTAL TABULATION
   41     u'\u000A',  # LINE FEED
   42     u'\u000B',  # VERTICAL TABULATION
   43     u'\u000C',  # FORM FEED
   44     u'\u000D',  # CARRIAGE RETURN
   45     u'\u001C',  # FILE SEPARATOR
   46     u'\u001D',  # GROUP SEPARATOR
   47     u'\u001E',  # RECORD SEPARATOR
   48     u'\u001F',  # UNIT SEPARATOR
   49     u'\u0020',  # SPACE
   50     u'\u0085',  # NEXT LINE
   51     #u'\u00A0', # NO-BREAK SPACE
   52     u'\u1680',  # OGHAM SPACE MARK
   53     u'\u2000',  # EN QUAD
   54     u'\u2001',  # EM QUAD
   55     u'\u2002',  # EN SPACE
   56     u'\u2003',  # EM SPACE
   57     u'\u2004',  # THREE-PER-EM SPACE
   58     u'\u2005',  # FOUR-PER-EM SPACE
   59     u'\u2006',  # SIX-PER-EM SPACE
   60     u'\u2007',  # FIGURE SPACE
   61     u'\u2008',  # PUNCTUATION SPACE
   62     u'\u2009',  # THIN SPACE
   63     u'\u200A',  # HAIR SPACE
   64     u'\u200B',  # ZERO WIDTH SPACE
   65     u'\u2028',  # LINE SEPARATOR
   66     u'\u2029',  # PARAGRAPH SEPARATOR
   67     u'\u202F',  # NARROW NO-BREAK SPACE
   68     u'\u205F',  # MEDIUM MATHEMATICAL SPACE
   69     u'\u3000',  # IDEOGRAPHIC SPACE
   70     ))
   71 _wsc_re_split=re.compile('[%s]+'% re.escape(_wsc)).split
   72 _wsc_end_search=re.compile('[%s]+$'% re.escape(_wsc)).search
   73 
   74 def _usConv(s, vMap, default=None):
   75     '''convert a strike/underline distance to a number'''
   76     if isStr(s):
   77         s = s.strip()
   78         if s:
   79             m = _re_us_value.match(s)
   80             if m:
   81                 return float(m.group(1))*vMap[m.group(2)]
   82             else:
   83                 return _parser_num(s,allowRelative=False)
   84         elif default:
   85             return default
   86     return s
   87 
   88 def split(text, delim=None):
   89     if isBytes(text): text = text.decode('utf8')
   90     if delim is not None and isBytes(delim): delim = delim.decode('utf8')
   91     return [uword for uword in (_wsc_re_split(text) if delim is None and u'\xa0' in text else text.split(delim))]
   92 
   93 def strip(text):
   94     if isBytes(text): text = text.decode('utf8')
   95     return text.strip(_wsc)
   96 
   97 def lstrip(text):
   98     if isBytes(text): text = text.decode('utf8')
   99     return text.lstrip(_wsc)
  100 
  101 def rstrip(text):
  102     if isBytes(text): text = text.decode('utf8')
  103     return text.rstrip(_wsc)
  104 
  105 class ParaLines(ABag):
  106     """
  107     class ParaLines contains the broken into lines representation of Paragraphs
  108         kind=0  Simple
  109         fontName, fontSize, textColor apply to whole Paragraph
  110         lines   [(extraSpace1,words1),....,(extraspaceN,wordsN)]
  111 
  112         kind==1 Complex
  113         lines   [FragLine1,...,FragLineN]
  114     """
  115 
  116 class FragLine(ABag):
  117     """
  118     class FragLine contains a styled line (ie a line with more than one style)::
  119 
  120         extraSpace  unused space for justification only
  121         wordCount   1+spaces in line for justification purposes
  122         words       [ParaFrags] style text lumps to be concatenated together
  123         fontSize    maximum fontSize seen on the line; not used at present,
  124                     but could be used for line spacing.
  125     """
  126 
  127 def _lineClean(L):
  128     return ' '.join(list(filter(truth,split(strip(L)))))
  129 
  130 def cleanBlockQuotedText(text,joiner=' '):
  131     """This is an internal utility which takes triple-
  132     quoted text form within the document and returns
  133     (hopefully) the paragraph the user intended originally."""
  134     L=list(filter(truth,list(map(_lineClean, split(text, '\n')))))
  135     return joiner.join(L)
  136 
  137 def setXPos(tx,dx):
  138     if dx>1e-6 or dx<-1e-6:
  139         tx.setXPos(dx)
  140 
  141 def _nbspCount(w):
  142     if isBytes(w):
  143         return w.count(b'\xc2\xa0')
  144     else:
  145         return w.count(u'\xa0')
  146 
  147 def _leftDrawParaLine( tx, offset, extraspace, words, last=0):
  148     simple = extraspace>-1e-8 or getattr(tx,'preformatted',False)
  149     text = ' '.join(words)
  150     setXPos(tx,offset)
  151     if not simple:
  152         nSpaces = len(words)+_nbspCount(text)-1
  153         simple = not nSpaces
  154     if simple:
  155         tx._textOut(text,1)
  156     else:
  157         tx.setWordSpace(extraspace / float(nSpaces))
  158         tx._textOut(text,1)
  159         tx.setWordSpace(0)
  160     setXPos(tx,-offset)
  161     return offset
  162 
  163 def _centerDrawParaLine( tx, offset, extraspace, words, last=0):
  164     simple = extraspace>-1e-8 or getattr(tx,'preformatted',False)
  165     text = ' '.join(words)
  166     if not simple:
  167         nSpaces = len(words)+_nbspCount(text)-1
  168         simple = not nSpaces
  169     if simple:
  170         m = offset + 0.5 * extraspace
  171         setXPos(tx,m)
  172         tx._textOut(text,1)
  173     else:
  174         m = offset
  175         tx.setWordSpace(extraspace / float(nSpaces))
  176         setXPos(tx,m)
  177         tx._textOut(text,1)
  178         tx.setWordSpace(0)
  179     setXPos(tx,-m)
  180     return m
  181 
  182 def _rightDrawParaLine( tx, offset, extraspace, words, last=0):
  183     simple = extraspace>-1e-8 or getattr(tx,'preformatted',False)
  184     text = ' '.join(words)
  185     if not simple:
  186         nSpaces = len(words)+_nbspCount(text)-1
  187         simple = not nSpaces
  188     if simple:
  189         m = offset + extraspace
  190         setXPos(tx,m)
  191         tx._textOut(' '.join(words),1)
  192     else:
  193         m = offset
  194         tx.setWordSpace(extraspace / float(nSpaces))
  195         setXPos(tx,m)
  196         tx._textOut(text,1)
  197         tx.setWordSpace(0)
  198     setXPos(tx,-m)
  199     return m
  200 
  201 def _justifyDrawParaLine( tx, offset, extraspace, words, last=0):
  202     setXPos(tx,offset)
  203     text  = ' '.join(words)
  204     simple = last or (-1e-8<extraspace<=1e-8) or getattr(tx,'preformatted',False)
  205     if not simple:
  206         nSpaces = len(words)+_nbspCount(text)-1
  207         simple = not nSpaces
  208     if simple:
  209         #last one or no extra space so left align
  210         tx._textOut(text,1)
  211     else:
  212         tx.setWordSpace(extraspace / float(nSpaces))
  213         tx._textOut(text,1)
  214         tx.setWordSpace(0)
  215     setXPos(tx,-offset)
  216     return offset
  217 
  218 def imgVRange(h,va,fontSize):
  219     '''return bottom,top offsets relative to baseline(0)'''
  220     if va=='baseline':
  221         iyo = 0
  222     elif va in ('text-top','top'):
  223         iyo = fontSize-h
  224     elif va=='middle':
  225         iyo = fontSize - (1.2*fontSize+h)*0.5
  226     elif va in ('text-bottom','bottom'):
  227         iyo = fontSize - 1.2*fontSize
  228     elif va=='super':
  229         iyo = 0.5*fontSize
  230     elif va=='sub':
  231         iyo = -0.5*fontSize
  232     elif hasattr(va,'normalizedValue'):
  233         iyo = va.normalizedValue(fontSize)
  234     else:
  235         iyo = va
  236     return iyo,iyo+h
  237 
  238 def imgNormV(v,nv):
  239     if hasattr(v,'normalizedValue'):
  240         return v.normalizedValue(nv)
  241     else:
  242         return v
  243 
  244 def _getDotsInfo(style):
  245     dots = style.endDots
  246     if isStr(dots):
  247         text = dots
  248         fontName = style.fontName
  249         fontSize = style.fontSize
  250         textColor = style.textColor
  251         backColor = style.backColor
  252         dy = 0
  253     else:
  254         text = getattr(dots,'text','.')
  255         fontName = getattr(dots,'fontName',style.fontName)
  256         fontSize = getattr(dots,'fontSize',style.fontSize)
  257         textColor = getattr(dots,'textColor',style.textColor)
  258         backColor = getattr(dots,'backColor',style.backColor)
  259         dy = getattr(dots,'dy',0)
  260     return text,fontName,fontSize,textColor,backColor,dy
  261 
  262 _56=5./6
  263 _16=1./6
  264 def _putFragLine(cur_x, tx, line, last, pKind):
  265     preformatted = tx.preformatted
  266     xs = tx.XtraState
  267     cur_y = xs.cur_y
  268     x0 = tx._x0
  269     autoLeading = xs.autoLeading
  270     leading = xs.leading
  271     cur_x += xs.leftIndent
  272     dal = autoLeading in ('min','max')
  273     if dal:
  274         if autoLeading=='max':
  275             ascent = max(_56*leading,line.ascent)
  276             descent = max(_16*leading,-line.descent)
  277         else:
  278             ascent = line.ascent
  279             descent = -line.descent
  280         leading = ascent+descent
  281     if tx._leading!=leading:
  282         tx.setLeading(leading)
  283     if dal:
  284         olb = tx._olb
  285         if olb is not None:
  286             xcy = olb-ascent
  287             if tx._oleading!=leading:
  288                 cur_y += leading - tx._oleading
  289             if abs(xcy-cur_y)>1e-8:
  290                 cur_y = xcy
  291                 tx.setTextOrigin(x0,cur_y)
  292                 xs.cur_y = cur_y
  293         tx._olb = cur_y - descent
  294         tx._oleading = leading
  295     ws = getattr(tx,'_wordSpace',0)
  296     nSpaces = 0
  297     words = line.words
  298     AL = []
  299     LL = []
  300     us_lines = xs.us_lines
  301     links = xs.links
  302     for i, f in enumerate(words):
  303         if hasattr(f,'cbDefn'):
  304             cbDefn = f.cbDefn
  305             kind = cbDefn.kind
  306             if kind=='img':
  307                 #draw image cbDefn,cur_y,cur_x
  308                 txfs = tx._fontsize
  309                 if txfs is None:
  310                     txfs = xs.style.fontSize
  311                 w = imgNormV(cbDefn.width,xs.paraWidth)
  312                 h = imgNormV(cbDefn.height,txfs)
  313                 iy0,iy1 = imgVRange(h,cbDefn.valign,txfs)
  314                 cur_x_s = cur_x + nSpaces*ws
  315                 tx._canvas.drawImage(cbDefn.image,cur_x_s,cur_y+iy0,w,h,mask='auto')
  316                 cur_x += w
  317                 cur_x_s += w
  318                 setXPos(tx,cur_x_s-tx._x0)
  319             else:
  320                 name = cbDefn.name
  321                 if kind=='anchor':
  322                     tx._canvas.bookmarkHorizontal(name,cur_x,cur_y+leading)
  323                 else:
  324                     func = getattr(tx._canvas,name,None)
  325                     if not func:
  326                         raise AttributeError("Missing %s callback attribute '%s'" % (kind,name))
  327                     tx._canvas._curr_tx_info=dict(tx=tx,cur_x=cur_x,cur_y=cur_y,leading=leading,xs=tx.XtraState)
  328                     try:
  329                         func(tx._canvas,kind,getattr(cbDefn,'label',None))
  330                     finally:
  331                         del tx._canvas._curr_tx_info
  332             if f is words[-1]:
  333                 if not tx._fontname:
  334                     tx.setFont(xs.style.fontName,xs.style.fontSize)
  335                 tx._textOut('',1)
  336         else:
  337             cur_x_s = cur_x + nSpaces*ws
  338             end_x = cur_x_s
  339             fontSize = f.fontSize
  340             textColor = f.textColor
  341             rise = f.rise
  342             if i > 0:
  343                 end_x = cur_x_s - (0 if preformatted else _trailingSpaceLength(words[i-1].text, tx))
  344             if (tx._fontname,tx._fontsize)!=(f.fontName,fontSize):
  345                 tx._setFont(f.fontName, fontSize)
  346             if xs.textColor!=textColor:
  347                 xs.textColor = textColor
  348                 tx.setFillColor(textColor)
  349             if xs.rise!=rise:
  350                 xs.rise=rise
  351                 tx.setRise(rise)
  352             text = f.text
  353             tx._textOut(text,f is words[-1])    # cheap textOut
  354             if LL != f.us_lines:
  355                 S = set(LL)
  356                 NS = set(f.us_lines)
  357                 nL = NS - S #new lines
  358                 eL = S - NS #ending lines
  359                 for l in eL:
  360                     us_lines[l] = us_lines[l],end_x
  361                 for l in nL:
  362                     us_lines[l] = (l,fontSize,textColor,cur_x_s),fontSize
  363                 LL = f.us_lines
  364             if LL:
  365                 for l in LL:
  366                     l0, fsmax = us_lines[l]
  367                     if fontSize>fsmax:
  368                         us_lines[l] = l0, fontSize
  369 
  370             nlo = rise - 0.2*fontSize
  371             nhi = rise + fontSize
  372             if AL != f.link:
  373                 S = set(AL)
  374                 NS = set(f.link)
  375                 nL = NS - S #new linkis
  376                 eL = S - NS #ending links
  377                 for l in eL:
  378                     links[l] = links[l],end_x
  379                 for l in nL:
  380                     links[l] = (l,cur_x),nlo,nhi
  381                 AL = f.link
  382             if AL:
  383                 for l in AL:
  384                     l0, lo, hi = links[l]
  385                     if nlo<lo or nhi>hi:
  386                         links[l] = l0,min(nlo,lo),max(nhi,hi)
  387 
  388             bg = getattr(f,'backColor',None)
  389             if bg and not xs.backColor:
  390                 xs.backColor = bg
  391                 xs.backColor_x = cur_x_s
  392             elif xs.backColor:
  393                 if not bg:
  394                     xs.backColors.append( (xs.backColor_x, end_x, xs.backColor) )
  395                     xs.backColor = None
  396                 elif f.backColor!=xs.backColor or xs.textColor!=xs.backColor:
  397                     xs.backColors.append( (xs.backColor_x, end_x, xs.backColor) )
  398                     xs.backColor = bg
  399                     xs.backColor_x = cur_x_s
  400             txtlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize)
  401             cur_x += txtlen
  402             nSpaces += text.count(' ')+_nbspCount(text)
  403 
  404     cur_x_s = cur_x+(nSpaces-1)*ws
  405     if last and pKind!='right' and xs.style.endDots:
  406         _do_dots_frag(cur_x,cur_x_s,line.maxWidth,xs,tx)
  407 
  408     if LL:
  409         for l in LL:
  410             us_lines[l] = us_lines[l], cur_x_s
  411 
  412     if AL:
  413         for l in AL:
  414             links[l] = links[l], cur_x_s
  415 
  416     if xs.backColor:
  417         xs.backColors.append( (xs.backColor_x, cur_x_s, xs.backColor) )
  418     if tx._x0!=x0:
  419         setXPos(tx,x0-tx._x0)
  420 
  421 def _do_dots_frag(cur_x, cur_x_s, maxWidth, xs, tx):
  422     text,fontName,fontSize,textColor,backColor,dy = _getDotsInfo(xs.style)
  423     txtlen = tx._canvas.stringWidth(text, fontName, fontSize)
  424     if cur_x_s+txtlen<=maxWidth:
  425         if tx._fontname!=fontName or tx._fontsize!=fontSize:
  426             tx.setFont(fontName,fontSize)
  427         maxWidth += getattr(tx,'_dotsOffsetX',tx._x0)
  428         tx.setTextOrigin(0,xs.cur_y+dy)
  429         setXPos(tx,cur_x_s-cur_x)
  430         n = int((maxWidth-cur_x_s)/txtlen)
  431         setXPos(tx,maxWidth - txtlen*n)
  432         if xs.textColor!=textColor:
  433             tx.setFillColor(textColor)
  434         if backColor: xs.backColors.append((cur_x,maxWidth,backColor))
  435         tx._textOut(n*text,1)
  436         if dy: tx.setTextOrigin(tx._x0,xs.cur_y-dy)
  437 
  438 def _leftDrawParaLineX( tx, offset, line, last=0):
  439     setXPos(tx,offset)
  440     extraSpace = line.extraSpace
  441     simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
  442     if not simple:
  443         nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
  444         simple = not nSpaces
  445     if simple:
  446         _putFragLine(offset, tx, line, last, 'left')
  447     else:
  448         tx.setWordSpace(extraSpace / float(nSpaces))
  449         _putFragLine(offset, tx, line, last, 'left')
  450         tx.setWordSpace(0)
  451     setXPos(tx,-offset)
  452 
  453 def _centerDrawParaLineX( tx, offset, line, last=0):
  454     tx._dotsOffsetX = offset + tx._x0
  455     try:
  456         extraSpace = line.extraSpace
  457         simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
  458         if not simple:
  459             nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
  460             simple = not nSpaces
  461         if simple:
  462             m = offset+0.5*line.extraSpace
  463             setXPos(tx,m)
  464             _putFragLine(m, tx, line, last,'center')
  465         else:
  466             m = offset
  467             tx.setWordSpace(extraSpace / float(nSpaces))
  468             _putFragLine(m, tx, line, last, 'center')
  469             tx.setWordSpace(0)
  470         setXPos(tx,-m)
  471     finally:
  472         del tx._dotsOffsetX
  473 
  474 def _rightDrawParaLineX( tx, offset, line, last=0):
  475     extraSpace = line.extraSpace
  476     simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
  477     if not simple:
  478         nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
  479         simple = not nSpaces
  480     if simple:
  481         m = offset+line.extraSpace
  482         setXPos(tx,m)
  483         _putFragLine(m,tx, line, last, 'right')
  484     else:
  485         m = offset
  486         tx.setWordSpace(extraSpace / float(nSpaces))
  487         _putFragLine(m, tx, line, last, 'right')
  488         tx.setWordSpace(0)
  489     setXPos(tx,-m)
  490 
  491 def _justifyDrawParaLineX( tx, offset, line, last=0):
  492     setXPos(tx,offset)
  493     extraSpace = line.extraSpace
  494     simple = last or abs(extraSpace)<=1e-8 or line.lineBreak
  495     if not simple:
  496         nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
  497         simple = not nSpaces
  498     if not simple:
  499         tx.setWordSpace(extraSpace / float(nSpaces))
  500         _putFragLine(offset, tx, line, last, 'justify')
  501         tx.setWordSpace(0)
  502     else:
  503         _putFragLine(offset, tx, line, last, 'justify') #no space modification
  504     setXPos(tx,-offset)
  505 
  506 def _trailingSpaceLength(text, tx):
  507     ws = _wsc_end_search(text)
  508     return tx._canvas.stringWidth(ws.group(), tx._fontname, tx._fontsize) if ws else 0
  509 
  510 class _HSFrag(list):
  511     pass
  512 
  513 class _SplitFrag(list):
  514     '''a split frag'''
  515     pass
  516 
  517 class _SplitFragH(_SplitFrag):
  518     '''a split frag that's the head part of the split'''
  519     pass
  520 
  521 
  522 class _SplitFragHY(_SplitFragH):
  523     '''a head split frag that need '-' removing befire rejoining'''
  524     pass
  525 
  526 class _SplitFragHS(_SplitFrag,_HSFrag):
  527     """a split frag that's followed by a space"""
  528     pass
  529 
  530 def _processed_frags(frags):
  531     try:
  532         return isinstance(frags[0][0],(float,int))
  533     except:
  534         return False
  535 
  536 _FK_TEXT = 0
  537 _FK_IMG = 1
  538 _FK_APPEND = 2
  539 _FK_BREAK = 3
  540 
  541 def _getFragWords(frags,maxWidth=None):
  542     ''' given a Parafrag list return a list of fragwords
  543         [[size, (f00,w00), ..., (f0n,w0n)],....,[size, (fm0,wm0), ..., (f0n,wmn)]]
  544         each pair f,w represents a style and some string
  545         each sublist represents a word
  546     '''
  547     def _rescaleFrag(f):
  548         w = f[0]
  549         if isinstance(w,_PCT):
  550             if w._normalizer!=maxWidth:
  551                 w._normalizer = maxWidth
  552                 w = w.normalizedValue(maxWidth)
  553                 f[0] = w
  554     R = []
  555     aR = R.append
  556     W = []
  557     if _processed_frags(frags):
  558         aW = W.append
  559         for f in frags:
  560             _rescaleFrag(f)
  561             if isinstance(f,_SplitFrag):
  562                 f0 = f[0]
  563                 if not W:
  564                     W0t = type(f0)
  565                     Wlen = 0
  566                     sty = None
  567                 else:
  568                     if isinstance(lf,_SplitFragHY):
  569                         sty, t = W[-1]
  570                         Wlen -= stringWidth(t[-1],sty.fontName,sty.fontSize) + 1e-8
  571                         W[-1] = (sty,t[:-1]) #strip the '-'
  572                 Wlen += f0
  573                 for ts,t in f[1:]:
  574                     if ts is sty:
  575                         W[-1] = (sty,W[-1][1]+t)
  576                     else:
  577                         aW((ts,t))
  578                         sty = ts
  579                 #W.extend(f[1:])
  580                 lf = f          #latest f in W
  581                 continue
  582             else:
  583                 if W:
  584                     #must end a joining
  585                     aR((_HSFrag if isinstance(lf,_HSFrag) else list)([W0t(Wlen)]+W))
  586                     del W[:]
  587                 aR(f)
  588         if W:
  589             #must end a joining
  590             aR((_HSFrag if isinstance(lf,_HSFrag) else list)([W0t(Wlen)]+W))
  591     else:
  592         hangingSpace = False
  593         n = 0
  594         hangingStrip = True
  595         for f in frags:
  596             text = f.text
  597             if text!='':
  598                 f._fkind = _FK_TEXT
  599                 if hangingStrip:
  600                     text = lstrip(text)
  601                     if not text: continue
  602                     hangingStrip = False
  603                 S = split(text)
  604                 if text[0] in whitespace or not S:
  605                     if W:
  606                         W.insert(0,n)   #end preceding word
  607                         aR(W)
  608                         whs = hangingSpace
  609                         W = []
  610                         hangingSpace = False
  611                         n = 0
  612                     else:
  613                         whs = R and isinstance(R[-1],_HSFrag)
  614                     if not whs:
  615                         S.insert(0,'')
  616                     elif not S:
  617                         continue
  618 
  619                 for w in S[:-1]:
  620                     W.append((f,w))
  621                     n += stringWidth(w, f.fontName, f.fontSize)
  622                     W.insert(0,n)
  623                     aR(_HSFrag(W))
  624                     W = []
  625                     n = 0
  626 
  627                 hangingSpace = False
  628                 w = S[-1]
  629                 W.append((f,w))
  630                 n += stringWidth(w, f.fontName, f.fontSize)
  631                 if text and text[-1] in whitespace:
  632                     W.insert(0,n)
  633                     aR(_HSFrag(W))
  634                     W = []
  635                     n = 0
  636             elif hasattr(f,'cbDefn'):
  637                 cb = f.cbDefn
  638                 w = getattr(cb,'width',0)
  639                 if w:
  640                     if hasattr(w,'normalizedValue'):
  641                         w._normalizer = maxWidth
  642                         w = w.normalizedValue(maxWidth)
  643                     if W:
  644                         W.insert(0,n)
  645                         aR(_HSFrag(W) if hangingSpace else W)
  646                         W = []
  647                         hangingSpace = False
  648                         n = 0
  649                     f._fkind = _FK_IMG
  650                     aR([w,(f,'')])
  651                     hangingStrip = False
  652                 else:
  653                     f._fkind = _FK_APPEND
  654                     if not W and R and isinstance(R[-1],_HSFrag):
  655                         R[-1].append((f,''))
  656                     else:
  657                         W.append((f,''))
  658             elif hasattr(f, 'lineBreak'):
  659                 #pass the frag through.  The line breaker will scan for it.
  660                 if W:
  661                     W.insert(0,n)
  662                     aR(W)
  663                     W = []
  664                     n = 0
  665                     hangingSpace = False
  666                 f._fkind = _FK_BREAK
  667                 aR([0,(f,'')])
  668                 hangingStrip = True
  669 
  670         if W:
  671             W.insert(0,n)
  672             aR(W)
  673     if not R:
  674         if frags:
  675             f = frags[0]
  676             f._fkind = _FK_TEXT
  677             R = [[0,(f,u'')]]
  678 
  679     return R
  680 
  681 def _fragWordIter(w):
  682     for f, s in w[1:]:
  683         if hasattr(f,'cbDefn'):
  684             yield f, getattr(f,'width',0), s
  685         elif s:
  686             if isBytes(s):
  687                 s = s.decode('utf8')    #only encoding allowed
  688             for c in s:
  689                 yield f, stringWidth(c,f.fontName, f.fontSize), c
  690         else:
  691             yield f, 0, s
  692 
  693 def _splitFragWord(w,maxWidth,maxWidths,lineno):
  694     '''given a frag word, w, as returned by getFragWords
  695     split it into frag words that fit in lines of length
  696     maxWidth
  697     maxWidths[lineno+1]
  698     .....
  699     maxWidths[lineno+n]
  700 
  701     return the new word list which is either 
  702     _SplitFrag....._SPlitFrag or
  703     _SplitFrag....._SplitFragHS if the word is hanging space.
  704     '''
  705     R = []
  706     maxlineno = len(maxWidths)-1
  707     W = []
  708     lineWidth = 0
  709     fragText = u''
  710     wordWidth = 0
  711     f = w[1][0]
  712     for g,cw,c in _fragWordIter(w):
  713         newLineWidth = lineWidth+cw
  714         tooLong = newLineWidth>maxWidth
  715         if g is not f or tooLong:
  716             f = f.clone()
  717             if hasattr(f,'text'):
  718                 f.text = fragText
  719             W.append((f,fragText))
  720             if tooLong:
  721                 W = _SplitFrag([wordWidth]+W)
  722                 R.append(W)
  723                 lineno += 1
  724                 maxWidth = maxWidths[min(maxlineno,lineno)]
  725                 W = []
  726                 newLineWidth = cw
  727                 wordWidth = 0
  728             fragText = u''
  729             f = g
  730         wordWidth += cw
  731         fragText += c
  732         lineWidth = newLineWidth
  733     W.append((f,fragText))
  734     W = (_SplitFragHS if isinstance(w,_HSFrag) else _SplitFragH)([wordWidth]+W)
  735 
  736     R.append(W)
  737     return R
  738 
  739 
  740 #derived from Django validator
  741 #https://github.com/django/django/blob/master/django/core/validators.py
  742 uri_pat = re.compile(u'(^(?:[a-z0-9\\.\\-\\+]*)://)(?:\\S+(?::\\S*)?@)?(?:(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}|\\[[0-9a-f:\\.]+\\]|([a-z\xa1-\uffff0-9](?:[a-z\xa1-\uffff0-9-]{0,61}[a-z\xa1-\uffff0-9])?(?:\\.(?!-)[a-z\xa1-\uffff0-9-]{1,63}(?<!-))*\\.(?!-)(?:[a-z\xa1-\uffff-]{2,63}|xn--[a-z0-9]{1,59})(?<!-)\\.?|localhost))(?::\\d{2,5})?(?:[/?#][^\\s]*)?\\Z', re.I)
  743 
  744 def _slash_parts(uri,scheme,slash):
  745     tail = u''
  746     while uri.endswith(slash):
  747         tail += slash
  748         uri = uri[:-1]
  749 
  750     i = 2
  751     while True:
  752         i = uri.find(slash,i)
  753         if i<0: break
  754         i += 1
  755         yield scheme+uri[:i],uri[i:]+tail
  756 
  757 def _uri_split_pairs(uri):
  758     if isBytes(uri): uri = uri.decode('utf8')
  759     m = uri_pat.match(uri)
  760     if not m: return None
  761     scheme = m.group(1)
  762     uri = uri[len(scheme):]
  763 
  764     slash = (u'\\' if not scheme and u'/' not in uri #might be a microsoft pattern
  765             else u'/')
  766     R = ([(scheme, uri)] if scheme and uri else []) + list(_slash_parts(uri,scheme,slash))
  767     R.reverse()
  768     return R
  769 
  770 #valid letters determined by inspection of
  771 #    https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin_script
  772 _hy_letters=u'A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u024f\u1e80-\u1e85\u1e00-\u1eff\u0410-\u044f\u1e02\u1e03\u1e0a\u1e0b\u1e1e\u1e1f\u1e40\u1e41\u1e56\u1e57\u1e60\u1e61\u1e6a\u1e6b\u1e9b\u1ef2\u1ef3'
  773 #explicit hyphens
  774 _hy_shy = u'-\xad'
  775 
  776 _hy_pfx_pat = re.compile(u'^[\'"([{\xbf\u2018\u201a\u201c\u201e]+')
  777 _hy_sfx_pat = re.compile(u'[]\'")}?!.,;:\u2019\u201b\u201d\u201f]+$')
  778 _hy_letters_pat=re.compile(u''.join((u"^[",_hy_letters,u"]+$")))
  779 _hy_shy_letters_pat=re.compile(u''.join((u"^[",_hy_shy,_hy_letters,"]+$")))
  780 _hy_shy_pat = re.compile(u''.join((u"([",_hy_shy,u"])")))
  781 
  782 def _hyGenPair(hyphenator, s, ww, newWidth, maxWidth, fontName, fontSize, uriWasteReduce, embeddedHyphenation, hymwl):
  783     if isBytes(s): s = s.decode('utf8') #only encoding allowed
  784     m = _hy_pfx_pat.match(s)
  785     if m:
  786         pfx = m.group(0)
  787         s = s[len(pfx):]
  788     else:
  789         pfx = u''
  790     m = _hy_sfx_pat.search(s)
  791     if m:
  792         sfx = m.group(0)
  793         s = s[:-len(sfx)]
  794     else:
  795         sfx = u''
  796     if len(s) < hymwl: return
  797 
  798     w0 = newWidth - ww
  799     R = _uri_split_pairs(s)
  800     if R is not None:
  801         #a uri match was seen
  802         if ww>maxWidth or (uriWasteReduce and w0 <= (1-uriWasteReduce)*maxWidth):
  803             #we matched a uri and it makes sense to split
  804             for h, t in R:
  805                 h = pfx+h
  806                 t = t + sfx
  807                 hw = stringWidth(h,fontName,fontSize)
  808                 tw = w0 + hw
  809                 if tw<=maxWidth:
  810                     return u'',0,hw,ww-hw,h,t
  811         return
  812 
  813     H = _hy_shy_pat.split(s)
  814     if hyphenator and  (_hy_letters_pat.match(s) or (_hy_shy_letters_pat.match(s) and u'' not in H)):
  815         hylen = stringWidth(u'-',fontName,fontSize)
  816         for h,t in hyphenator(s):
  817             h = pfx + h
  818             if not _hy_shy_pat.match(h[-1]):
  819                 jc = u'-'
  820                 jclen = hylen
  821             else:
  822                 jc = u''
  823                 jclen = 0
  824             t = t + sfx
  825             hw = stringWidth(h,fontName,fontSize)
  826             tw = hw+w0 + jclen
  827             if tw<=maxWidth:
  828                 return jc,jclen,hw,ww-hw,h,t
  829 
  830     #even though the above tries for words with '-' it may be that no split ended with '-'
  831     #so this may succeed where the above does not
  832     n = len(H)
  833     if n>=3 and embeddedHyphenation and u'' not in H and _hy_shy_letters_pat.match(s):
  834         for i in reversed(xrange(2,n,2)):
  835             h = pfx + ''.join(H[:i])
  836             t = ''.join(H[i:]) + sfx
  837             hw = stringWidth(h,fontName,fontSize)
  838             tw = hw+w0
  839             if tw<=maxWidth:
  840                 return u'',0,hw,ww-hw,h,t
  841 
  842 def _fragWordSplitRep(FW):
  843     '''takes a frag word and assembles a unicode word from it
  844     if a rise is seen or a non-zerowidth cbdefn then we return
  845     None. Otherwise we return (uword,([i1,c1],[i2,c2],...])
  846     where each ii is the index of the word fragment in the word
  847     '''
  848     cc = plen = 0
  849     X = []
  850     eX = X.extend
  851     U = []
  852     aU = U.append
  853     for i in xrange(1,len(FW)):
  854         f, t = FW[i]
  855         if f.rise!=0: return None
  856         if hasattr(f,'cbDefn') and getattr(f.cbDefn,'width',0): return
  857         if not t: continue
  858         if isBytes(t): t = t.decode('utf8')
  859         aU(t)
  860         eX(len(t)*[(i,cc)])
  861         cc += len(t)
  862     return u''.join(U),tuple(X)
  863 
  864 def _rebuildFragWord(F):
  865     '''F are the frags'''
  866     return [sum((stringWidth(u,s.fontName,s.fontSize) for s,u in F))]+F
  867 
  868 def _hyGenFragsPair(hyphenator, FW, newWidth, maxWidth, uriWasteReduce, embeddedHyphenation, hymwl):
  869     X = _fragWordSplitRep(FW)
  870     if not X: return
  871     s, X = X
  872     if isBytes(s): s = s.decode('utf8') #only encoding allowed
  873     m = _hy_pfx_pat.match(s)
  874     if m:
  875         pfx = m.group(0)
  876         s = s[len(pfx):]
  877     else:
  878         pfx = u''
  879     m = _hy_sfx_pat.search(s)
  880     if m:
  881         sfx = m.group(0)
  882         s = s[:-len(sfx)]
  883     else:
  884         sfx = u''
  885     if len(s) < hymwl: return
  886     ww = FW[0]
  887     w0 = newWidth - ww
  888 
  889     #try for a uri
  890     R = _uri_split_pairs(s)
  891     if R is not None:
  892         #a uri match was seen
  893         if ww>maxWidth or (uriWasteReduce and w0 <= (1-uriWasteReduce)*maxWidth):
  894             #we matched a uri and it makes sense to split
  895             for h, t in R:
  896                 h = pfx+h
  897                 pos = len(h)
  898                 #FW[fx] is split
  899                 fx, cc = X[pos]
  900                 FL = FW[1:fx]
  901                 ffx, sfx = FW[fx]
  902                 sfxl = sfx[:pos-cc]
  903                 if sfxl: FL.append((ffx,sfxl))
  904                 sfxr = sfx[pos-cc:]
  905                 FR = FW[fx+1:]
  906                 if sfxr: FR.insert(0,(ffx,sfxr))
  907                 h = _rebuildFragWord(FL)
  908                 hw = h[0]
  909                 t = [ww-hw]+FR
  910                 tw = w0+hw
  911                 if tw<=maxWidth:
  912                     return u'',0,hw,ww-hw,h,t
  913         return
  914 
  915     H = _hy_shy_pat.split(s)
  916     if hyphenator and (_hy_letters_pat.match(s) or (_hy_shy_letters_pat.match(s) and u'' not in H)):
  917         #not too diffcult for now
  918         for h,t in hyphenator(s):
  919             h = pfx+h
  920             pos = len(h)
  921             #FW[fx] is split
  922             fx, cc = X[pos]
  923             FL = FW[1:fx]
  924             ffx, sfx = FW[fx]
  925             sfxl = sfx[:pos-cc]
  926             if not _hy_shy_pat.match(h[-1]):
  927                 jc = u'-'
  928             else:
  929                 jc = u''
  930             if sfxl or jc:
  931                 FL.append((ffx,sfxl+jc))
  932             sfxr = sfx[pos-cc:]
  933             FR = FW[fx+1:]
  934             if sfxr: FR.insert(0,(ffx,sfxr))
  935             h = _rebuildFragWord(FL)
  936             hw = h[0]
  937             ohw = stringWidth(sfxl,ffx.fontName,ffx.fontSize)
  938             t = [ww-ohw]+FR
  939             tw = w0+hw
  940             if tw<=maxWidth:
  941                 return jc,hw-ohw,hw,ww-ohw,h,t
  942 
  943     #even though the above tries for words with '-' it may be that no split ended with '-'
  944     #so this may succeed where the above does not
  945     n = len(H)
  946     if n>=3 and embeddedHyphenation and u'' not in H and _hy_shy_letters_pat.match(s):
  947         for i in reversed(xrange(2,n,2)):
  948             pos = len(pfx + u''.join(H[:i]))
  949             fx, cc = X[pos]
  950             #FW[fx] is split
  951             FL = FW[1:fx]
  952             ffx, sfx = FW[fx]
  953             sfxl = sfx[:pos-cc]
  954             if sfxl: FL.append((ffx,sfxl))
  955             sfxr = sfx[pos-cc:]
  956             FR = FW[fx+1:]
  957             if sfxr: FR.insert(0,(ffx,sfxr))
  958             h = _rebuildFragWord(FL)
  959             hw = h[0]
  960             t = [ww-hw]+FR
  961             tw = w0+hw
  962             if tw<=maxWidth:
  963                 return u'',0,hw,ww-hw,h,t
  964 
  965 def _hyphenateFragWord(hyphenator,FW,newWidth,maxWidth,uriWasteReduce,embeddedHyphenation,
  966                         hymwl=hyphenationMinWordLength):
  967     ww = FW[0]
  968     if ww==0: return []
  969     if len(FW)==2:
  970         f, s = FW[1]
  971         R = _hyGenPair(hyphenator, s, ww, newWidth, maxWidth, f.fontName, f.fontSize,uriWasteReduce,embeddedHyphenation, hymwl)
  972         if R:
  973             jc, hylen, hw, tw, h, t = R
  974             return [(_SplitFragHY if jc else _SplitFragH)([hw+hylen,(f,h+jc)]),(_SplitFragHS if isinstance(FW,_HSFrag) else _SplitFrag)([tw,(f,t)])]
  975     else:
  976         R = _hyGenFragsPair(hyphenator, FW, newWidth, maxWidth,uriWasteReduce,embeddedHyphenation, hymwl)
  977         if R:
  978             jc, hylen, hw, tw, h, t = R
  979             return [(_SplitFragHY if jc else _SplitFragH)(h),(_SplitFragHS if isinstance(FW,_HSFrag) else _SplitFrag)(t)]
  980 
  981     return None
  982 
  983 class _SplitWord(unicodeT):
  984     pass
  985 
  986 class _SplitWordH(unicodeT):
  987     pass
  988 
  989 class _SplitWordHY(_SplitWordH):
  990     '''head part of a hyphenation word pair'''
  991     pass
  992 
  993 def _hyphenateWord(hyphenator,fontName,fontSize,w,ww,newWidth,maxWidth, uriWasteReduce,embeddedHyphenation,
  994                     hymwl=hyphenationMinWordLength):
  995     if ww==0: return []
  996     R = _hyGenPair(hyphenator, w, ww, newWidth, maxWidth, fontName, fontSize, uriWasteReduce,embeddedHyphenation, hymwl)
  997     if R:
  998         hy, hylen, hw, tw, h, t = R
  999         return [(_SplitWordHY if hy else _SplitWordH)(h+hy),_SplitWord(t)]
 1000 
 1001 def _splitWord(w,maxWidth,maxWidths,lineno,fontName,fontSize,encoding='utf8'):
 1002     '''
 1003     split w into words that fit in lines of length
 1004     maxWidth
 1005     maxWidths[lineno+1]
 1006     .....
 1007     maxWidths[lineno+n]
 1008 
 1009     then push those new words onto words
 1010     '''
 1011     #TODO fix this to use binary search for the split points
 1012     R = []
 1013     aR = R.append
 1014     maxlineno = len(maxWidths)-1
 1015     lineWidth = 0
 1016     wordText = u''
 1017     if isBytes(w):
 1018         w = w.decode(encoding)
 1019     for c in w:
 1020         cw = stringWidth(c,fontName,fontSize,encoding)
 1021         newLineWidth = lineWidth+cw
 1022         if newLineWidth>maxWidth:
 1023             aR(_SplitWord(wordText))
 1024             lineno += 1
 1025             maxWidth = maxWidths[min(maxlineno,lineno)]
 1026             newLineWidth = cw
 1027             wordText = u''
 1028         wordText += c
 1029         lineWidth = newLineWidth
 1030     aR(_SplitWord(wordText))
 1031     return [c for c in R if c]
 1032 
 1033 def _yieldBLParaWords(blPara,start,stop):
 1034     state = 0
 1035     R = []
 1036     aR = R.append
 1037     for l in blPara.lines[start:stop]:
 1038         for w in l[1]:
 1039             if isinstance(w,_SplitWord):
 1040                 if R and isinstance(R[-1],_SplitWordHY):
 1041                     R[-1] = R[-1][:-1]  #remove unwanted -
 1042                 aR(w)
 1043                 continue
 1044             else:
 1045                 if R:
 1046                     yield ''.join(R)
 1047                     del R[:]
 1048             yield w
 1049     if R:
 1050         yield ''.join(R)
 1051 
 1052 def _split_blParaSimple(blPara,start,stop):
 1053     f = blPara.clone()
 1054     for a in ('lines', 'kind', 'text'):
 1055         if hasattr(f,a): delattr(f,a)
 1056     f.words = list(_yieldBLParaWords(blPara,start,stop))
 1057     return [f]
 1058 
 1059 def _split_blParaHard(blPara,start,stop):
 1060     f = []
 1061     lines = blPara.lines[start:stop]
 1062     for l in lines:
 1063         for w in l.words:
 1064             f.append(w)
 1065         if l is not lines[-1]:
 1066             i = len(f)-1
 1067             while i>=0 and hasattr(f[i],'cbDefn') and not getattr(f[i].cbDefn,'width',0): i -= 1
 1068             if i>=0:
 1069                 g = f[i]
 1070                 if not g.text: g.text = ' '
 1071                 elif g.text[-1]!=' ': g.text += ' '
 1072     return f
 1073 
 1074 def _drawBullet(canvas, offset, cur_y, bulletText, style, rtl):
 1075     '''draw a bullet text could be a simple string or a frag list'''
 1076     bulletAnchor = style.bulletAnchor
 1077     if rtl or style.bulletAnchor!='start':
 1078         numeric = bulletAnchor=='numeric'
 1079         if isStr(bulletText):
 1080             t =  bulletText
 1081             q = numeric and decimalSymbol in t
 1082             if q: t = t[:t.index(decimalSymbol)]
 1083             bulletWidth = stringWidth(t, style.bulletFontName, style.bulletFontSize)
 1084             if q: bulletWidth += 0.5 * stringWidth(decimalSymbol, style.bulletFontName, style.bulletFontSize)
 1085         else:
 1086             #it's a list of fragments
 1087             bulletWidth = 0
 1088             for f in bulletText:
 1089                 t = f.text
 1090                 q = numeric and decimalSymbol in t
 1091                 if q:
 1092                     t = t[:t.index(decimalSymbol)]
 1093                     bulletWidth += 0.5 * stringWidth(decimalSymbol, f.fontName, f.fontSize)
 1094                 bulletWidth += stringWidth(t, f.fontName, f.fontSize)
 1095                 if q:
 1096                     break
 1097     else:
 1098         bulletWidth = 0
 1099     if bulletAnchor=='middle': bulletWidth *= 0.5
 1100     cur_y += getattr(style,"bulletOffsetY",0)
 1101     if not rtl:
 1102         tx2 = canvas.beginText(style.bulletIndent-bulletWidth,cur_y)
 1103     else:
 1104         width = rtl[0]
 1105         bulletStart = width+style.rightIndent-(style.bulletIndent+bulletWidth)
 1106         tx2 = canvas.beginText(bulletStart, cur_y)
 1107     tx2.setFont(style.bulletFontName, style.bulletFontSize)
 1108     tx2.setFillColor(getattr(style,'bulletColor',style.textColor))
 1109     if isStr(bulletText):
 1110         tx2.textOut(bulletText)
 1111     else:
 1112         for f in bulletText:
 1113             tx2.setFont(f.fontName, f.fontSize)
 1114             tx2.setFillColor(f.textColor)
 1115             tx2.textOut(f.text)
 1116 
 1117     canvas.drawText(tx2)
 1118     if not rtl:
 1119         #AR making definition lists a bit less ugly
 1120         #bulletEnd = tx2.getX()
 1121         bulletEnd = tx2.getX() + style.bulletFontSize * 0.6
 1122         offset = max(offset,bulletEnd - style.leftIndent)
 1123     return offset
 1124 
 1125 def _handleBulletWidth(bulletText,style,maxWidths):
 1126     '''work out bullet width and adjust maxWidths[0] if neccessary
 1127     '''
 1128     if bulletText:
 1129         if isStr(bulletText):
 1130             bulletWidth = stringWidth( bulletText, style.bulletFontName, style.bulletFontSize)
 1131         else:
 1132             #it's a list of fragments
 1133             bulletWidth = 0
 1134             for f in bulletText:
 1135                 bulletWidth += stringWidth(f.text, f.fontName, f.fontSize)
 1136         bulletLen = style.bulletIndent + bulletWidth + 0.6 * style.bulletFontSize
 1137         if style.wordWrap=='RTL':
 1138             indent = style.rightIndent+style.firstLineIndent
 1139         else:
 1140             indent = style.leftIndent+style.firstLineIndent
 1141         if bulletLen > indent:
 1142             #..then it overruns, and we have less space available on line 1
 1143             maxWidths[0] -= (bulletLen - indent)
 1144 
 1145 def splitLines0(frags,widths):
 1146     '''
 1147     given a list of ParaFrags we return a list of ParaLines
 1148 
 1149     each ParaLine has
 1150     1)  ExtraSpace
 1151     2)  blankCount
 1152     3)  [textDefns....]
 1153     each text definition is a (ParaFrag, start, limit) triplet
 1154     '''
 1155     #initialise the algorithm
 1156     lines   = []
 1157     lineNum = 0
 1158     maxW    = widths[lineNum]
 1159     i       = -1
 1160     l       = len(frags)
 1161     lim     = start = 0
 1162     while 1:
 1163         #find a non whitespace character
 1164         while i<l:
 1165             while start<lim and text[start]==' ': start += 1
 1166             if start==lim:
 1167                 i += 1
 1168                 if i==l: break
 1169                 start = 0
 1170                 f = frags[i]
 1171                 text = f.text
 1172                 lim = len(text)
 1173             else:
 1174                 break   # we found one
 1175 
 1176         if start==lim: break    #if we didn't find one we are done
 1177 
 1178         #start of a line
 1179         g       = (None,None,None)
 1180         line    = []
 1181         cLen    = 0
 1182         nSpaces = 0
 1183         while cLen<maxW:
 1184             j = text.find(' ',start)
 1185             if j<0: j==lim
 1186             w = stringWidth(text[start:j],f.fontName,f.fontSize)
 1187             cLen += w
 1188             if cLen>maxW and line!=[]:
 1189                 cLen = cLen-w
 1190                 #this is the end of the line
 1191                 while g.text[lim]==' ':
 1192                     lim = lim - 1
 1193                     nSpaces = nSpaces-1
 1194                 break
 1195             if j<0: j = lim
 1196             if g[0] is f: g[2] = j  #extend
 1197             else:
 1198                 g = (f,start,j)
 1199                 line.append(g)
 1200             if j==lim:
 1201                 i += 1
 1202 
 1203 def _do_line(tx, x1, y1, x2, y2, nlw, nsc):
 1204     canv = tx._canvas
 1205     olw = canv._lineWidth
 1206     if nlw!=olw:
 1207         canv.setLineWidth(nlw)
 1208     osc = canv._strokeColorObj
 1209     if nsc!=osc:
 1210         canv.setStrokeColor(nsc)
 1211     canv.line(x1, y1, x2, y2)
 1212 
 1213 def _do_under_line(i, x1, ws, tx, us_lines):
 1214     xs = tx.XtraState
 1215     style = xs.style
 1216     y0 = xs.cur_y - i*style.leading
 1217     f = xs.f
 1218     fs = f.fontSize
 1219     tc = f.textColor
 1220     values = dict(L=fs,F=fs,f=fs)
 1221     dw = tx._defaultLineWidth
 1222     x2 = x1 + tx._canvas.stringWidth(' '.join(tx.XtraState.lines[i][1]), tx._fontname, fs)
 1223     for n,k,c,w,o,r,m,g in us_lines:
 1224         underline = k=='underline'
 1225         lw = _usConv(w,values,default=tx._defaultLineWidth)
 1226         lg = _usConv(g,values,default=1)
 1227         dy = lg+lw
 1228         if not underline: dy = -dy
 1229         y = y0 + r + _usConv(('-0.125*L' if underline else '0.25*L') if o=='' else o,values)
 1230         if not c: c = tc
 1231         while m>0:
 1232             tx._do_line(x1, y, x2, y, lw, c)
 1233             y -= dy
 1234             m -= 1
 1235 
 1236 _scheme_re = re.compile('^[a-zA-Z][-+a-zA-Z0-9]+$')
 1237 def _doLink(tx,link,rect):
 1238     if not link: return
 1239     if link.startswith('#'):
 1240         tx._canvas.linkRect("", link[1:], rect, relative=1)
 1241     else:
 1242         parts = link.split(':',1)
 1243         scheme = len(parts)==2 and parts[0].lower() or ''
 1244         if scheme=='document':
 1245             tx._canvas.linkRect("", parts[1], rect, relative=1)
 1246         elif _scheme_re.match(scheme):
 1247             kind=scheme.lower()=='pdf' and 'GoToR' or 'URI'
 1248             if kind=='GoToR': link = parts[1]
 1249             tx._canvas.linkURL(link, rect, relative=1, kind=kind)
 1250         else:
 1251             tx._canvas.linkURL(link, rect, relative=1, kind='URI')
 1252 
 1253 def _do_link_line(i, t_off, ws, tx):
 1254     xs = tx.XtraState
 1255     leading = xs.style.leading
 1256     y = xs.cur_y - i*leading - xs.f.fontSize/8.0 # 8.0 factor copied from para.py
 1257     text = ' '.join(xs.lines[i][1])
 1258     textlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize)
 1259     for n, link in xs.link:
 1260         _doLink(tx, link, (t_off, y, t_off+textlen, y+leading))
 1261 
 1262 def _do_post_text(tx):
 1263     xs = tx.XtraState
 1264     y0 = xs.cur_y
 1265     f = xs.f
 1266     leading = xs.style.leading
 1267     autoLeading = xs.autoLeading
 1268     fontSize = f.fontSize
 1269     if autoLeading=='max':
 1270         leading = max(leading,1.2*fontSize)
 1271     elif autoLeading=='min':
 1272         leading = 1.2*fontSize
 1273 
 1274     if xs.backColors:
 1275         yl = y0 + fontSize
 1276         ydesc = yl - leading
 1277 
 1278         for x1,x2,c in xs.backColors:
 1279             tx._canvas.setFillColor(c)
 1280             tx._canvas.rect(x1,ydesc,x2-x1,leading,stroke=0,fill=1)
 1281         xs.backColors=[]
 1282         xs.backColor=None
 1283 
 1284     for (((n,link),x1),lo,hi),x2 in sorted(xs.links.values()):
 1285         _doLink(tx, link, (x1, y0+lo, x2, y0+hi))
 1286     xs.links = {}
 1287 
 1288     if xs.us_lines:
 1289         #print 'lines'
 1290         dw = tx._defaultLineWidth
 1291         values = dict(L=fontSize)
 1292         for (((n,k,c,w,o,r,m,g),fs,tc,x1),fsmax),x2 in sorted(xs.us_lines.values()):
 1293             underline = k=='underline'
 1294             values['f'] = fs
 1295             values['F'] = fsmax
 1296             lw = _usConv(w,values,default=tx._defaultLineWidth)
 1297             lg = _usConv(g,values,default=1)
 1298             dy = lg+lw
 1299             if not underline: dy = -dy
 1300             y = y0 + r + _usConv(o if o!='' else ('-0.125*L' if underline else '0.25*L'),values)
 1301             #print 'n=%s k=%s x1=%s x2=%s r=%s c=%s w=%r o=%r fs=%r tc=%s y=%s lw=%r offs=%r' % (n,k,x1,x2,r,(c.hexval() if c else ''),w,o,fs,tc.hexval(),y,lw,y-y0-r)
 1302             if not c: c = tc
 1303             while m>0:
 1304                 tx._do_line(x1, y, x2, y, lw, c)
 1305                 y -= dy
 1306                 m -= 1
 1307         xs.us_lines = {}
 1308 
 1309     xs.cur_y -= leading
 1310 
 1311 def textTransformFrags(frags,style):
 1312     tt = style.textTransform
 1313     if tt:
 1314         tt=tt.lower()
 1315         if tt=='lowercase':
 1316             tt = unicodeT.lower
 1317         elif tt=='uppercase':
 1318             tt = unicodeT.upper
 1319         elif  tt=='capitalize':
 1320             tt = unicodeT.title
 1321         elif tt=='none':
 1322             return
 1323         else:
 1324             raise ValueError('ParaStyle.textTransform value %r is invalid' % style.textTransform)
 1325         n = len(frags)
 1326         if n==1:
 1327             #single fragment the easy case
 1328             frags[0].text = tt(frags[0].text)
 1329         elif tt is unicodeT.title:
 1330             pb = True
 1331             for f in frags:
 1332                 u = f.text
 1333                 if not u: continue
 1334                 if u.startswith(u' ') or pb:
 1335                     u = tt(u)
 1336                 else:
 1337                     i = u.find(u' ')
 1338                     if i>=0:
 1339                         u = u[:i]+tt(u[i:])
 1340                 pb = u.endswith(u' ')
 1341                 f.text = u
 1342         else:
 1343             for f in frags:
 1344                 u = f.text
 1345                 if not u: continue
 1346                 f.text = tt(u)
 1347 
 1348 class cjkU(unicodeT):
 1349     '''simple class to hold the frag corresponding to a str'''
 1350     def __new__(cls,value,frag,encoding):
 1351         self = unicodeT.__new__(cls,value)
 1352         self._frag = frag
 1353         if hasattr(frag,'cbDefn'):
 1354             w = getattr(frag.cbDefn,'width',0)
 1355             self._width = w
 1356         else:
 1357             self._width = stringWidth(value,frag.fontName,frag.fontSize)
 1358         return self
 1359     frag = property(lambda self: self._frag)
 1360     width = property(lambda self: self._width)
 1361 
 1362 def makeCJKParaLine(U,maxWidth,widthUsed,extraSpace,lineBreak,calcBounds):
 1363     words = []
 1364     CW = []
 1365     f0 = FragLine()
 1366     maxSize = maxAscent = minDescent = 0
 1367     for u in U:
 1368         f = u.frag
 1369         fontSize = f.fontSize
 1370         if calcBounds:
 1371             cbDefn = getattr(f,'cbDefn',None)
 1372             if getattr(cbDefn,'width',0):
 1373                 descent, ascent = imgVRange(imgNormV(cbDefn.height,fontSize),cbDefn.valign,fontSize)
 1374             else:
 1375                 ascent, descent = getAscentDescent(f.fontName,fontSize)
 1376         else:
 1377             ascent, descent = getAscentDescent(f.fontName,fontSize)
 1378         maxSize = max(maxSize,fontSize)
 1379         maxAscent = max(maxAscent,ascent)
 1380         minDescent = min(minDescent,descent)
 1381         if not sameFrag(f0,f):
 1382             f0=f0.clone()
 1383             f0.text = u''.join(CW)
 1384             words.append(f0)
 1385             CW = []
 1386             f0 = f
 1387         CW.append(u)
 1388     if CW:
 1389         f0=f0.clone()
 1390         f0.text = u''.join(CW)
 1391         words.append(f0)
 1392     return FragLine(kind=1,extraSpace=extraSpace,wordCount=1,words=words[1:],fontSize=maxSize,ascent=maxAscent,descent=minDescent,maxWidth=maxWidth,currentWidth=widthUsed,lineBreak=lineBreak)
 1393 
 1394 def cjkFragSplit(frags, maxWidths, calcBounds, encoding='utf8'):
 1395     '''This attempts to be wordSplit for frags using the dumb algorithm'''
 1396     U = []  #get a list of single glyphs with their widths etc etc
 1397     for f in frags:
 1398         text = f.text
 1399         if isBytes(text):
 1400             text = text.decode(encoding)
 1401         if text:
 1402             U.extend([cjkU(t,f,encoding) for t in text])
 1403         else:
 1404             U.append(cjkU(text,f,encoding))
 1405     lines = []
 1406     i = widthUsed = lineStartPos = 0
 1407     maxWidth = maxWidths[0]
 1408     nU = len(U)
 1409     while i<nU:
 1410         u = U[i]
 1411         i += 1
 1412         w = u.width
 1413         if hasattr(w,'normalizedValue'):
 1414             w._normalizer = maxWidth
 1415             w = w.normalizedValue(maxWidth)
 1416         widthUsed += w
 1417         lineBreak = hasattr(u.frag,'lineBreak')
 1418         endLine = (widthUsed>maxWidth + _FUZZ and widthUsed>0) or lineBreak
 1419         if endLine:
 1420             extraSpace = maxWidth - widthUsed
 1421             if not lineBreak:
 1422                 if ord(u)<0x3000:
 1423                     # we appear to be inside a non-Asian script section.
 1424                     # (this is a very crude test but quick to compute).
 1425                     # This is likely to be quite rare so the speed of the
 1426                     # code below is hopefully not a big issue.  The main
 1427                     # situation requiring this is that a document title
 1428                     # with an english product name in it got cut.
 1429 
 1430 
 1431                     # we count back and look for
 1432                     #  - a space-like character
 1433                     #  - reversion to Kanji (which would be a good split point)
 1434                     #  - in the worst case, roughly half way back along the line
 1435                     limitCheck = (lineStartPos+i)>>1        #(arbitrary taste issue)
 1436                     for j in xrange(i-1,limitCheck,-1):
 1437                         uj = U[j]
 1438                         if uj and category(uj)=='Zs' or ord(uj)>=0x3000:
 1439                             k = j+1
 1440                             if k<i:
 1441                                 j = k+1
 1442                                 extraSpace += sum(U[ii].width for ii in xrange(j,i))
 1443                                 w = U[k].width
 1444                                 u = U[k]
 1445                                 i = j
 1446                                 break
 1447 
 1448                 #we are pushing this character back, but
 1449                 #the most important of the Japanese typography rules
 1450                 #if this character cannot start a line, wrap it up to this line so it hangs
 1451                 #in the right margin. We won't do two or more though - that's unlikely and
 1452                 #would result in growing ugliness.
 1453                 #and increase the extra space
 1454                 #bug fix contributed by Alexander Vasilenko <alexs.vasilenko@gmail.com>
 1455                 if u not in ALL_CANNOT_START and i>lineStartPos+1:
 1456                     #otherwise we need to push the character back
 1457                     #the i>lineStart+1 condition ensures progress
 1458                     i -= 1
 1459                     extraSpace += w
 1460             lines.append(makeCJKParaLine(U[lineStartPos:i],maxWidth,widthUsed,extraSpace,lineBreak,calcBounds))
 1461             try:
 1462                 maxWidth = maxWidths[len(lines)]
 1463             except IndexError:
 1464                 maxWidth = maxWidths[-1]  # use the last one
 1465 
 1466             lineStartPos = i
 1467             widthUsed = 0
 1468 
 1469     #any characters left?
 1470     if widthUsed > 0:
 1471         lines.append(makeCJKParaLine(U[lineStartPos:],maxWidth,widthUsed,maxWidth-widthUsed,False,calcBounds))
 1472 
 1473     return ParaLines(kind=1,lines=lines)
 1474 
 1475 class Paragraph(Flowable):
 1476     """ Paragraph(text, style, bulletText=None, caseSensitive=1)
 1477         text a string of stuff to go into the paragraph.
 1478         style is a style definition as in reportlab.lib.styles.
 1479         bulletText is an optional bullet defintion.
 1480         caseSensitive set this to 0 if you want the markup tags and their attributes to be case-insensitive.
 1481 
 1482         This class is a flowable that can format a block of text
 1483         into a paragraph with a given style.
 1484 
 1485         The paragraph Text can contain XML-like markup including the tags:
 1486         <b> ... </b> - bold
 1487         < u [color="red"] [width="pts"] [offset="pts"]> < /u > - underline
 1488             width and offset can be empty meaning use existing canvas line width
 1489             or with an f/F suffix regarded as a fraction of the font size
 1490         < strike > < /strike > - strike through has the same parameters as underline
 1491         <i> ... </i> - italics
 1492         <u> ... </u> - underline
 1493         <strike> ... </strike> - strike through
 1494         <super> ... </super> - superscript
 1495         <sub> ... </sub> - subscript
 1496         <font name=fontfamily/fontname color=colorname size=float>
 1497         <span name=fontfamily/fontname color=colorname backcolor=colorname size=float style=stylename>
 1498         <onDraw name=callable label="a label"/>
 1499         <index [name="callablecanvasattribute"] label="a label"/>
 1500         <link>link text</link>
 1501             attributes of links
 1502                 size/fontSize/uwidth/uoffset=num
 1503                 name/face/fontName=name
 1504                 fg/textColor/color/ucolor=color
 1505                 backcolor/backColor/bgcolor=color
 1506                 dest/destination/target/href/link=target
 1507                 underline=bool turn on underline
 1508         <a>anchor text</a>
 1509             attributes of anchors
 1510                 size/fontSize/uwidth/uoffset=num
 1511                 fontName=name
 1512                 fg/textColor/color/ucolor=color
 1513                 backcolor/backColor/bgcolor=color
 1514                 href=href
 1515                 underline="yes|no"
 1516         <a name="anchorpoint"/>
 1517         <unichar name="unicode character name"/>
 1518         <unichar value="unicode code point"/>
 1519         <img src="path" width="1in" height="1in" valign="bottom"/>
 1520                 width="w%" --> fontSize*w/100   idea from Roberto Alsina
 1521                 height="h%" --> linewidth*h/100 <ralsina@netmanagers.com.ar>
 1522 
 1523         The whole may be surrounded by <para> </para> tags
 1524 
 1525         The <b> and <i> tags will work for the built-in fonts (Helvetica
 1526         /Times / Courier).  For other fonts you need to register a family
 1527         of 4 fonts using reportlab.pdfbase.pdfmetrics.registerFont; then
 1528         use the addMapping function to tell the library that these 4 fonts
 1529         form a family e.g.
 1530         from reportlab.lib.fonts import addMapping
 1531         addMapping('Vera', 0, 0, 'Vera')    #normal
 1532         addMapping('Vera', 0, 1, 'Vera-Italic')    #italic
 1533         addMapping('Vera', 1, 0, 'Vera-Bold')    #bold
 1534         addMapping('Vera', 1, 1, 'Vera-BoldItalic')    #italic and bold
 1535 
 1536         It will also be able to handle any MathML specified Greek characters.
 1537     """
 1538     def __init__(self, text, style, bulletText = None, frags=None, caseSensitive=1, encoding='utf8'):
 1539         self.caseSensitive = caseSensitive
 1540         self.encoding = encoding
 1541         self._setup(text, style, bulletText or getattr(style,'bulletText',None), frags, cleanBlockQuotedText)
 1542 
 1543 
 1544     def __repr__(self):
 1545         n = self.__class__.__name__
 1546         L = [n+"("]
 1547         keys = list(self.__dict__.keys())
 1548         for k in keys:
 1549             L.append('%s: %s' % (repr(k).replace("\n", " ").replace("  "," "),repr(getattr(self, k)).replace("\n", " ").replace("  "," ")))
 1550         L.append(") #"+n)
 1551         return '\n'.join(L)
 1552 
 1553     def _setup(self, text, style, bulletText, frags, cleaner):
 1554 
 1555         #This used to be a global parser to save overhead.
 1556         #In the interests of thread safety it is being instantiated per paragraph.
 1557         #On the next release, we'll replace with a cElementTree parser
 1558 
 1559         if frags is None:
 1560             text = cleaner(text)
 1561             _parser = ParaParser()
 1562             _parser.caseSensitive = self.caseSensitive
 1563             style, frags, bulletTextFrags = _parser.parse(text,style)
 1564             if frags is None:
 1565                 raise ValueError("xml parser error (%s) in paragraph beginning\n'%s'"\
 1566                     % (_parser.errors[0],text[:min(30,len(text))]))
 1567             textTransformFrags(frags,style)
 1568             if bulletTextFrags: bulletText = bulletTextFrags
 1569 
 1570         #AR hack
 1571         self.text = text
 1572         self.frags = frags  #either the parse fragments or frag word list
 1573         self.style = style
 1574         self.bulletText = bulletText
 1575         self.debug = 0  #turn this on to see a pretty one with all the margins etc.
 1576 
 1577     def wrap(self, availWidth, availHeight):
 1578         if availWidth<_FUZZ:
 1579             #we cannot fit here
 1580             return 0, 0x7fffffff
 1581         # work out widths array for breaking
 1582         self.width = availWidth
 1583         style = self.style
 1584         leftIndent = style.leftIndent
 1585         first_line_width = availWidth - (leftIndent+style.firstLineIndent) - style.rightIndent
 1586         later_widths = availWidth - leftIndent - style.rightIndent
 1587         self._wrapWidths = [first_line_width, later_widths]
 1588         if style.wordWrap == 'CJK':
 1589             #use Asian text wrap algorithm to break characters
 1590             blPara = self.breakLinesCJK(self._wrapWidths)
 1591         else:
 1592             blPara = self.breakLines(self._wrapWidths)
 1593         self.blPara = blPara
 1594         autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
 1595         leading = style.leading
 1596         if blPara.kind==1:
 1597             if autoLeading not in ('','off'):
 1598                 height = 0
 1599                 if autoLeading=='max':
 1600                     for l in blPara.lines:
 1601                         height += max(l.ascent-l.descent,leading)
 1602                 elif autoLeading=='min':
 1603                     for l in blPara.lines:
 1604                         height += l.ascent - l.descent
 1605                 else:
 1606                     raise ValueError('invalid autoLeading value %r' % autoLeading)
 1607             else:
 1608                 height = len(blPara.lines) * leading
 1609         else:
 1610             if autoLeading=='max':
 1611                 leading = max(leading,blPara.ascent-blPara.descent)
 1612             elif autoLeading=='min':
 1613                 leading = blPara.ascent-blPara.descent
 1614             height = len(blPara.lines) * leading
 1615         self.height = height
 1616         return self.width, height
 1617 
 1618     def minWidth(self):
 1619         'Attempt to determine a minimum sensible width'
 1620         frags = self.frags
 1621         nFrags= len(frags)
 1622         if not nFrags: return 0
 1623         if nFrags==1 and not _processed_frags(frags):
 1624             f = frags[0]
 1625             fS = f.fontSize
 1626             fN = f.fontName
 1627             return max(stringWidth(w,fN,fS) for w in (split(f.text, ' ') if hasattr(f,'text') else f.words))
 1628         else:
 1629             return max(w[0] for w in _getFragWords(frags))
 1630 
 1631     def _split_blParaProcessed(self,blPara,start,stop):
 1632         if not stop: return []
 1633         lines = blPara.lines
 1634         sFW = lines[start].sFW
 1635         sFWN = lines[stop].sFW if stop!=len(lines) else len(self.frags)
 1636         return self.frags[sFW:sFWN]
 1637 
 1638     def _get_split_blParaFunc(self):
 1639         return (_split_blParaSimple if self.blPara.kind==0 
 1640                     else (_split_blParaHard if not _processed_frags(self.frags)
 1641                         else self._split_blParaProcessed))
 1642 
 1643     def split(self,availWidth, availHeight):
 1644         if len(self.frags)<=0 or availWidth<_FUZZ or availHeight<_FUZZ: return []
 1645 
 1646         #the split information is all inside self.blPara
 1647         if not hasattr(self,'blPara'):
 1648             self.wrap(availWidth,availHeight)
 1649         blPara = self.blPara
 1650         style = self.style
 1651         autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
 1652         leading = style.leading
 1653         lines = blPara.lines
 1654         if blPara.kind==1 and autoLeading not in ('','off'):
 1655             s = height = 0
 1656             if autoLeading=='max':
 1657                 for i,l in enumerate(blPara.lines):
 1658                     h = max(l.ascent-l.descent,leading)
 1659                     n = height+h
 1660                     if n>availHeight+1e-8:
 1661                         break
 1662                     height = n
 1663                     s = i+1
 1664             elif autoLeading=='min':
 1665                 for i,l in enumerate(blPara.lines):
 1666                     n = height+l.ascent-l.descent
 1667                     if n>availHeight+1e-8:
 1668                         break
 1669                     height = n
 1670                     s = i+1
 1671             else:
 1672                 raise ValueError('invalid autoLeading value %r' % autoLeading)
 1673         else:
 1674             l = leading
 1675             if autoLeading=='max':
 1676                 l = max(leading,1.2*style.fontSize)
 1677             elif autoLeading=='min':
 1678                 l = 1.2*style.fontSize
 1679             s = int(availHeight/float(l))
 1680             height = s*l
 1681 
 1682         allowOrphans = getattr(self,'allowOrphans',getattr(style,'allowOrphans',0))
 1683         if (not allowOrphans and s<=1) or s==0: #orphan or not enough room
 1684             del self.blPara
 1685             return []
 1686         n = len(lines)
 1687         allowWidows = getattr(self,'allowWidows',getattr(style,'allowWidows',1))
 1688         if n<=s:
 1689             return [self]
 1690         if not allowWidows:
 1691             if n==s+1: #widow?
 1692                 if (allowOrphans and n==3) or n>3:
 1693                     s -= 1  #give the widow some company
 1694                 else:
 1695                     del self.blPara #no room for adjustment; force the whole para onwards
 1696                     return []
 1697         func = self._get_split_blParaFunc()
 1698 
 1699         if style.endDots:
 1700             style1 = deepcopy(style)
 1701             style1.endDots = None
 1702         else:
 1703             style1 = style
 1704         P1=self.__class__(None,style1,bulletText=self.bulletText,frags=func(blPara,0,s))
 1705         #this is a major hack
 1706         P1.blPara = ParaLines(kind=1,lines=blPara.lines[0:s],aH=availHeight,aW=availWidth)
 1707         P1._JustifyLast = 1
 1708         P1._splitpara = 1
 1709         P1.height = height
 1710         P1.width = availWidth
 1711         if style.firstLineIndent != 0:
 1712             style = deepcopy(style)
 1713             style.firstLineIndent = 0
 1714         P2=self.__class__(None,style,bulletText=None,frags=func(blPara,s,n))
 1715         #propagate attributes that might be on self; suggestion from Dirk Holtwick
 1716         for a in ('autoLeading',    #possible attributes that might be directly on self.
 1717                 ):
 1718             if hasattr(self,a):
 1719                 setattr(P1,a,getattr(self,a))
 1720                 setattr(P2,a,getattr(self,a))
 1721 
 1722         return [P1,P2]
 1723 
 1724     def draw(self):
 1725         #call another method for historical reasons.  Besides, I
 1726         #suspect I will be playing with alternate drawing routines
 1727         #so not doing it here makes it easier to switch.
 1728         self.drawPara(self.debug)
 1729 
 1730     def breakLines(self, width):
 1731         """
 1732         Returns a broken line structure. There are two cases
 1733 
 1734         A) For the simple case of a single formatting input fragment the output is
 1735             A fragment specifier with
 1736                 - kind = 0
 1737                 - fontName, fontSize, leading, textColor
 1738                 - lines=  A list of lines
 1739 
 1740                         Each line has two items.
 1741 
 1742                         1. unused width in points
 1743                         2. word list
 1744 
 1745         B) When there is more than one input formatting fragment the output is
 1746             A fragment specifier with
 1747                - kind = 1
 1748                - lines=  A list of fragments each having fields
 1749                             - extraspace (needed for justified)
 1750                             - fontSize
 1751                             - words=word list
 1752                                 each word is itself a fragment with
 1753                                 various settings
 1754             in addition frags becomes a frag word list
 1755 
 1756         This structure can be used to easily draw paragraphs with the various alignments.
 1757         You can supply either a single width or a list of widths; the latter will have its
 1758         last item repeated until necessary. A 2-element list is useful when there is a
 1759         different first line indent; a longer list could be created to facilitate custom wraps
 1760         around irregular objects."""
 1761 
 1762         self._width_max = 0
 1763         if not isinstance(width,(tuple,list)): maxWidths = [width]
 1764         else: maxWidths = width
 1765         lines = []
 1766         self.height = lineno = 0
 1767         maxlineno = len(maxWidths)-1
 1768         style = self.style
 1769         hyphenator = getattr(style,'hyphenationLang','')
 1770         if hyphenator:
 1771             if isStr(hyphenator):
 1772                 hyphenator = hyphenator.strip()
 1773                 if hyphenator and pyphen:
 1774                     hyphenator = pyphen.Pyphen(lang=hyphenator).iterate
 1775                 else:
 1776                     hyphenator = None
 1777             elif not callable(hyphenator):
 1778                 raise ValueError('hyphenator should be a language spec or a callable unicode -->  pairs not %r' % hyphenator) 
 1779         else:
 1780             hyphenator = None
 1781         uriWasteReduce = style.uriWasteReduce
 1782         embeddedHyphenation = style.embeddedHyphenation
 1783         spaceShrinkage = style.spaceShrinkage
 1784         splitLongWords = style.splitLongWords
 1785         attemptHyphenation = hyphenator or uriWasteReduce or embeddedHyphenation
 1786         if attemptHyphenation:
 1787             hymwl = getattr(style,'hyphenationMinWordLength',hyphenationMinWordLength)
 1788         self._splitLongWordCount = self._hyphenations = 0
 1789 
 1790         #for bullets, work out width and ensure we wrap the right amount onto line one
 1791         _handleBulletWidth(self.bulletText,style,maxWidths)
 1792 
 1793         maxWidth = maxWidths[0]
 1794 
 1795         autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
 1796         calcBounds = autoLeading not in ('','off')
 1797         frags = self.frags
 1798         nFrags= len(frags)
 1799         if (nFrags==1 
 1800                 and not (style.endDots or hasattr(frags[0],'cbDefn') or hasattr(frags[0],'backColor')
 1801                             or _processed_frags(frags))):
 1802             f = frags[0]
 1803             fontSize = f.fontSize
 1804             fontName = f.fontName
 1805             ascent, descent = getAscentDescent(fontName,fontSize)
 1806             if hasattr(f,'text'):
 1807                 text = strip(f.text)
 1808                 if not text:
 1809                     return f.clone(kind=0, lines=[],ascent=ascent,descent=descent,fontSize=fontSize)
 1810                 else:
 1811                     words = split(text)
 1812             else:
 1813                 words = f.words[:]
 1814                 for w in words:
 1815                     if strip(w): break
 1816                 else:
 1817                     return f.clone(kind=0, lines=[],ascent=ascent,descent=descent,fontSize=fontSize)
 1818             spaceWidth = stringWidth(' ', fontName, fontSize, self.encoding)
 1819             dSpaceShrink = spaceShrinkage*spaceWidth
 1820             spaceShrink = 0
 1821             cLine = []
 1822             currentWidth = -spaceWidth   # hack to get around extra space for word 1
 1823             while words:
 1824                 word = words.pop(0)
 1825                 #this underscores my feeling that Unicode throughout would be easier!
 1826                 wordWidth = stringWidth(word, fontName, fontSize, self.encoding)
 1827                 newWidth = currentWidth + spaceWidth + wordWidth
 1828                 if newWidth>maxWidth+spaceShrink and not isinstance(word,_SplitWordH):
 1829                     if attemptHyphenation:
 1830                         hyOk = not getattr(f,'nobr',False)
 1831                         hsw = _hyphenateWord(hyphenator if hyOk else None,
 1832                                 fontName, fontSize, word, wordWidth, newWidth, maxWidth+spaceShrink,
 1833                                     uriWasteReduce if hyOk else False,
 1834                                     embeddedHyphenation and hyOk, hymwl)
 1835                         if hsw:
 1836                             words[0:0] = hsw
 1837                             self._hyphenations += 1
 1838                             continue
 1839                     if splitLongWords and not isinstance(word,_SplitWord):
 1840                         nmw = min(lineno,maxlineno)
 1841                         if wordWidth>max(maxWidths[nmw:nmw+1]):
 1842                             #a long word
 1843                             words[0:0] = _splitWord(word,maxWidth-spaceWidth-currentWidth,maxWidths,lineno,fontName,fontSize,self.encoding)
 1844                             self._splitLongWordCount += 1
 1845                             continue
 1846                 if newWidth <= (maxWidth+spaceShrink) or not len(cLine):
 1847                     # fit one more on this line
 1848                     cLine.append(word)
 1849                     currentWidth = newWidth
 1850                     spaceShrink += dSpaceShrink
 1851                 else:
 1852                     if currentWidth > self._width_max: self._width_max = currentWidth
 1853                     #end of line
 1854                     lines.append((maxWidth - currentWidth, cLine))
 1855                     cLine = [word]
 1856                     spaceShrink = 0
 1857                     currentWidth = wordWidth
 1858                     lineno += 1
 1859                     maxWidth = maxWidths[min(maxlineno,lineno)]
 1860 
 1861             #deal with any leftovers on the final line
 1862             if cLine!=[]:
 1863                 if currentWidth>self._width_max: self._width_max = currentWidth
 1864                 lines.append((maxWidth - currentWidth, cLine))
 1865 
 1866             return f.clone(kind=0, lines=lines,ascent=ascent,descent=descent,fontSize=fontSize)
 1867         elif nFrags<=0:
 1868             return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName,
 1869                             textColor=style.textColor, ascent=style.fontSize,descent=-0.2*style.fontSize,
 1870                             lines=[])
 1871         else:
 1872             njlbv = not style.justifyBreaks
 1873             words = []
 1874             FW = []
 1875             aFW = FW.append
 1876             _words = _getFragWords(frags,maxWidth)
 1877             sFW = 0
 1878             while _words:
 1879                 w = _words.pop(0)
 1880                 aFW(w)
 1881                 f = w[-1][0]
 1882                 fontName = f.fontName
 1883                 fontSize = f.fontSize
 1884 
 1885                 if not words:
 1886                     n = dSpaceShrink = spaceShrink = spaceWidth = currentWidth = 0
 1887                     maxSize = fontSize
 1888                     maxAscent, minDescent = getAscentDescent(fontName,fontSize)
 1889 
 1890                 wordWidth = w[0]
 1891                 f = w[1][0]
 1892                 if wordWidth>0:
 1893                     newWidth = currentWidth + spaceWidth + wordWidth
 1894                 else:
 1895                     newWidth = currentWidth
 1896 
 1897                 #test to see if this frag is a line break. If it is we will only act on it
 1898                 #if the current width is non-negative or the previous thing was a deliberate lineBreak
 1899                 lineBreak = f._fkind==_FK_BREAK
 1900                 if not lineBreak and newWidth>(maxWidth+spaceShrink) and not isinstance(w,_SplitFragH):
 1901                     if attemptHyphenation:
 1902                         hyOk = not getattr(f,'nobr',False)
 1903                         hsw = _hyphenateFragWord(hyphenator if hyOk else None,
 1904                                     w,newWidth,maxWidth+spaceShrink,
 1905                                     uriWasteReduce if hyOk else False,
 1906                                     embeddedHyphenation and hyOk, hymwl)
 1907                         if hsw:
 1908                             _words[0:0] = hsw
 1909                             FW.pop(-1)  #remove this as we are doing this one again
 1910                             self._hyphenations += 1
 1911                             continue
 1912                     if splitLongWords and not isinstance(w,_SplitFrag):
 1913                         nmw = min(lineno,maxlineno)
 1914                         if wordWidth>max(maxWidths[nmw:nmw+1]):
 1915                             #a long word
 1916                             _words[0:0] = _splitFragWord(w,maxWidth-spaceWidth-currentWidth,maxWidths,lineno)
 1917                             FW.pop(-1)  #remove this as we are doing this one again
 1918                             self._splitLongWordCount += 1
 1919                             continue
 1920                 endLine = (newWidth>(maxWidth+spaceShrink) and n>0) or lineBreak
 1921                 if not endLine:
 1922                     if lineBreak: continue      #throw it away
 1923                     nText = w[1][1]
 1924                     if nText: n += 1
 1925                     fontSize = f.fontSize
 1926                     if calcBounds:
 1927                         if f._fkind==_FK_IMG:
 1928                             descent,ascent = imgVRange(imgNormV(f.cbDefn.height,fontSize),f.cbDefn.valign,fontSize)
 1929                         else:
 1930                             ascent, descent = getAscentDescent(f.fontName,fontSize)
 1931                     else:
 1932                         ascent, descent = getAscentDescent(f.fontName,fontSize)
 1933                     maxSize = max(maxSize,fontSize)
 1934                     maxAscent = max(maxAscent,ascent)
 1935                     minDescent = min(minDescent,descent)
 1936                     if not words:
 1937                         g = f.clone()
 1938                         words = [g]
 1939                         g.text = nText
 1940                     elif not sameFrag(g,f):
 1941                         if spaceWidth:
 1942                             i = len(words)-1
 1943                             while i>=0:
 1944                                 wi = words[i]
 1945                                 i -= 1
 1946                                 if wi._fkind==_FK_TEXT:
 1947                                     if not wi.text.endswith(' '):
 1948                                         wi.text += ' '
 1949                                         spaceShrink += dSpaceShrink
 1950                                     break
 1951                         g = f.clone()
 1952                         words.append(g)
 1953                         g.text = nText
 1954                     elif spaceWidth:
 1955                         if not g.text.endswith(' '):
 1956                             g.text += ' ' + nText
 1957                             spaceShrink += dSpaceShrink
 1958                         else:
 1959                             g.text += nText
 1960                     else:
 1961                         g.text += nText
 1962 
 1963                     spaceWidth = stringWidth(' ',fontName,fontSize) if isinstance(w,_HSFrag) else 0 #of the space following this word
 1964                     dSpaceShrink = spaceWidth*spaceShrinkage
 1965 
 1966                     ni = 0
 1967                     for i in w[2:]:
 1968                         g = i[0].clone()
 1969                         g.text=i[1]
 1970                         if g.text: ni = 1
 1971                         words.append(g)
 1972                         fontSize = g.fontSize
 1973                         if calcBounds:
 1974                             if g._fkind==_FK_IMG:
 1975                                 descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
 1976                             else:
 1977                                 ascent, descent = getAscentDescent(g.fontName,fontSize)
 1978                         else:
 1979                             ascent, descent = getAscentDescent(g.fontName,fontSize)
 1980                         maxSize = max(maxSize,fontSize)
 1981                         maxAscent = max(maxAscent,ascent)
 1982                         minDescent = min(minDescent,descent)
 1983                     if not nText and ni:
 1984                         #one bit at least of the word was real
 1985                         n+=1
 1986 
 1987                     currentWidth = newWidth
 1988                 else:  #either it won't fit, or it's a lineBreak tag
 1989                     if lineBreak:
 1990                         g = f.clone()
 1991                         #del g.lineBreak
 1992                         words.append(g)
 1993 
 1994                     if currentWidth>self._width_max: self._width_max = currentWidth
 1995                     #end of line
 1996                     lines.append(FragLine(extraSpace=maxWidth-currentWidth, wordCount=n,
 1997                                         lineBreak=lineBreak and njlbv, words=words, fontSize=maxSize, ascent=maxAscent, descent=minDescent, maxWidth=maxWidth,
 1998                                         sFW=sFW))
 1999                     sFW = len(FW)-1
 2000 
 2001                     #start new line
 2002                     lineno += 1
 2003                     maxWidth = maxWidths[min(maxlineno,lineno)]
 2004 
 2005                     if lineBreak:
 2006                         words = []
 2007                         continue
 2008 
 2009                     spaceWidth = stringWidth(' ',fontName,fontSize) if isinstance(w,_HSFrag) else 0 #of the space following this word
 2010                     dSpaceShrink = spaceWidth*spaceShrinkage
 2011                     currentWidth = wordWidth
 2012                     n = 1
 2013                     spaceShrink = 0
 2014                     g = f.clone()
 2015                     maxSize = g.fontSize
 2016                     if calcBounds:
 2017                         if g._fkind==_FK_IMG:
 2018                             descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
 2019                         else:
 2020                             maxAscent, minDescent = getAscentDescent(g.fontName,maxSize)
 2021                     else:
 2022                         maxAscent, minDescent = getAscentDescent(g.fontName,maxSize)
 2023                     words = [g]
 2024                     g.text = w[1][1]
 2025 
 2026                     for i in w[2:]:
 2027                         g = i[0].clone()
 2028                         g.text=i[1]
 2029                         words.append(g)
 2030                         fontSize = g.fontSize
 2031                         if calcBounds:
 2032                             if g._fkind==_FK_IMG:
 2033                                 descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
 2034                             else:
 2035                                 ascent, descent = getAscentDescent(g.fontName,fontSize)
 2036                         else:
 2037                             ascent, descent = getAscentDescent(g.fontName,fontSize)
 2038                         maxSize = max(maxSize,fontSize)
 2039                         maxAscent = max(maxAscent,ascent)
 2040                         minDescent = min(minDescent,descent)
 2041 
 2042             #deal with any leftovers on the final line
 2043             if words:
 2044                 if currentWidth>self._width_max: self._width_max = currentWidth
 2045                 lines.append(ParaLines(extraSpace=(maxWidth - currentWidth),wordCount=n,lineBreak=False,
 2046                                     words=words, fontSize=maxSize,ascent=maxAscent,descent=minDescent,maxWidth=maxWidth,sFW=sFW))
 2047             self.frags = FW
 2048             return ParaLines(kind=1, lines=lines)
 2049 
 2050     def breakLinesCJK(self, maxWidths):
 2051         """Initially, the dumbest possible wrapping algorithm.
 2052         Cannot handle font variations."""
 2053 
 2054         if not isinstance(maxWidths,(list,tuple)): maxWidths = [maxWidths]
 2055         style = self.style
 2056         self.height = 0
 2057 
 2058         #for bullets, work out width and ensure we wrap the right amount onto line one
 2059         _handleBulletWidth(self.bulletText, style, maxWidths)
 2060         frags = self.frags
 2061         nFrags = len(frags)
 2062         if nFrags==1 and not hasattr(frags[0],'cbDefn') and not style.endDots:
 2063             f = frags[0]
 2064             if hasattr(self,'blPara') and getattr(self,'_splitpara',0):
 2065                 return f.clone(kind=0, lines=self.blPara.lines)
 2066             #single frag case
 2067             lines = []
 2068             lineno = 0
 2069             if hasattr(f,'text'):
 2070                 text = f.text
 2071             else:
 2072                 text = ''.join(getattr(f,'words',[]))
 2073 
 2074             from reportlab.lib.textsplit import wordSplit
 2075             lines = wordSplit(text, maxWidths, f.fontName, f.fontSize)
 2076             #the paragraph drawing routine assumes multiple frags per line, so we need an
 2077             #extra list like this
 2078             #  [space, [text]]
 2079             #
 2080             wrappedLines = [(sp, [line]) for (sp, line) in lines]
 2081             return f.clone(kind=0, lines=wrappedLines, ascent=f.fontSize, descent=-0.2*f.fontSize)
 2082         elif nFrags<=0:
 2083             return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName,
 2084                             textColor=style.textColor, lines=[],ascent=style.fontSize,descent=-0.2*style.fontSize)
 2085 
 2086         #general case nFrags>1 or special
 2087         if hasattr(self,'blPara') and getattr(self,'_splitpara',0):
 2088             return self.blPara
 2089         autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
 2090         calcBounds = autoLeading not in ('','off')
 2091         return cjkFragSplit(frags, maxWidths, calcBounds)
 2092 
 2093     def beginText(self, x, y):
 2094         return self.canv.beginText(x, y)
 2095 
 2096     def drawPara(self,debug=0):
 2097         """Draws a paragraph according to the given style.
 2098         Returns the final y position at the bottom. Not safe for
 2099         paragraphs without spaces e.g. Japanese; wrapping
 2100         algorithm will go infinite."""
 2101 
 2102         #stash the key facts locally for speed
 2103         canvas = self.canv
 2104         style = self.style
 2105         blPara = self.blPara
 2106         lines = blPara.lines
 2107         leading = style.leading
 2108         autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
 2109 
 2110         #work out the origin for line 1
 2111         leftIndent = style.leftIndent
 2112         cur_x = leftIndent
 2113 
 2114         if debug:
 2115             bw = 0.5
 2116             bc = Color(1,1,0)
 2117             bg = Color(0.9,0.9,0.9)
 2118         else:
 2119             bw = getattr(style,'borderWidth',None)
 2120             bc = getattr(style,'borderColor',None)
 2121             bg = style.backColor
 2122 
 2123         #if has a background or border, draw it
 2124         if bg or (bc and bw):
 2125             canvas.saveState()
 2126             op = canvas.rect
 2127             kwds = dict(fill=0,stroke=0)
 2128             if bc and bw:
 2129                 canvas.setStrokeColor(bc)
 2130                 canvas.setLineWidth(bw)
 2131                 kwds['stroke'] = 1
 2132                 br = getattr(style,'borderRadius',0)
 2133                 if br and not debug:
 2134                     op = canvas.roundRect
 2135                     kwds['radius'] = br
 2136             if bg:
 2137                 canvas.setFillColor(bg)
 2138                 kwds['fill'] = 1
 2139             bp = getattr(style,'borderPadding',0)
 2140             tbp, rbp, bbp, lbp = normalizeTRBL(bp)
 2141             op(leftIndent - lbp,
 2142                         -bbp,
 2143                         self.width - (leftIndent+style.rightIndent) + lbp+rbp,
 2144                         self.height + tbp+bbp,
 2145                         **kwds)
 2146             canvas.restoreState()
 2147 
 2148         nLines = len(lines)
 2149         bulletText = self.bulletText
 2150         if nLines > 0:
 2151             _offsets = getattr(self,'_offsets',[0])
 2152             _offsets += (nLines-len(_offsets))*[_offsets[-1]]
 2153             canvas.saveState()
 2154             #canvas.addLiteral('%% %s.drawPara' % _className(self))
 2155             alignment = style.alignment
 2156             offset = style.firstLineIndent+_offsets[0]
 2157             lim = nLines-1
 2158             noJustifyLast = not getattr(self,'_JustifyLast',False)
 2159             jllwc = style.justifyLastLine
 2160 
 2161             if blPara.kind==0:
 2162                 if alignment == TA_LEFT:
 2163                     dpl = _leftDrawParaLine
 2164                 elif alignment == TA_CENTER:
 2165                     dpl = _centerDrawParaLine
 2166                 elif self.style.alignment == TA_RIGHT:
 2167                     dpl = _rightDrawParaLine
 2168                 elif self.style.alignment == TA_JUSTIFY:
 2169                     dpl = _justifyDrawParaLine
 2170                 f = blPara
 2171                 if paraFontSizeHeightOffset:
 2172                     cur_y = self.height - f.fontSize
 2173                 else:
 2174                     cur_y = self.height - getattr(f,'ascent',f.fontSize)
 2175                 if bulletText:
 2176                     offset = _drawBullet(canvas,offset,cur_y,bulletText,style,rtl=style.wordWrap=='RTL' and self._wrapWidths or False)
 2177 
 2178                 #set up the font etc.
 2179                 canvas.setFillColor(f.textColor)
 2180 
 2181                 tx = self.beginText(cur_x, cur_y)
 2182                 tx.preformatted = 'preformatted' in self.__class__.__name__.lower()
 2183                 if autoLeading=='max':
 2184                     leading = max(leading,blPara.ascent-blPara.descent)
 2185                 elif autoLeading=='min':
 2186                     leading = blPara.ascent-blPara.descent
 2187 
 2188                 # set the paragraph direction
 2189                 tx.direction = self.style.wordWrap
 2190 
 2191                 #now the font for the rest of the paragraph
 2192                 tx.setFont(f.fontName, f.fontSize, leading)
 2193                 ws = lines[0][0]
 2194                 words = lines[0][1]
 2195                 lastLine = noJustifyLast and nLines==1
 2196                 if lastLine and jllwc and len(words)>jllwc:
 2197                     lastLine=False
 2198                 t_off = dpl( tx, offset, ws, words, lastLine)
 2199                 if f.us_lines or f.link or style.endDots:
 2200                     tx._do_line = MethodType(_do_line,tx)
 2201                     tx.xs = xs = tx.XtraState = ABag()
 2202                     tx._defaultLineWidth = canvas._lineWidth
 2203                     xs.cur_y = cur_y
 2204                     xs.f = f
 2205                     xs.style = style
 2206                     xs.lines = lines
 2207                     xs.link=f.link
 2208                     xs.textColor = f.textColor
 2209                     xs.backColors = []
 2210                     dx = t_off+leftIndent
 2211                     if dpl!=_justifyDrawParaLine: ws = 0
 2212                     if f.us_lines:
 2213                         _do_under_line(0, dx, ws, tx, f.us_lines)
 2214                     if f.link: _do_link_line(0, dx, ws, tx)
 2215                     if lastLine and style.endDots and dpl!=_rightDrawParaLine: _do_dots(0, dx, ws, xs, tx, dpl)
 2216 
 2217                     #now the middle of the paragraph, aligned with the left margin which is our origin.
 2218                     for i in xrange(1, nLines):
 2219                         ws = lines[i][0]
 2220                         words = lines[i][1]
 2221                         lastLine = noJustifyLast and i==lim
 2222                         if lastLine and jllwc and len(words)>jllwc:
 2223                             lastLine=False
 2224                         t_off = dpl( tx, _offsets[i], ws, words, lastLine)
 2225                         dx = t_off+leftIndent
 2226                         if dpl!=_justifyDrawParaLine: ws = 0
 2227                         if f.us_lines:
 2228                             _do_under_line(i, t_off, ws, tx, f.us_lines)
 2229                         if f.link: _do_link_line(i, dx, ws, tx)
 2230                         if lastLine and style.endDots and dpl!=_rightDrawParaLine: _do_dots(i, dx, ws, xs, tx, dpl)
 2231                 else:
 2232                     for i in xrange(1, nLines):
 2233                         words = lines[i][1]
 2234                         lastLine = noJustifyLast and i==lim
 2235                         if lastLine and jllwc and len(words)>jllwc:
 2236                             lastLine=False
 2237                         dpl( tx, _offsets[i], lines[i][0], words, lastLine)
 2238             else:
 2239                 if self.style.wordWrap == 'RTL':
 2240                     for line in lines:
 2241                         line.words = line.words[::-1]
 2242                 f = lines[0]
 2243                 if paraFontSizeHeightOffset:
 2244                     cur_y = self.height - f.fontSize
 2245                 else:
 2246                     cur_y = self.height - getattr(f,'ascent',f.fontSize)
 2247                 # default?
 2248                 dpl = _leftDrawParaLineX
 2249                 if bulletText:
 2250                     oo = offset
 2251                     offset = _drawBullet(canvas,offset,cur_y,bulletText,style, rtl=style.wordWrap=='RTL' and self._wrapWidths or False)
 2252                 if alignment == TA_LEFT:
 2253                     dpl = _leftDrawParaLineX
 2254                 elif alignment == TA_CENTER:
 2255                     dpl = _centerDrawParaLineX
 2256                 elif self.style.alignment == TA_RIGHT:
 2257                     dpl = _rightDrawParaLineX
 2258                 elif self.style.alignment == TA_JUSTIFY:
 2259                     dpl = _justifyDrawParaLineX
 2260                 else:
 2261                     raise ValueError("bad align %s" % repr(alignment))
 2262 
 2263                 #set up the font etc.
 2264                 tx = self.beginText(cur_x, cur_y)
 2265                 tx.preformatted = 'preformatted' in self.__class__.__name__.lower()
 2266                 tx._defaultLineWidth = canvas._lineWidth
 2267                 tx._underlineWidth = getattr(style,'underlineWidth','')
 2268                 tx._underlineOffset = getattr(style,'underlineOffset','') or '-0.125f'
 2269                 tx._strikeWidth = getattr(style,'strikeWidth','')
 2270                 tx._strikeOffset = getattr(style,'strikeOffset','') or '0.25f'
 2271                 tx._do_line = MethodType(_do_line,tx)
 2272                 # set the paragraph direction
 2273                 tx.direction = self.style.wordWrap
 2274 
 2275                 xs = tx.XtraState=ABag()
 2276                 xs.textColor=None
 2277                 xs.backColor=None
 2278                 xs.rise=0
 2279                 xs.backColors=[]
 2280                 xs.us_lines = {}
 2281                 xs.links = {}
 2282                 xs.link={}
 2283                 xs.leading = style.leading
 2284                 xs.leftIndent = leftIndent
 2285                 tx._leading = None
 2286                 tx._olb = None
 2287                 xs.cur_y = cur_y
 2288                 xs.f = f
 2289                 xs.style = style
 2290                 xs.autoLeading = autoLeading
 2291                 xs.paraWidth = self.width
 2292 
 2293                 tx._fontname,tx._fontsize = None, None
 2294                 line = lines[0]
 2295                 lastLine = noJustifyLast and nLines==1
 2296                 if lastLine and jllwc and line.wordCount>jllwc:
 2297                     lastLine=False
 2298                 dpl( tx, offset, line, lastLine)
 2299                 _do_post_text(tx)
 2300 
 2301                 #now the middle of the paragraph, aligned with the left margin which is our origin.
 2302                 for i in xrange(1, nLines):
 2303                     line = lines[i]
 2304                     lastLine = noJustifyLast and i==lim
 2305                     if lastLine and jllwc and line.wordCount>jllwc:
 2306                         lastLine=False
 2307                     dpl( tx, _offsets[i], line, lastLine)
 2308                     _do_post_text(tx)
 2309 
 2310             canvas.drawText(tx)
 2311             canvas.restoreState()
 2312 
 2313     def getPlainText(self,identify=None):
 2314         """Convenience function for templates which want access
 2315         to the raw text, without XML tags. """
 2316         frags = getattr(self,'frags',None)
 2317         if frags:
 2318             plains = []
 2319             plains_append = plains.append
 2320             if _processed_frags(frags):
 2321                 for word in frags:
 2322                     for style,text in word[1:]:
 2323                         plains_append(text)
 2324                     if isinstance(word,_HSFrag):
 2325                         plains_append(' ')
 2326             else:
 2327                 for frag in frags:
 2328                     if hasattr(frag, 'text'):
 2329                         plains_append(frag.text)
 2330             return ''.join(plains)
 2331         elif identify:
 2332             text = getattr(self,'text',None)
 2333             if text is None: text = repr(self)
 2334             return text
 2335         else:
 2336             return ''
 2337 
 2338     def getActualLineWidths0(self):
 2339         """Convenience function; tells you how wide each line
 2340         actually is.  For justified styles, this will be
 2341         the same as the wrap width; for others it might be
 2342         useful for seeing if paragraphs will fit in spaces."""
 2343         assert hasattr(self, 'width'), "Cannot call this method before wrap()"
 2344         if self.blPara.kind:
 2345             func = lambda frag, w=self.width: w - frag.extraSpace
 2346         else:
 2347             func = lambda frag, w=self.width: w - frag[0]
 2348         return list(map(func,self.blPara.lines))
 2349 
 2350     @staticmethod
 2351     def dumpFrags(frags,indent=4,full=False):
 2352         R = ['[']
 2353         aR = R.append
 2354         for i,f in enumerate(frags):
 2355             if full:
 2356                 aR('    [%r,' % f[0])
 2357                 for fx in f[1:]:
 2358                     aR('        (%s,)' % repr(fx[0]))
 2359                     aR('        %r),' % fx[1])
 2360                     aR('    ], #%d %s' % (i,f.__class__.__name__))
 2361                 aR('    ]')
 2362             else:
 2363                 aR('[%r, %s], #%d %s' % (f[0],', '.join(('(%s,%r)' % (fx[0].__class__.__name__,fx[1]) for fx in f[1:])),i,f.__class__.__name__))
 2364         i = indent*' '
 2365         return i + ('\n'+i).join(R)
 2366 
 2367 if __name__=='__main__':    #NORUNTESTS
 2368     def dumpParagraphLines(P):
 2369         print('dumpParagraphLines(<Paragraph @ %d>)' % id(P))
 2370         lines = P.blPara.lines
 2371         outw = sys.stdout.write
 2372         for l,line in enumerate(lines):
 2373             line = lines[l]
 2374             if hasattr(line,'words'):
 2375                 words = line.words
 2376             else:
 2377                 words = line[1]
 2378             nwords = len(words)
 2379             outw('line%d: %d(%s)\n  ' % (l,nwords,str(getattr(line,'wordCount','Unknown'))))
 2380             for w in xrange(nwords):
 2381                 outw(" %d:'%s'"%(w,getattr(words[w],'text',words[w])))
 2382             print()
 2383 
 2384     def fragDump(w):
 2385         R= ["'%s'" % w[1]]
 2386         for a in ('fontName', 'fontSize', 'textColor', 'rise', 'underline', 'strike', 'link', 'cbDefn','lineBreak'):
 2387             if hasattr(w[0],a):
 2388                 R.append('%s=%r' % (a,getattr(w[0],a)))
 2389         return ', '.join(R)
 2390 
 2391     def dumpParagraphFrags(P):
 2392         print('dumpParagraphFrags(<Paragraph @ %d>) minWidth() = %.2f' % (id(P), P.minWidth()))
 2393         frags = P.frags
 2394         n =len(frags)
 2395         for l in xrange(n):
 2396             print("frag%d: '%s' %s" % (l, frags[l].text,' '.join(['%s=%s' % (k,getattr(frags[l],k)) for k in frags[l].__dict__ if k!=text])))
 2397 
 2398         outw = sys.stdout.write
 2399         l = 0
 2400         cum = 0
 2401         for W in _getFragWords(frags,360):
 2402             cum += W[0]
 2403             outw("fragword%d: cum=%3d size=%d" % (l, cum, W[0]))
 2404             for w in W[1:]:
 2405                 outw(' (%s)' % fragDump(w))
 2406             print()
 2407             l += 1
 2408 
 2409 
 2410     from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
 2411     from reportlab.lib.units import cm
 2412     import sys
 2413     TESTS = sys.argv[1:]
 2414     if TESTS==[]: TESTS=['4']
 2415     def flagged(i,TESTS=TESTS):
 2416         return 'all' in TESTS or '*' in TESTS or str(i) in TESTS
 2417 
 2418     styleSheet = getSampleStyleSheet()
 2419     B = styleSheet['BodyText']
 2420     style = ParagraphStyle("discussiontext", parent=B)
 2421     style.fontName= 'Helvetica'
 2422     if flagged(1):
 2423         text='''The <font name=courier color=green>CMYK</font> or subtractive method follows the way a printer
 2424 mixes three pigments (cyan, magenta, and yellow) to form colors.
 2425 Because mixing chemicals is more difficult than combining light there
 2426 is a fourth parameter for darkness.  For example a chemical
 2427 combination of the <font name=courier color=green>CMY</font> pigments generally never makes a perfect
 2428 black -- instead producing a muddy color -- so, to get black printers
 2429 don't use the <font name=courier color=green>CMY</font> pigments but use a direct black ink.  Because
 2430 <font name=courier color=green>CMYK</font> maps more directly to the way printer hardware works it may
 2431 be the case that &amp;| &amp; | colors specified in <font name=courier color=green>CMYK</font> will provide better fidelity
 2432 and better control when printed.
 2433 '''
 2434         P=Paragraph(text,style)
 2435         dumpParagraphFrags(P)
 2436         aW, aH = 456.0, 42.8
 2437         w,h = P.wrap(aW, aH)
 2438         dumpParagraphLines(P)
 2439         S = P.split(aW,aH)
 2440         for s in S:
 2441             s.wrap(aW,aH)
 2442             dumpParagraphLines(s)
 2443             aH = 500
 2444 
 2445     if flagged(2):
 2446         P=Paragraph("""Price<super><font color="red">*</font></super>""", styleSheet['Normal'])
 2447         dumpParagraphFrags(P)
 2448         w,h = P.wrap(24, 200)
 2449         dumpParagraphLines(P)
 2450 
 2451     if flagged(3):
 2452         text = """Dieses Kapitel bietet eine schnelle <b><font color=red>Programme :: starten</font></b>
 2453 <onDraw name=myIndex label="Programme :: starten">
 2454 <b><font color=red>Eingabeaufforderung :: (&gt;&gt;&gt;)</font></b>
 2455 <onDraw name=myIndex label="Eingabeaufforderung :: (&gt;&gt;&gt;)">
 2456 <b><font color=red>&gt;&gt;&gt; (Eingabeaufforderung)</font></b>
 2457 <onDraw name=myIndex label="&gt;&gt;&gt; (Eingabeaufforderung)">
 2458 Einf&#xfc;hrung in Python <b><font color=red>Python :: Einf&#xfc;hrung</font></b>
 2459 <onDraw name=myIndex label="Python :: Einf&#xfc;hrung">.
 2460 Das Ziel ist, die grundlegenden Eigenschaften von Python darzustellen, ohne
 2461 sich zu sehr in speziellen Regeln oder Details zu verstricken. Dazu behandelt
 2462 dieses Kapitel kurz die wesentlichen Konzepte wie Variablen, Ausdr&#xfc;cke,
 2463 Kontrollfluss, Funktionen sowie Ein- und Ausgabe. Es erhebt nicht den Anspruch,
 2464 umfassend zu sein."""
 2465         P=Paragraph(text, styleSheet['Code'])
 2466         dumpParagraphFrags(P)
 2467         w,h = P.wrap(6*72, 9.7*72)
 2468         dumpParagraphLines(P)
 2469 
 2470     if flagged(4):
 2471         text='''Die eingebaute Funktion <font name=Courier>range(i, j [, stride])</font><onDraw name=myIndex label="eingebaute Funktionen::range()"><onDraw name=myIndex label="range() (Funktion)"><onDraw name=myIndex label="Funktionen::range()"> erzeugt eine Liste von Ganzzahlen und f&#xfc;llt sie mit Werten <font name=Courier>k</font>, f&#xfc;r die gilt: <font name=Courier>i &lt;= k &lt; j</font>. Man kann auch eine optionale Schrittweite angeben. Die eingebaute Funktion <font name=Courier>xrange()</font><onDraw name=myIndex label="eingebaute Funktionen::xrange()"><onDraw name=myIndex label="xrange() (Funktion)"><onDraw name=myIndex label="Funktionen::xrange()"> erf&#xfc;llt einen &#xe4;hnlichen Zweck, gibt aber eine unver&#xe4;nderliche Sequenz vom Typ <font name=Courier>XRangeType</font><onDraw name=myIndex label="XRangeType"> zur&#xfc;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. <font name=Courier>XRangeType</font> kennt eine einzige Methode, <font name=Courier>s.tolist()</font><onDraw name=myIndex label="XRangeType::tolist() (Methode)"><onDraw name=myIndex label="s.tolist() (Methode)"><onDraw name=myIndex label="Methoden::s.tolist()">, die seine Werte in eine Liste umwandelt.'''
 2472         aW = 420
 2473         aH = 64.4
 2474         P=Paragraph(text, B)
 2475         dumpParagraphFrags(P)
 2476         w,h = P.wrap(aW,aH)
 2477         print('After initial wrap',w,h)
 2478         dumpParagraphLines(P)
 2479         S = P.split(aW,aH)
 2480         dumpParagraphFrags(S[0])
 2481         w0,h0 = S[0].wrap(aW,aH)
 2482         print('After split wrap',w0,h0)
 2483         dumpParagraphLines(S[0])
 2484 
 2485     if flagged(5):
 2486         text = '<para> %s <![CDATA[</font></b>& %s < >]]></para>' % (chr(163),chr(163))
 2487         P=Paragraph(text, styleSheet['Code'])
 2488         dumpParagraphFrags(P)
 2489         w,h = P.wrap(6*72, 9.7*72)
 2490         dumpParagraphLines(P)
 2491 
 2492     if flagged(6):
 2493         for text in ['''Here comes <FONT FACE="Helvetica" SIZE="14pt">Helvetica 14</FONT> with <STRONG>strong</STRONG> <EM>emphasis</EM>.''',
 2494                      '''Here comes <font face="Helvetica" size="14pt">Helvetica 14</font> with <Strong>strong</Strong> <em>emphasis</em>.''',
 2495                      '''Here comes <font face="Courier" size="3cm">Courier 3cm</font> and normal again.''',
 2496                      ]:
 2497             P=Paragraph(text, styleSheet['Normal'], caseSensitive=0)
 2498             dumpParagraphFrags(P)
 2499             w,h = P.wrap(6*72, 9.7*72)
 2500             dumpParagraphLines(P)
 2501 
 2502     if flagged(7):
 2503         text = """<para align="CENTER" fontSize="24" leading="30"><b>Generated by:</b>Dilbert</para>"""
 2504         P=Paragraph(text, styleSheet['Code'])
 2505         dumpParagraphFrags(P)
 2506         w,h = P.wrap(6*72, 9.7*72)
 2507         dumpParagraphLines(P)
 2508 
 2509     if flagged(8):
 2510         text ="""- bullet 0<br/>- bullet 1<br/>- bullet 2<br/>- bullet 3<br/>- bullet 4<br/>- bullet 5"""
 2511         P=Paragraph(text, styleSheet['Normal'])
 2512         dumpParagraphFrags(P)
 2513         w,h = P.wrap(6*72, 9.7*72)
 2514         dumpParagraphLines(P)
 2515         S = P.split(6*72,h/2.0)
 2516         print(len(S))
 2517         dumpParagraphFrags(S[0])
 2518         dumpParagraphLines(S[0])
 2519         S[1].wrap(6*72, 9.7*72)
 2520         dumpParagraphFrags(S[1])
 2521         dumpParagraphLines(S[1])
 2522 
 2523 
 2524     if flagged(9):
 2525         text="""Furthermore, the fundamental error of
 2526 regarding <img src="../docs/images/testimg.gif" width="3" height="7"/> functional notions as
 2527 categorial delimits a general
 2528 convention regarding the forms of the<br/>
 2529 grammar. I suggested that these results
 2530 would follow from the assumption that"""
 2531         P=Paragraph(text,ParagraphStyle('aaa',parent=styleSheet['Normal'],align=TA_JUSTIFY))
 2532         dumpParagraphFrags(P)
 2533         w,h = P.wrap(6*cm-12, 9.7*72)
 2534         dumpParagraphLines(P)
 2535 
 2536     if flagged(10):
 2537         text="""a b c\xc2\xa0d e f"""
 2538         P=Paragraph(text,ParagraphStyle('aaa',parent=styleSheet['Normal'],align=TA_JUSTIFY))
 2539         dumpParagraphFrags(P)
 2540         w,h = P.wrap(6*cm-12, 9.7*72)
 2541         dumpParagraphLines(P)
 2542 
 2543     if flagged(11):
 2544         text="""This page tests out a number of attributes of the <b>paraStyle</b><onDraw name="_indexAdd" label="paraStyle"/> tag.
 2545 This paragraph is in a style we have called "style1". It should be a normal <onDraw name="_indexAdd" label="normal"/> paragraph, set in Courier 12 pt.
 2546 It should be a normal<onDraw name="_indexAdd" label="normal"/> paragraph, set in Courier (not bold).
 2547 It should be a normal<onDraw name="_indexAdd" label="normal"/> paragraph, set in Courier 12 pt."""
 2548         P=Paragraph(text,style=ParagraphStyle('style1',fontName="Courier",fontSize=10))
 2549         dumpParagraphFrags(P)
 2550         w,h = P.wrap(6.27*72-12,10000)
 2551         dumpParagraphLines(P)