#Copyright ReportLab Europe Ltd. 2000-2017
#see license.txt for license details
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/platypus/paragraph.py
__all__=(
'Paragraph',
'cleanBlockQuotedText',
'ParaLines',
'FragLine',
)
__version__='3.5.20'
__doc__='''The standard paragraph implementation'''
from string import whitespace
from operator import truth
from unicodedata import category
from reportlab.pdfbase.pdfmetrics import stringWidth, getFont, getAscentDescent
from reportlab.platypus.paraparser import ParaParser, _PCT, _num as _parser_num, _re_us_value
from reportlab.platypus.flowables import Flowable
from reportlab.lib.colors import Color
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.lib.geomutils import normalizeTRBL
from reportlab.lib.textsplit import wordSplit, ALL_CANNOT_START
from copy import deepcopy
from reportlab.lib.abag import ABag
from reportlab.rl_config import platypus_link_underline, decimalSymbol, _FUZZ,\
paraFontSizeHeightOffset, hyphenationMinWordLength
from reportlab.lib.utils import _className, isBytes, unicodeT, bytesT, isStr
from reportlab.lib.rl_accel import sameFrag
from reportlab import xrange
import re
from types import MethodType
try:
import pyphen
except:
pyphen = None
#on UTF8/py33 branch, split and strip must be unicode-safe!
#thanks to Dirk Holtwick for helpful discussions/insight
#on this one
_wsc = ''.join((
u'\u0009', # HORIZONTAL TABULATION
u'\u000A', # LINE FEED
u'\u000B', # VERTICAL TABULATION
u'\u000C', # FORM FEED
u'\u000D', # CARRIAGE RETURN
u'\u001C', # FILE SEPARATOR
u'\u001D', # GROUP SEPARATOR
u'\u001E', # RECORD SEPARATOR
u'\u001F', # UNIT SEPARATOR
u'\u0020', # SPACE
u'\u0085', # NEXT LINE
#u'\u00A0', # NO-BREAK SPACE
u'\u1680', # OGHAM SPACE MARK
u'\u2000', # EN QUAD
u'\u2001', # EM QUAD
u'\u2002', # EN SPACE
u'\u2003', # EM SPACE
u'\u2004', # THREE-PER-EM SPACE
u'\u2005', # FOUR-PER-EM SPACE
u'\u2006', # SIX-PER-EM SPACE
u'\u2007', # FIGURE SPACE
u'\u2008', # PUNCTUATION SPACE
u'\u2009', # THIN SPACE
u'\u200A', # HAIR SPACE
u'\u200B', # ZERO WIDTH SPACE
u'\u2028', # LINE SEPARATOR
u'\u2029', # PARAGRAPH SEPARATOR
u'\u202F', # NARROW NO-BREAK SPACE
u'\u205F', # MEDIUM MATHEMATICAL SPACE
u'\u3000', # IDEOGRAPHIC SPACE
))
_wsc_re_split=re.compile('[%s]+'% re.escape(_wsc)).split
_wsc_end_search=re.compile('[%s]+$'% re.escape(_wsc)).search
def _usConv(s, vMap, default=None):
'''convert a strike/underline distance to a number'''
if isStr(s):
s = s.strip()
if s:
m = _re_us_value.match(s)
if m:
return float(m.group(1))*vMap[m.group(2)]
else:
return _parser_num(s,allowRelative=False)
elif default:
return default
return s
def split(text, delim=None):
if isBytes(text): text = text.decode('utf8')
if delim is not None and isBytes(delim): delim = delim.decode('utf8')
return [uword for uword in (_wsc_re_split(text) if delim is None and u'\xa0' in text else text.split(delim))]
def strip(text):
if isBytes(text): text = text.decode('utf8')
return text.strip(_wsc)
def lstrip(text):
if isBytes(text): text = text.decode('utf8')
return text.lstrip(_wsc)
def rstrip(text):
if isBytes(text): text = text.decode('utf8')
return text.rstrip(_wsc)
class ParaLines(ABag):
"""
class ParaLines contains the broken into lines representation of Paragraphs
kind=0 Simple
fontName, fontSize, textColor apply to whole Paragraph
lines [(extraSpace1,words1),....,(extraspaceN,wordsN)]
kind==1 Complex
lines [FragLine1,...,FragLineN]
"""
class FragLine(ABag):
"""
class FragLine contains a styled line (ie a line with more than one style)::
extraSpace unused space for justification only
wordCount 1+spaces in line for justification purposes
words [ParaFrags] style text lumps to be concatenated together
fontSize maximum fontSize seen on the line; not used at present,
but could be used for line spacing.
"""
def _lineClean(L):
return ' '.join(list(filter(truth,split(strip(L)))))
def cleanBlockQuotedText(text,joiner=' '):
"""This is an internal utility which takes triple-
quoted text form within the document and returns
(hopefully) the paragraph the user intended originally."""
L=list(filter(truth,list(map(_lineClean, split(text, '\n')))))
return joiner.join(L)
def setXPos(tx,dx):
if dx>1e-6 or dx<-1e-6:
tx.setXPos(dx)
def _nbspCount(w):
if isBytes(w):
return w.count(b'\xc2\xa0')
else:
return w.count(u'\xa0')
def _leftDrawParaLine( tx, offset, extraspace, words, last=0):
simple = extraspace>-1e-8 or getattr(tx,'preformatted',False)
text = ' '.join(words)
setXPos(tx,offset)
if not simple:
nSpaces = len(words)+_nbspCount(text)-1
simple = not nSpaces
if simple:
tx._textOut(text,1)
else:
tx.setWordSpace(extraspace / float(nSpaces))
tx._textOut(text,1)
tx.setWordSpace(0)
setXPos(tx,-offset)
return offset
def _centerDrawParaLine( tx, offset, extraspace, words, last=0):
simple = extraspace>-1e-8 or getattr(tx,'preformatted',False)
text = ' '.join(words)
if not simple:
nSpaces = len(words)+_nbspCount(text)-1
simple = not nSpaces
if simple:
m = offset + 0.5 * extraspace
setXPos(tx,m)
tx._textOut(text,1)
else:
m = offset
tx.setWordSpace(extraspace / float(nSpaces))
setXPos(tx,m)
tx._textOut(text,1)
tx.setWordSpace(0)
setXPos(tx,-m)
return m
def _rightDrawParaLine( tx, offset, extraspace, words, last=0):
simple = extraspace>-1e-8 or getattr(tx,'preformatted',False)
text = ' '.join(words)
if not simple:
nSpaces = len(words)+_nbspCount(text)-1
simple = not nSpaces
if simple:
m = offset + extraspace
setXPos(tx,m)
tx._textOut(' '.join(words),1)
else:
m = offset
tx.setWordSpace(extraspace / float(nSpaces))
setXPos(tx,m)
tx._textOut(text,1)
tx.setWordSpace(0)
setXPos(tx,-m)
return m
def _justifyDrawParaLine( tx, offset, extraspace, words, last=0):
setXPos(tx,offset)
text = ' '.join(words)
simple = last or (-1e-8<extraspace<=1e-8) or getattr(tx,'preformatted',False)
if not simple:
nSpaces = len(words)+_nbspCount(text)-1
simple = not nSpaces
if simple:
#last one or no extra space so left align
tx._textOut(text,1)
else:
tx.setWordSpace(extraspace / float(nSpaces))
tx._textOut(text,1)
tx.setWordSpace(0)
setXPos(tx,-offset)
return offset
def imgVRange(h,va,fontSize):
'''return bottom,top offsets relative to baseline(0)'''
if va=='baseline':
iyo = 0
elif va in ('text-top','top'):
iyo = fontSize-h
elif va=='middle':
iyo = fontSize - (1.2*fontSize+h)*0.5
elif va in ('text-bottom','bottom'):
iyo = fontSize - 1.2*fontSize
elif va=='super':
iyo = 0.5*fontSize
elif va=='sub':
iyo = -0.5*fontSize
elif hasattr(va,'normalizedValue'):
iyo = va.normalizedValue(fontSize)
else:
iyo = va
return iyo,iyo+h
def imgNormV(v,nv):
if hasattr(v,'normalizedValue'):
return v.normalizedValue(nv)
else:
return v
def _getDotsInfo(style):
dots = style.endDots
if isStr(dots):
text = dots
fontName = style.fontName
fontSize = style.fontSize
textColor = style.textColor
backColor = style.backColor
dy = 0
else:
text = getattr(dots,'text','.')
fontName = getattr(dots,'fontName',style.fontName)
fontSize = getattr(dots,'fontSize',style.fontSize)
textColor = getattr(dots,'textColor',style.textColor)
backColor = getattr(dots,'backColor',style.backColor)
dy = getattr(dots,'dy',0)
return text,fontName,fontSize,textColor,backColor,dy
_56=5./6
_16=1./6
def _putFragLine(cur_x, tx, line, last, pKind):
preformatted = tx.preformatted
xs = tx.XtraState
cur_y = xs.cur_y
x0 = tx._x0
autoLeading = xs.autoLeading
leading = xs.leading
cur_x += xs.leftIndent
dal = autoLeading in ('min','max')
if dal:
if autoLeading=='max':
ascent = max(_56*leading,line.ascent)
descent = max(_16*leading,-line.descent)
else:
ascent = line.ascent
descent = -line.descent
leading = ascent+descent
if tx._leading!=leading:
tx.setLeading(leading)
if dal:
olb = tx._olb
if olb is not None:
xcy = olb-ascent
if tx._oleading!=leading:
cur_y += leading - tx._oleading
if abs(xcy-cur_y)>1e-8:
cur_y = xcy
tx.setTextOrigin(x0,cur_y)
xs.cur_y = cur_y
tx._olb = cur_y - descent
tx._oleading = leading
ws = getattr(tx,'_wordSpace',0)
nSpaces = 0
words = line.words
AL = []
LL = []
us_lines = xs.us_lines
links = xs.links
for i, f in enumerate(words):
if hasattr(f,'cbDefn'):
cbDefn = f.cbDefn
kind = cbDefn.kind
if kind=='img':
#draw image cbDefn,cur_y,cur_x
txfs = tx._fontsize
if txfs is None:
txfs = xs.style.fontSize
w = imgNormV(cbDefn.width,xs.paraWidth)
h = imgNormV(cbDefn.height,txfs)
iy0,iy1 = imgVRange(h,cbDefn.valign,txfs)
cur_x_s = cur_x + nSpaces*ws
tx._canvas.drawImage(cbDefn.image,cur_x_s,cur_y+iy0,w,h,mask='auto')
cur_x += w
cur_x_s += w
setXPos(tx,cur_x_s-tx._x0)
else:
name = cbDefn.name
if kind=='anchor':
tx._canvas.bookmarkHorizontal(name,cur_x,cur_y+leading)
else:
func = getattr(tx._canvas,name,None)
if not func:
raise AttributeError("Missing %s callback attribute '%s'" % (kind,name))
tx._canvas._curr_tx_info=dict(tx=tx,cur_x=cur_x,cur_y=cur_y,leading=leading,xs=tx.XtraState)
try:
func(tx._canvas,kind,getattr(cbDefn,'label',None))
finally:
del tx._canvas._curr_tx_info
if f is words[-1]:
if not tx._fontname:
tx.setFont(xs.style.fontName,xs.style.fontSize)
tx._textOut('',1)
else:
cur_x_s = cur_x + nSpaces*ws
end_x = cur_x_s
fontSize = f.fontSize
textColor = f.textColor
rise = f.rise
if i > 0:
end_x = cur_x_s - (0 if preformatted else _trailingSpaceLength(words[i-1].text, tx))
if (tx._fontname,tx._fontsize)!=(f.fontName,fontSize):
tx._setFont(f.fontName, fontSize)
if xs.textColor!=textColor:
xs.textColor = textColor
tx.setFillColor(textColor)
if xs.rise!=rise:
xs.rise=rise
tx.setRise(rise)
text = f.text
tx._textOut(text,f is words[-1]) # cheap textOut
if LL != f.us_lines:
S = set(LL)
NS = set(f.us_lines)
nL = NS - S #new lines
eL = S - NS #ending lines
for l in eL:
us_lines[l] = us_lines[l],end_x
for l in nL:
us_lines[l] = (l,fontSize,textColor,cur_x_s),fontSize
LL = f.us_lines
if LL:
for l in LL:
l0, fsmax = us_lines[l]
if fontSize>fsmax:
us_lines[l] = l0, fontSize
nlo = rise - 0.2*fontSize
nhi = rise + fontSize
if AL != f.link:
S = set(AL)
NS = set(f.link)
nL = NS - S #new linkis
eL = S - NS #ending links
for l in eL:
links[l] = links[l],end_x
for l in nL:
links[l] = (l,cur_x),nlo,nhi
AL = f.link
if AL:
for l in AL:
l0, lo, hi = links[l]
if nlo<lo or nhi>hi:
links[l] = l0,min(nlo,lo),max(nhi,hi)
bg = getattr(f,'backColor',None)
if bg and not xs.backColor:
xs.backColor = bg
xs.backColor_x = cur_x_s
elif xs.backColor:
if not bg:
xs.backColors.append( (xs.backColor_x, end_x, xs.backColor) )
xs.backColor = None
elif f.backColor!=xs.backColor or xs.textColor!=xs.backColor:
xs.backColors.append( (xs.backColor_x, end_x, xs.backColor) )
xs.backColor = bg
xs.backColor_x = cur_x_s
txtlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize)
cur_x += txtlen
nSpaces += text.count(' ')+_nbspCount(text)
cur_x_s = cur_x+(nSpaces-1)*ws
if last and pKind!='right' and xs.style.endDots:
_do_dots_frag(cur_x,cur_x_s,line.maxWidth,xs,tx)
if LL:
for l in LL:
us_lines[l] = us_lines[l], cur_x_s
if AL:
for l in AL:
links[l] = links[l], cur_x_s
if xs.backColor:
xs.backColors.append( (xs.backColor_x, cur_x_s, xs.backColor) )
if tx._x0!=x0:
setXPos(tx,x0-tx._x0)
def _do_dots_frag(cur_x, cur_x_s, maxWidth, xs, tx):
text,fontName,fontSize,textColor,backColor,dy = _getDotsInfo(xs.style)
txtlen = tx._canvas.stringWidth(text, fontName, fontSize)
if cur_x_s+txtlen<=maxWidth:
if tx._fontname!=fontName or tx._fontsize!=fontSize:
tx.setFont(fontName,fontSize)
maxWidth += getattr(tx,'_dotsOffsetX',tx._x0)
tx.setTextOrigin(0,xs.cur_y+dy)
setXPos(tx,cur_x_s-cur_x)
n = int((maxWidth-cur_x_s)/txtlen)
setXPos(tx,maxWidth - txtlen*n)
if xs.textColor!=textColor:
tx.setFillColor(textColor)
if backColor: xs.backColors.append((cur_x,maxWidth,backColor))
tx._textOut(n*text,1)
if dy: tx.setTextOrigin(tx._x0,xs.cur_y-dy)
def _leftDrawParaLineX( tx, offset, line, last=0):
setXPos(tx,offset)
extraSpace = line.extraSpace
simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
if not simple:
nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
simple = not nSpaces
if simple:
_putFragLine(offset, tx, line, last, 'left')
else:
tx.setWordSpace(extraSpace / float(nSpaces))
_putFragLine(offset, tx, line, last, 'left')
tx.setWordSpace(0)
setXPos(tx,-offset)
def _centerDrawParaLineX( tx, offset, line, last=0):
tx._dotsOffsetX = offset + tx._x0
try:
extraSpace = line.extraSpace
simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
if not simple:
nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
simple = not nSpaces
if simple:
m = offset+0.5*line.extraSpace
setXPos(tx,m)
_putFragLine(m, tx, line, last,'center')
else:
m = offset
tx.setWordSpace(extraSpace / float(nSpaces))
_putFragLine(m, tx, line, last, 'center')
tx.setWordSpace(0)
setXPos(tx,-m)
finally:
del tx._dotsOffsetX
def _rightDrawParaLineX( tx, offset, line, last=0):
extraSpace = line.extraSpace
simple = extraSpace>-1e-8 or getattr(line,'preformatted',False)
if not simple:
nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
simple = not nSpaces
if simple:
m = offset+line.extraSpace
setXPos(tx,m)
_putFragLine(m,tx, line, last, 'right')
else:
m = offset
tx.setWordSpace(extraSpace / float(nSpaces))
_putFragLine(m, tx, line, last, 'right')
tx.setWordSpace(0)
setXPos(tx,-m)
def _justifyDrawParaLineX( tx, offset, line, last=0):
setXPos(tx,offset)
extraSpace = line.extraSpace
simple = last or abs(extraSpace)<=1e-8 or line.lineBreak
if not simple:
nSpaces = line.wordCount+sum([_nbspCount(w.text) for w in line.words if not hasattr(w,'cbDefn')])-1
simple = not nSpaces
if not simple:
tx.setWordSpace(extraSpace / float(nSpaces))
_putFragLine(offset, tx, line, last, 'justify')
tx.setWordSpace(0)
else:
_putFragLine(offset, tx, line, last, 'justify') #no space modification
setXPos(tx,-offset)
def _trailingSpaceLength(text, tx):
ws = _wsc_end_search(text)
return tx._canvas.stringWidth(ws.group(), tx._fontname, tx._fontsize) if ws else 0
class _HSFrag(list):
pass
class _SplitFrag(list):
'''a split frag'''
pass
class _SplitFragH(_SplitFrag):
'''a split frag that's the head part of the split'''
pass
class _SplitFragHY(_SplitFragH):
'''a head split frag that need '-' removing befire rejoining'''
pass
class _SplitFragHS(_SplitFrag,_HSFrag):
"""a split frag that's followed by a space"""
pass
def _processed_frags(frags):
try:
return isinstance(frags[0][0],(float,int))
except:
return False
_FK_TEXT = 0
_FK_IMG = 1
_FK_APPEND = 2
_FK_BREAK = 3
def _getFragWords(frags,maxWidth=None):
''' given a Parafrag list return a list of fragwords
[[size, (f00,w00), ..., (f0n,w0n)],....,[size, (fm0,wm0), ..., (f0n,wmn)]]
each pair f,w represents a style and some string
each sublist represents a word
'''
def _rescaleFrag(f):
w = f[0]
if isinstance(w,_PCT):
if w._normalizer!=maxWidth:
w._normalizer = maxWidth
w = w.normalizedValue(maxWidth)
f[0] = w
R = []
aR = R.append
W = []
if _processed_frags(frags):
aW = W.append
for f in frags:
_rescaleFrag(f)
if isinstance(f,_SplitFrag):
f0 = f[0]
if not W:
W0t = type(f0)
Wlen = 0
sty = None
else:
if isinstance(lf,_SplitFragHY):
sty, t = W[-1]
Wlen -= stringWidth(t[-1],sty.fontName,sty.fontSize) + 1e-8
W[-1] = (sty,t[:-1]) #strip the '-'
Wlen += f0
for ts,t in f[1:]:
if ts is sty:
W[-1] = (sty,W[-1][1]+t)
else:
aW((ts,t))
sty = ts
#W.extend(f[1:])
lf = f #latest f in W
continue
else:
if W:
#must end a joining
aR((_HSFrag if isinstance(lf,_HSFrag) else list)([W0t(Wlen)]+W))
del W[:]
aR(f)
if W:
#must end a joining
aR((_HSFrag if isinstance(lf,_HSFrag) else list)([W0t(Wlen)]+W))
else:
hangingSpace = False
n = 0
hangingStrip = True
for f in frags:
text = f.text
if text!='':
f._fkind = _FK_TEXT
if hangingStrip:
text = lstrip(text)
if not text: continue
hangingStrip = False
S = split(text)
if text[0] in whitespace or not S:
if W:
W.insert(0,n) #end preceding word
aR(W)
whs = hangingSpace
W = []
hangingSpace = False
n = 0
else:
whs = R and isinstance(R[-1],_HSFrag)
if not whs:
S.insert(0,'')
elif not S:
continue
for w in S[:-1]:
W.append((f,w))
n += stringWidth(w, f.fontName, f.fontSize)
W.insert(0,n)
aR(_HSFrag(W))
W = []
n = 0
hangingSpace = False
w = S[-1]
W.append((f,w))
n += stringWidth(w, f.fontName, f.fontSize)
if text and text[-1] in whitespace:
W.insert(0,n)
aR(_HSFrag(W))
W = []
n = 0
elif hasattr(f,'cbDefn'):
cb = f.cbDefn
w = getattr(cb,'width',0)
if w:
if hasattr(w,'normalizedValue'):
w._normalizer = maxWidth
w = w.normalizedValue(maxWidth)
if W:
W.insert(0,n)
aR(_HSFrag(W) if hangingSpace else W)
W = []
hangingSpace = False
n = 0
f._fkind = _FK_IMG
aR([w,(f,'')])
hangingStrip = False
else:
f._fkind = _FK_APPEND
if not W and R and isinstance(R[-1],_HSFrag):
R[-1].append((f,''))
else:
W.append((f,''))
elif hasattr(f, 'lineBreak'):
#pass the frag through. The line breaker will scan for it.
if W:
W.insert(0,n)
aR(W)
W = []
n = 0
hangingSpace = False
f._fkind = _FK_BREAK
aR([0,(f,'')])
hangingStrip = True
if W:
W.insert(0,n)
aR(W)
if not R:
if frags:
f = frags[0]
f._fkind = _FK_TEXT
R = [[0,(f,u'')]]
return R
def _fragWordIter(w):
for f, s in w[1:]:
if hasattr(f,'cbDefn'):
yield f, getattr(f,'width',0), s
elif s:
if isBytes(s):
s = s.decode('utf8') #only encoding allowed
for c in s:
yield f, stringWidth(c,f.fontName, f.fontSize), c
else:
yield f, 0, s
def _splitFragWord(w,maxWidth,maxWidths,lineno):
'''given a frag word, w, as returned by getFragWords
split it into frag words that fit in lines of length
maxWidth
maxWidths[lineno+1]
.....
maxWidths[lineno+n]
return the new word list which is either
_SplitFrag....._SPlitFrag or
_SplitFrag....._SplitFragHS if the word is hanging space.
'''
R = []
maxlineno = len(maxWidths)-1
W = []
lineWidth = 0
fragText = u''
wordWidth = 0
f = w[1][0]
for g,cw,c in _fragWordIter(w):
newLineWidth = lineWidth+cw
tooLong = newLineWidth>maxWidth
if g is not f or tooLong:
f = f.clone()
if hasattr(f,'text'):
f.text = fragText
W.append((f,fragText))
if tooLong:
W = _SplitFrag([wordWidth]+W)
R.append(W)
lineno += 1
maxWidth = maxWidths[min(maxlineno,lineno)]
W = []
newLineWidth = cw
wordWidth = 0
fragText = u''
f = g
wordWidth += cw
fragText += c
lineWidth = newLineWidth
W.append((f,fragText))
W = (_SplitFragHS if isinstance(w,_HSFrag) else _SplitFragH)([wordWidth]+W)
R.append(W)
return R
#derived from Django validator
#https://github.com/django/django/blob/master/django/core/validators.py
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)
def _slash_parts(uri,scheme,slash):
tail = u''
while uri.endswith(slash):
tail += slash
uri = uri[:-1]
i = 2
while True:
i = uri.find(slash,i)
if i<0: break
i += 1
yield scheme+uri[:i],uri[i:]+tail
def _uri_split_pairs(uri):
if isBytes(uri): uri = uri.decode('utf8')
m = uri_pat.match(uri)
if not m: return None
scheme = m.group(1)
uri = uri[len(scheme):]
slash = (u'\\' if not scheme and u'/' not in uri #might be a microsoft pattern
else u'/')
R = ([(scheme, uri)] if scheme and uri else []) + list(_slash_parts(uri,scheme,slash))
R.reverse()
return R
#valid letters determined by inspection of
# https://en.wikipedia.org/wiki/List_of_Unicode_characters#Latin_script
_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'
#explicit hyphens
_hy_shy = u'-\xad'
_hy_pfx_pat = re.compile(u'^[\'"([{\xbf\u2018\u201a\u201c\u201e]+')
_hy_sfx_pat = re.compile(u'[]\'")}?!.,;:\u2019\u201b\u201d\u201f]+$')
_hy_letters_pat=re.compile(u''.join((u"^[",_hy_letters,u"]+$")))
_hy_shy_letters_pat=re.compile(u''.join((u"^[",_hy_shy,_hy_letters,"]+$")))
_hy_shy_pat = re.compile(u''.join((u"([",_hy_shy,u"])")))
def _hyGenPair(hyphenator, s, ww, newWidth, maxWidth, fontName, fontSize, uriWasteReduce, embeddedHyphenation, hymwl):
if isBytes(s): s = s.decode('utf8') #only encoding allowed
m = _hy_pfx_pat.match(s)
if m:
pfx = m.group(0)
s = s[len(pfx):]
else:
pfx = u''
m = _hy_sfx_pat.search(s)
if m:
sfx = m.group(0)
s = s[:-len(sfx)]
else:
sfx = u''
if len(s) < hymwl: return
w0 = newWidth - ww
R = _uri_split_pairs(s)
if R is not None:
#a uri match was seen
if ww>maxWidth or (uriWasteReduce and w0 <= (1-uriWasteReduce)*maxWidth):
#we matched a uri and it makes sense to split
for h, t in R:
h = pfx+h
t = t + sfx
hw = stringWidth(h,fontName,fontSize)
tw = w0 + hw
if tw<=maxWidth:
return u'',0,hw,ww-hw,h,t
return
H = _hy_shy_pat.split(s)
if hyphenator and (_hy_letters_pat.match(s) or (_hy_shy_letters_pat.match(s) and u'' not in H)):
hylen = stringWidth(u'-',fontName,fontSize)
for h,t in hyphenator(s):
h = pfx + h
if not _hy_shy_pat.match(h[-1]):
jc = u'-'
jclen = hylen
else:
jc = u''
jclen = 0
t = t + sfx
hw = stringWidth(h,fontName,fontSize)
tw = hw+w0 + jclen
if tw<=maxWidth:
return jc,jclen,hw,ww-hw,h,t
#even though the above tries for words with '-' it may be that no split ended with '-'
#so this may succeed where the above does not
n = len(H)
if n>=3 and embeddedHyphenation and u'' not in H and _hy_shy_letters_pat.match(s):
for i in reversed(xrange(2,n,2)):
h = pfx + ''.join(H[:i])
t = ''.join(H[i:]) + sfx
hw = stringWidth(h,fontName,fontSize)
tw = hw+w0
if tw<=maxWidth:
return u'',0,hw,ww-hw,h,t
def _fragWordSplitRep(FW):
'''takes a frag word and assembles a unicode word from it
if a rise is seen or a non-zerowidth cbdefn then we return
None. Otherwise we return (uword,([i1,c1],[i2,c2],...])
where each ii is the index of the word fragment in the word
'''
cc = plen = 0
X = []
eX = X.extend
U = []
aU = U.append
for i in xrange(1,len(FW)):
f, t = FW[i]
if f.rise!=0: return None
if hasattr(f,'cbDefn') and getattr(f.cbDefn,'width',0): return
if not t: continue
if isBytes(t): t = t.decode('utf8')
aU(t)
eX(len(t)*[(i,cc)])
cc += len(t)
return u''.join(U),tuple(X)
def _rebuildFragWord(F):
'''F are the frags'''
return [sum((stringWidth(u,s.fontName,s.fontSize) for s,u in F))]+F
def _hyGenFragsPair(hyphenator, FW, newWidth, maxWidth, uriWasteReduce, embeddedHyphenation, hymwl):
X = _fragWordSplitRep(FW)
if not X: return
s, X = X
if isBytes(s): s = s.decode('utf8') #only encoding allowed
m = _hy_pfx_pat.match(s)
if m:
pfx = m.group(0)
s = s[len(pfx):]
else:
pfx = u''
m = _hy_sfx_pat.search(s)
if m:
sfx = m.group(0)
s = s[:-len(sfx)]
else:
sfx = u''
if len(s) < hymwl: return
ww = FW[0]
w0 = newWidth - ww
#try for a uri
R = _uri_split_pairs(s)
if R is not None:
#a uri match was seen
if ww>maxWidth or (uriWasteReduce and w0 <= (1-uriWasteReduce)*maxWidth):
#we matched a uri and it makes sense to split
for h, t in R:
h = pfx+h
pos = len(h)
#FW[fx] is split
fx, cc = X[pos]
FL = FW[1:fx]
ffx, sfx = FW[fx]
sfxl = sfx[:pos-cc]
if sfxl: FL.append((ffx,sfxl))
sfxr = sfx[pos-cc:]
FR = FW[fx+1:]
if sfxr: FR.insert(0,(ffx,sfxr))
h = _rebuildFragWord(FL)
hw = h[0]
t = [ww-hw]+FR
tw = w0+hw
if tw<=maxWidth:
return u'',0,hw,ww-hw,h,t
return
H = _hy_shy_pat.split(s)
if hyphenator and (_hy_letters_pat.match(s) or (_hy_shy_letters_pat.match(s) and u'' not in H)):
#not too diffcult for now
for h,t in hyphenator(s):
h = pfx+h
pos = len(h)
#FW[fx] is split
fx, cc = X[pos]
FL = FW[1:fx]
ffx, sfx = FW[fx]
sfxl = sfx[:pos-cc]
if not _hy_shy_pat.match(h[-1]):
jc = u'-'
else:
jc = u''
if sfxl or jc:
FL.append((ffx,sfxl+jc))
sfxr = sfx[pos-cc:]
FR = FW[fx+1:]
if sfxr: FR.insert(0,(ffx,sfxr))
h = _rebuildFragWord(FL)
hw = h[0]
ohw = stringWidth(sfxl,ffx.fontName,ffx.fontSize)
t = [ww-ohw]+FR
tw = w0+hw
if tw<=maxWidth:
return jc,hw-ohw,hw,ww-ohw,h,t
#even though the above tries for words with '-' it may be that no split ended with '-'
#so this may succeed where the above does not
n = len(H)
if n>=3 and embeddedHyphenation and u'' not in H and _hy_shy_letters_pat.match(s):
for i in reversed(xrange(2,n,2)):
pos = len(pfx + u''.join(H[:i]))
fx, cc = X[pos]
#FW[fx] is split
FL = FW[1:fx]
ffx, sfx = FW[fx]
sfxl = sfx[:pos-cc]
if sfxl: FL.append((ffx,sfxl))
sfxr = sfx[pos-cc:]
FR = FW[fx+1:]
if sfxr: FR.insert(0,(ffx,sfxr))
h = _rebuildFragWord(FL)
hw = h[0]
t = [ww-hw]+FR
tw = w0+hw
if tw<=maxWidth:
return u'',0,hw,ww-hw,h,t
def _hyphenateFragWord(hyphenator,FW,newWidth,maxWidth,uriWasteReduce,embeddedHyphenation,
hymwl=hyphenationMinWordLength):
ww = FW[0]
if ww==0: return []
if len(FW)==2:
f, s = FW[1]
R = _hyGenPair(hyphenator, s, ww, newWidth, maxWidth, f.fontName, f.fontSize,uriWasteReduce,embeddedHyphenation, hymwl)
if R:
jc, hylen, hw, tw, h, t = R
return [(_SplitFragHY if jc else _SplitFragH)([hw+hylen,(f,h+jc)]),(_SplitFragHS if isinstance(FW,_HSFrag) else _SplitFrag)([tw,(f,t)])]
else:
R = _hyGenFragsPair(hyphenator, FW, newWidth, maxWidth,uriWasteReduce,embeddedHyphenation, hymwl)
if R:
jc, hylen, hw, tw, h, t = R
return [(_SplitFragHY if jc else _SplitFragH)(h),(_SplitFragHS if isinstance(FW,_HSFrag) else _SplitFrag)(t)]
return None
class _SplitWord(unicodeT):
pass
class _SplitWordH(unicodeT):
pass
class _SplitWordHY(_SplitWordH):
'''head part of a hyphenation word pair'''
pass
def _hyphenateWord(hyphenator,fontName,fontSize,w,ww,newWidth,maxWidth, uriWasteReduce,embeddedHyphenation,
hymwl=hyphenationMinWordLength):
if ww==0: return []
R = _hyGenPair(hyphenator, w, ww, newWidth, maxWidth, fontName, fontSize, uriWasteReduce,embeddedHyphenation, hymwl)
if R:
hy, hylen, hw, tw, h, t = R
return [(_SplitWordHY if hy else _SplitWordH)(h+hy),_SplitWord(t)]
def _splitWord(w,maxWidth,maxWidths,lineno,fontName,fontSize,encoding='utf8'):
'''
split w into words that fit in lines of length
maxWidth
maxWidths[lineno+1]
.....
maxWidths[lineno+n]
then push those new words onto words
'''
#TODO fix this to use binary search for the split points
R = []
aR = R.append
maxlineno = len(maxWidths)-1
lineWidth = 0
wordText = u''
if isBytes(w):
w = w.decode(encoding)
for c in w:
cw = stringWidth(c,fontName,fontSize,encoding)
newLineWidth = lineWidth+cw
if newLineWidth>maxWidth:
aR(_SplitWord(wordText))
lineno += 1
maxWidth = maxWidths[min(maxlineno,lineno)]
newLineWidth = cw
wordText = u''
wordText += c
lineWidth = newLineWidth
aR(_SplitWord(wordText))
return [c for c in R if c]
def _yieldBLParaWords(blPara,start,stop):
state = 0
R = []
aR = R.append
for l in blPara.lines[start:stop]:
for w in l[1]:
if isinstance(w,_SplitWord):
if R and isinstance(R[-1],_SplitWordHY):
R[-1] = R[-1][:-1] #remove unwanted -
aR(w)
continue
else:
if R:
yield ''.join(R)
del R[:]
yield w
if R:
yield ''.join(R)
def _split_blParaSimple(blPara,start,stop):
f = blPara.clone()
for a in ('lines', 'kind', 'text'):
if hasattr(f,a): delattr(f,a)
f.words = list(_yieldBLParaWords(blPara,start,stop))
return [f]
def _split_blParaHard(blPara,start,stop):
f = []
lines = blPara.lines[start:stop]
for l in lines:
for w in l.words:
f.append(w)
if l is not lines[-1]:
i = len(f)-1
while i>=0 and hasattr(f[i],'cbDefn') and not getattr(f[i].cbDefn,'width',0): i -= 1
if i>=0:
g = f[i]
if not g.text: g.text = ' '
elif g.text[-1]!=' ': g.text += ' '
return f
def _drawBullet(canvas, offset, cur_y, bulletText, style, rtl):
'''draw a bullet text could be a simple string or a frag list'''
bulletAnchor = style.bulletAnchor
if rtl or style.bulletAnchor!='start':
numeric = bulletAnchor=='numeric'
if isStr(bulletText):
t = bulletText
q = numeric and decimalSymbol in t
if q: t = t[:t.index(decimalSymbol)]
bulletWidth = stringWidth(t, style.bulletFontName, style.bulletFontSize)
if q: bulletWidth += 0.5 * stringWidth(decimalSymbol, style.bulletFontName, style.bulletFontSize)
else:
#it's a list of fragments
bulletWidth = 0
for f in bulletText:
t = f.text
q = numeric and decimalSymbol in t
if q:
t = t[:t.index(decimalSymbol)]
bulletWidth += 0.5 * stringWidth(decimalSymbol, f.fontName, f.fontSize)
bulletWidth += stringWidth(t, f.fontName, f.fontSize)
if q:
break
else:
bulletWidth = 0
if bulletAnchor=='middle': bulletWidth *= 0.5
cur_y += getattr(style,"bulletOffsetY",0)
if not rtl:
tx2 = canvas.beginText(style.bulletIndent-bulletWidth,cur_y)
else:
width = rtl[0]
bulletStart = width+style.rightIndent-(style.bulletIndent+bulletWidth)
tx2 = canvas.beginText(bulletStart, cur_y)
tx2.setFont(style.bulletFontName, style.bulletFontSize)
tx2.setFillColor(getattr(style,'bulletColor',style.textColor))
if isStr(bulletText):
tx2.textOut(bulletText)
else:
for f in bulletText:
tx2.setFont(f.fontName, f.fontSize)
tx2.setFillColor(f.textColor)
tx2.textOut(f.text)
canvas.drawText(tx2)
if not rtl:
#AR making definition lists a bit less ugly
#bulletEnd = tx2.getX()
bulletEnd = tx2.getX() + style.bulletFontSize * 0.6
offset = max(offset,bulletEnd - style.leftIndent)
return offset
def _handleBulletWidth(bulletText,style,maxWidths):
'''work out bullet width and adjust maxWidths[0] if neccessary
'''
if bulletText:
if isStr(bulletText):
bulletWidth = stringWidth( bulletText, style.bulletFontName, style.bulletFontSize)
else:
#it's a list of fragments
bulletWidth = 0
for f in bulletText:
bulletWidth += stringWidth(f.text, f.fontName, f.fontSize)
bulletLen = style.bulletIndent + bulletWidth + 0.6 * style.bulletFontSize
if style.wordWrap=='RTL':
indent = style.rightIndent+style.firstLineIndent
else:
indent = style.leftIndent+style.firstLineIndent
if bulletLen > indent:
#..then it overruns, and we have less space available on line 1
maxWidths[0] -= (bulletLen - indent)
def splitLines0(frags,widths):
'''
given a list of ParaFrags we return a list of ParaLines
each ParaLine has
1) ExtraSpace
2) blankCount
3) [textDefns....]
each text definition is a (ParaFrag, start, limit) triplet
'''
#initialise the algorithm
lines = []
lineNum = 0
maxW = widths[lineNum]
i = -1
l = len(frags)
lim = start = 0
while 1:
#find a non whitespace character
while i<l:
while start<lim and text[start]==' ': start += 1
if start==lim:
i += 1
if i==l: break
start = 0
f = frags[i]
text = f.text
lim = len(text)
else:
break # we found one
if start==lim: break #if we didn't find one we are done
#start of a line
g = (None,None,None)
line = []
cLen = 0
nSpaces = 0
while cLen<maxW:
j = text.find(' ',start)
if j<0: j==lim
w = stringWidth(text[start:j],f.fontName,f.fontSize)
cLen += w
if cLen>maxW and line!=[]:
cLen = cLen-w
#this is the end of the line
while g.text[lim]==' ':
lim = lim - 1
nSpaces = nSpaces-1
break
if j<0: j = lim
if g[0] is f: g[2] = j #extend
else:
g = (f,start,j)
line.append(g)
if j==lim:
i += 1
def _do_line(tx, x1, y1, x2, y2, nlw, nsc):
canv = tx._canvas
olw = canv._lineWidth
if nlw!=olw:
canv.setLineWidth(nlw)
osc = canv._strokeColorObj
if nsc!=osc:
canv.setStrokeColor(nsc)
canv.line(x1, y1, x2, y2)
def _do_under_line(i, x1, ws, tx, us_lines):
xs = tx.XtraState
style = xs.style
y0 = xs.cur_y - i*style.leading
f = xs.f
fs = f.fontSize
tc = f.textColor
values = dict(L=fs,F=fs,f=fs)
dw = tx._defaultLineWidth
x2 = x1 + tx._canvas.stringWidth(' '.join(tx.XtraState.lines[i][1]), tx._fontname, fs)
for n,k,c,w,o,r,m,g in us_lines:
underline = k=='underline'
lw = _usConv(w,values,default=tx._defaultLineWidth)
lg = _usConv(g,values,default=1)
dy = lg+lw
if not underline: dy = -dy
y = y0 + r + _usConv(('-0.125*L' if underline else '0.25*L') if o=='' else o,values)
if not c: c = tc
while m>0:
tx._do_line(x1, y, x2, y, lw, c)
y -= dy
m -= 1
_scheme_re = re.compile('^[a-zA-Z][-+a-zA-Z0-9]+$')
def _doLink(tx,link,rect):
if not link: return
if link.startswith('#'):
tx._canvas.linkRect("", link[1:], rect, relative=1)
else:
parts = link.split(':',1)
scheme = len(parts)==2 and parts[0].lower() or ''
if scheme=='document':
tx._canvas.linkRect("", parts[1], rect, relative=1)
elif _scheme_re.match(scheme):
kind=scheme.lower()=='pdf' and 'GoToR' or 'URI'
if kind=='GoToR': link = parts[1]
tx._canvas.linkURL(link, rect, relative=1, kind=kind)
else:
tx._canvas.linkURL(link, rect, relative=1, kind='URI')
def _do_link_line(i, t_off, ws, tx):
xs = tx.XtraState
leading = xs.style.leading
y = xs.cur_y - i*leading - xs.f.fontSize/8.0 # 8.0 factor copied from para.py
text = ' '.join(xs.lines[i][1])
textlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize)
for n, link in xs.link:
_doLink(tx, link, (t_off, y, t_off+textlen, y+leading))
def _do_post_text(tx):
xs = tx.XtraState
y0 = xs.cur_y
f = xs.f
leading = xs.style.leading
autoLeading = xs.autoLeading
fontSize = f.fontSize
if autoLeading=='max':
leading = max(leading,1.2*fontSize)
elif autoLeading=='min':
leading = 1.2*fontSize
if xs.backColors:
yl = y0 + fontSize
ydesc = yl - leading
for x1,x2,c in xs.backColors:
tx._canvas.setFillColor(c)
tx._canvas.rect(x1,ydesc,x2-x1,leading,stroke=0,fill=1)
xs.backColors=[]
xs.backColor=None
for (((n,link),x1),lo,hi),x2 in sorted(xs.links.values()):
_doLink(tx, link, (x1, y0+lo, x2, y0+hi))
xs.links = {}
if xs.us_lines:
#print 'lines'
dw = tx._defaultLineWidth
values = dict(L=fontSize)
for (((n,k,c,w,o,r,m,g),fs,tc,x1),fsmax),x2 in sorted(xs.us_lines.values()):
underline = k=='underline'
values['f'] = fs
values['F'] = fsmax
lw = _usConv(w,values,default=tx._defaultLineWidth)
lg = _usConv(g,values,default=1)
dy = lg+lw
if not underline: dy = -dy
y = y0 + r + _usConv(o if o!='' else ('-0.125*L' if underline else '0.25*L'),values)
#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)
if not c: c = tc
while m>0:
tx._do_line(x1, y, x2, y, lw, c)
y -= dy
m -= 1
xs.us_lines = {}
xs.cur_y -= leading
def textTransformFrags(frags,style):
tt = style.textTransform
if tt:
tt=tt.lower()
if tt=='lowercase':
tt = unicodeT.lower
elif tt=='uppercase':
tt = unicodeT.upper
elif tt=='capitalize':
tt = unicodeT.title
elif tt=='none':
return
else:
raise ValueError('ParaStyle.textTransform value %r is invalid' % style.textTransform)
n = len(frags)
if n==1:
#single fragment the easy case
frags[0].text = tt(frags[0].text)
elif tt is unicodeT.title:
pb = True
for f in frags:
u = f.text
if not u: continue
if u.startswith(u' ') or pb:
u = tt(u)
else:
i = u.find(u' ')
if i>=0:
u = u[:i]+tt(u[i:])
pb = u.endswith(u' ')
f.text = u
else:
for f in frags:
u = f.text
if not u: continue
f.text = tt(u)
class cjkU(unicodeT):
'''simple class to hold the frag corresponding to a str'''
def __new__(cls,value,frag,encoding):
self = unicodeT.__new__(cls,value)
self._frag = frag
if hasattr(frag,'cbDefn'):
w = getattr(frag.cbDefn,'width',0)
self._width = w
else:
self._width = stringWidth(value,frag.fontName,frag.fontSize)
return self
frag = property(lambda self: self._frag)
width = property(lambda self: self._width)
def makeCJKParaLine(U,maxWidth,widthUsed,extraSpace,lineBreak,calcBounds):
words = []
CW = []
f0 = FragLine()
maxSize = maxAscent = minDescent = 0
for u in U:
f = u.frag
fontSize = f.fontSize
if calcBounds:
cbDefn = getattr(f,'cbDefn',None)
if getattr(cbDefn,'width',0):
descent, ascent = imgVRange(imgNormV(cbDefn.height,fontSize),cbDefn.valign,fontSize)
else:
ascent, descent = getAscentDescent(f.fontName,fontSize)
else:
ascent, descent = getAscentDescent(f.fontName,fontSize)
maxSize = max(maxSize,fontSize)
maxAscent = max(maxAscent,ascent)
minDescent = min(minDescent,descent)
if not sameFrag(f0,f):
f0=f0.clone()
f0.text = u''.join(CW)
words.append(f0)
CW = []
f0 = f
CW.append(u)
if CW:
f0=f0.clone()
f0.text = u''.join(CW)
words.append(f0)
return FragLine(kind=1,extraSpace=extraSpace,wordCount=1,words=words[1:],fontSize=maxSize,ascent=maxAscent,descent=minDescent,maxWidth=maxWidth,currentWidth=widthUsed,lineBreak=lineBreak)
def cjkFragSplit(frags, maxWidths, calcBounds, encoding='utf8'):
'''This attempts to be wordSplit for frags using the dumb algorithm'''
U = [] #get a list of single glyphs with their widths etc etc
for f in frags:
text = f.text
if isBytes(text):
text = text.decode(encoding)
if text:
U.extend([cjkU(t,f,encoding) for t in text])
else:
U.append(cjkU(text,f,encoding))
lines = []
i = widthUsed = lineStartPos = 0
maxWidth = maxWidths[0]
nU = len(U)
while i<nU:
u = U[i]
i += 1
w = u.width
if hasattr(w,'normalizedValue'):
w._normalizer = maxWidth
w = w.normalizedValue(maxWidth)
widthUsed += w
lineBreak = hasattr(u.frag,'lineBreak')
endLine = (widthUsed>maxWidth + _FUZZ and widthUsed>0) or lineBreak
if endLine:
extraSpace = maxWidth - widthUsed
if not lineBreak:
if ord(u)<0x3000:
# we appear to be inside a non-Asian script section.
# (this is a very crude test but quick to compute).
# This is likely to be quite rare so the speed of the
# code below is hopefully not a big issue. The main
# situation requiring this is that a document title
# with an english product name in it got cut.
# we count back and look for
# - a space-like character
# - reversion to Kanji (which would be a good split point)
# - in the worst case, roughly half way back along the line
limitCheck = (lineStartPos+i)>>1 #(arbitrary taste issue)
for j in xrange(i-1,limitCheck,-1):
uj = U[j]
if uj and category(uj)=='Zs' or ord(uj)>=0x3000:
k = j+1
if k<i:
j = k+1
extraSpace += sum(U[ii].width for ii in xrange(j,i))
w = U[k].width
u = U[k]
i = j
break
#we are pushing this character back, but
#the most important of the Japanese typography rules
#if this character cannot start a line, wrap it up to this line so it hangs
#in the right margin. We won't do two or more though - that's unlikely and
#would result in growing ugliness.
#and increase the extra space
#bug fix contributed by Alexander Vasilenko <alexs.vasilenko@gmail.com>
if u not in ALL_CANNOT_START and i>lineStartPos+1:
#otherwise we need to push the character back
#the i>lineStart+1 condition ensures progress
i -= 1
extraSpace += w
lines.append(makeCJKParaLine(U[lineStartPos:i],maxWidth,widthUsed,extraSpace,lineBreak,calcBounds))
try:
maxWidth = maxWidths[len(lines)]
except IndexError:
maxWidth = maxWidths[-1] # use the last one
lineStartPos = i
widthUsed = 0
#any characters left?
if widthUsed > 0:
lines.append(makeCJKParaLine(U[lineStartPos:],maxWidth,widthUsed,maxWidth-widthUsed,False,calcBounds))
return ParaLines(kind=1,lines=lines)
class Paragraph(Flowable):
""" Paragraph(text, style, bulletText=None, caseSensitive=1)
text a string of stuff to go into the paragraph.
style is a style definition as in reportlab.lib.styles.
bulletText is an optional bullet defintion.
caseSensitive set this to 0 if you want the markup tags and their attributes to be case-insensitive.
This class is a flowable that can format a block of text
into a paragraph with a given style.
The paragraph Text can contain XML-like markup including the tags:
<b> ... </b> - bold
< u [color="red"] [width="pts"] [offset="pts"]> < /u > - underline
width and offset can be empty meaning use existing canvas line width
or with an f/F suffix regarded as a fraction of the font size
< strike > < /strike > - strike through has the same parameters as underline
<i> ... </i> - italics
<u> ... </u> - underline
<strike> ... </strike> - strike through
<super> ... </super> - superscript
<sub> ... </sub> - subscript
<font name=fontfamily/fontname color=colorname size=float>
<span name=fontfamily/fontname color=colorname backcolor=colorname size=float style=stylename>
<onDraw name=callable label="a label"/>
<index [name="callablecanvasattribute"] label="a label"/>
<link>link text</link>
attributes of links
size/fontSize/uwidth/uoffset=num
name/face/fontName=name
fg/textColor/color/ucolor=color
backcolor/backColor/bgcolor=color
dest/destination/target/href/link=target
underline=bool turn on underline
<a>anchor text</a>
attributes of anchors
size/fontSize/uwidth/uoffset=num
fontName=name
fg/textColor/color/ucolor=color
backcolor/backColor/bgcolor=color
href=href
underline="yes|no"
<a name="anchorpoint"/>
<unichar name="unicode character name"/>
<unichar value="unicode code point"/>
<img src="path" width="1in" height="1in" valign="bottom"/>
width="w%" --> fontSize*w/100 idea from Roberto Alsina
height="h%" --> linewidth*h/100 <ralsina@netmanagers.com.ar>
The whole may be surrounded by <para> </para> tags
The <b> and <i> tags will work for the built-in fonts (Helvetica
/Times / Courier). For other fonts you need to register a family
of 4 fonts using reportlab.pdfbase.pdfmetrics.registerFont; then
use the addMapping function to tell the library that these 4 fonts
form a family e.g.
from reportlab.lib.fonts import addMapping
addMapping('Vera', 0, 0, 'Vera') #normal
addMapping('Vera', 0, 1, 'Vera-Italic') #italic
addMapping('Vera', 1, 0, 'Vera-Bold') #bold
addMapping('Vera', 1, 1, 'Vera-BoldItalic') #italic and bold
It will also be able to handle any MathML specified Greek characters.
"""
def __init__(self, text, style, bulletText = None, frags=None, caseSensitive=1, encoding='utf8'):
self.caseSensitive = caseSensitive
self.encoding = encoding
self._setup(text, style, bulletText or getattr(style,'bulletText',None), frags, cleanBlockQuotedText)
def __repr__(self):
n = self.__class__.__name__
L = [n+"("]
keys = list(self.__dict__.keys())
for k in keys:
L.append('%s: %s' % (repr(k).replace("\n", " ").replace(" "," "),repr(getattr(self, k)).replace("\n", " ").replace(" "," ")))
L.append(") #"+n)
return '\n'.join(L)
def _setup(self, text, style, bulletText, frags, cleaner):
#This used to be a global parser to save overhead.
#In the interests of thread safety it is being instantiated per paragraph.
#On the next release, we'll replace with a cElementTree parser
if frags is None:
text = cleaner(text)
_parser = ParaParser()
_parser.caseSensitive = self.caseSensitive
style, frags, bulletTextFrags = _parser.parse(text,style)
if frags is None:
raise ValueError("xml parser error (%s) in paragraph beginning\n'%s'"\
% (_parser.errors[0],text[:min(30,len(text))]))
textTransformFrags(frags,style)
if bulletTextFrags: bulletText = bulletTextFrags
#AR hack
self.text = text
self.frags = frags #either the parse fragments or frag word list
self.style = style
self.bulletText = bulletText
self.debug = 0 #turn this on to see a pretty one with all the margins etc.
def wrap(self, availWidth, availHeight):
if availWidth<_FUZZ:
#we cannot fit here
return 0, 0x7fffffff
# work out widths array for breaking
self.width = availWidth
style = self.style
leftIndent = style.leftIndent
first_line_width = availWidth - (leftIndent+style.firstLineIndent) - style.rightIndent
later_widths = availWidth - leftIndent - style.rightIndent
self._wrapWidths = [first_line_width, later_widths]
if style.wordWrap == 'CJK':
#use Asian text wrap algorithm to break characters
blPara = self.breakLinesCJK(self._wrapWidths)
else:
blPara = self.breakLines(self._wrapWidths)
self.blPara = blPara
autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
leading = style.leading
if blPara.kind==1:
if autoLeading not in ('','off'):
height = 0
if autoLeading=='max':
for l in blPara.lines:
height += max(l.ascent-l.descent,leading)
elif autoLeading=='min':
for l in blPara.lines:
height += l.ascent - l.descent
else:
raise ValueError('invalid autoLeading value %r' % autoLeading)
else:
height = len(blPara.lines) * leading
else:
if autoLeading=='max':
leading = max(leading,blPara.ascent-blPara.descent)
elif autoLeading=='min':
leading = blPara.ascent-blPara.descent
height = len(blPara.lines) * leading
self.height = height
return self.width, height
def minWidth(self):
'Attempt to determine a minimum sensible width'
frags = self.frags
nFrags= len(frags)
if not nFrags: return 0
if nFrags==1 and not _processed_frags(frags):
f = frags[0]
fS = f.fontSize
fN = f.fontName
return max(stringWidth(w,fN,fS) for w in (split(f.text, ' ') if hasattr(f,'text') else f.words))
else:
return max(w[0] for w in _getFragWords(frags))
def _split_blParaProcessed(self,blPara,start,stop):
if not stop: return []
lines = blPara.lines
sFW = lines[start].sFW
sFWN = lines[stop].sFW if stop!=len(lines) else len(self.frags)
return self.frags[sFW:sFWN]
def _get_split_blParaFunc(self):
return (_split_blParaSimple if self.blPara.kind==0
else (_split_blParaHard if not _processed_frags(self.frags)
else self._split_blParaProcessed))
def split(self,availWidth, availHeight):
if len(self.frags)<=0 or availWidth<_FUZZ or availHeight<_FUZZ: return []
#the split information is all inside self.blPara
if not hasattr(self,'blPara'):
self.wrap(availWidth,availHeight)
blPara = self.blPara
style = self.style
autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
leading = style.leading
lines = blPara.lines
if blPara.kind==1 and autoLeading not in ('','off'):
s = height = 0
if autoLeading=='max':
for i,l in enumerate(blPara.lines):
h = max(l.ascent-l.descent,leading)
n = height+h
if n>availHeight+1e-8:
break
height = n
s = i+1
elif autoLeading=='min':
for i,l in enumerate(blPara.lines):
n = height+l.ascent-l.descent
if n>availHeight+1e-8:
break
height = n
s = i+1
else:
raise ValueError('invalid autoLeading value %r' % autoLeading)
else:
l = leading
if autoLeading=='max':
l = max(leading,1.2*style.fontSize)
elif autoLeading=='min':
l = 1.2*style.fontSize
s = int(availHeight/float(l))
height = s*l
allowOrphans = getattr(self,'allowOrphans',getattr(style,'allowOrphans',0))
if (not allowOrphans and s<=1) or s==0: #orphan or not enough room
del self.blPara
return []
n = len(lines)
allowWidows = getattr(self,'allowWidows',getattr(style,'allowWidows',1))
if n<=s:
return [self]
if not allowWidows:
if n==s+1: #widow?
if (allowOrphans and n==3) or n>3:
s -= 1 #give the widow some company
else:
del self.blPara #no room for adjustment; force the whole para onwards
return []
func = self._get_split_blParaFunc()
if style.endDots:
style1 = deepcopy(style)
style1.endDots = None
else:
style1 = style
P1=self.__class__(None,style1,bulletText=self.bulletText,frags=func(blPara,0,s))
#this is a major hack
P1.blPara = ParaLines(kind=1,lines=blPara.lines[0:s],aH=availHeight,aW=availWidth)
#do not justify text if linebreak was inserted after the text
#bug reported and fix contributed by Niharika Singh <nsingh@shoobx.com>
P1._JustifyLast = not (isinstance(blPara.lines[s-1],FragLine)
and hasattr(blPara.lines[s-1], 'lineBreak')
and blPara.lines[s-1].lineBreak)
P1._splitpara = 1
P1.height = height
P1.width = availWidth
if style.firstLineIndent != 0:
style = deepcopy(style)
style.firstLineIndent = 0
P2=self.__class__(None,style,bulletText=None,frags=func(blPara,s,n))
#propagate attributes that might be on self; suggestion from Dirk Holtwick
for a in ('autoLeading', #possible attributes that might be directly on self.
):
if hasattr(self,a):
setattr(P1,a,getattr(self,a))
setattr(P2,a,getattr(self,a))
return [P1,P2]
def draw(self):
#call another method for historical reasons. Besides, I
#suspect I will be playing with alternate drawing routines
#so not doing it here makes it easier to switch.
self.drawPara(self.debug)
def breakLines(self, width):
"""
Returns a broken line structure. There are two cases
A) For the simple case of a single formatting input fragment the output is
A fragment specifier with
- kind = 0
- fontName, fontSize, leading, textColor
- lines= A list of lines
Each line has two items.
1. unused width in points
2. word list
B) When there is more than one input formatting fragment the output is
A fragment specifier with
- kind = 1
- lines= A list of fragments each having fields
- extraspace (needed for justified)
- fontSize
- words=word list
each word is itself a fragment with
various settings
in addition frags becomes a frag word list
This structure can be used to easily draw paragraphs with the various alignments.
You can supply either a single width or a list of widths; the latter will have its
last item repeated until necessary. A 2-element list is useful when there is a
different first line indent; a longer list could be created to facilitate custom wraps
around irregular objects."""
self._width_max = 0
if not isinstance(width,(tuple,list)): maxWidths = [width]
else: maxWidths = width
lines = []
self.height = lineno = 0
maxlineno = len(maxWidths)-1
style = self.style
hyphenator = getattr(style,'hyphenationLang','')
if hyphenator:
if isStr(hyphenator):
hyphenator = hyphenator.strip()
if hyphenator and pyphen:
hyphenator = pyphen.Pyphen(lang=hyphenator).iterate
else:
hyphenator = None
elif not callable(hyphenator):
raise ValueError('hyphenator should be a language spec or a callable unicode --> pairs not %r' % hyphenator)
else:
hyphenator = None
uriWasteReduce = style.uriWasteReduce
embeddedHyphenation = style.embeddedHyphenation
spaceShrinkage = style.spaceShrinkage
splitLongWords = style.splitLongWords
attemptHyphenation = hyphenator or uriWasteReduce or embeddedHyphenation
if attemptHyphenation:
hymwl = getattr(style,'hyphenationMinWordLength',hyphenationMinWordLength)
self._splitLongWordCount = self._hyphenations = 0
#for bullets, work out width and ensure we wrap the right amount onto line one
_handleBulletWidth(self.bulletText,style,maxWidths)
maxWidth = maxWidths[0]
autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
calcBounds = autoLeading not in ('','off')
frags = self.frags
nFrags= len(frags)
if (nFrags==1
and not (style.endDots or hasattr(frags[0],'cbDefn') or hasattr(frags[0],'backColor')
or _processed_frags(frags))):
f = frags[0]
fontSize = f.fontSize
fontName = f.fontName
ascent, descent = getAscentDescent(fontName,fontSize)
if hasattr(f,'text'):
text = strip(f.text)
if not text:
return f.clone(kind=0, lines=[],ascent=ascent,descent=descent,fontSize=fontSize)
else:
words = split(text)
else:
words = f.words[:]
for w in words:
if strip(w): break
else:
return f.clone(kind=0, lines=[],ascent=ascent,descent=descent,fontSize=fontSize)
spaceWidth = stringWidth(' ', fontName, fontSize, self.encoding)
dSpaceShrink = spaceShrinkage*spaceWidth
spaceShrink = 0
cLine = []
currentWidth = -spaceWidth # hack to get around extra space for word 1
while words:
word = words.pop(0)
#this underscores my feeling that Unicode throughout would be easier!
wordWidth = stringWidth(word, fontName, fontSize, self.encoding)
newWidth = currentWidth + spaceWidth + wordWidth
if newWidth>maxWidth+spaceShrink and not isinstance(word,_SplitWordH):
if attemptHyphenation:
hyOk = not getattr(f,'nobr',False)
hsw = _hyphenateWord(hyphenator if hyOk else None,
fontName, fontSize, word, wordWidth, newWidth, maxWidth+spaceShrink,
uriWasteReduce if hyOk else False,
embeddedHyphenation and hyOk, hymwl)
if hsw:
words[0:0] = hsw
self._hyphenations += 1
continue
if splitLongWords and not isinstance(word,_SplitWord):
nmw = min(lineno,maxlineno)
if wordWidth>max(maxWidths[nmw:nmw+1]):
#a long word
words[0:0] = _splitWord(word,maxWidth-spaceWidth-currentWidth,maxWidths,lineno,fontName,fontSize,self.encoding)
self._splitLongWordCount += 1
continue
if newWidth <= (maxWidth+spaceShrink) or not len(cLine):
# fit one more on this line
cLine.append(word)
currentWidth = newWidth
spaceShrink += dSpaceShrink
else:
if currentWidth > self._width_max: self._width_max = currentWidth
#end of line
lines.append((maxWidth - currentWidth, cLine))
cLine = [word]
spaceShrink = 0
currentWidth = wordWidth
lineno += 1
maxWidth = maxWidths[min(maxlineno,lineno)]
#deal with any leftovers on the final line
if cLine!=[]:
if currentWidth>self._width_max: self._width_max = currentWidth
lines.append((maxWidth - currentWidth, cLine))
return f.clone(kind=0, lines=lines,ascent=ascent,descent=descent,fontSize=fontSize)
elif nFrags<=0:
return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName,
textColor=style.textColor, ascent=style.fontSize,descent=-0.2*style.fontSize,
lines=[])
else:
njlbv = not style.justifyBreaks
words = []
FW = []
aFW = FW.append
_words = _getFragWords(frags,maxWidth)
sFW = 0
while _words:
w = _words.pop(0)
aFW(w)
f = w[-1][0]
fontName = f.fontName
fontSize = f.fontSize
if not words:
n = dSpaceShrink = spaceShrink = spaceWidth = currentWidth = 0
maxSize = fontSize
maxAscent, minDescent = getAscentDescent(fontName,fontSize)
wordWidth = w[0]
f = w[1][0]
if wordWidth>0:
newWidth = currentWidth + spaceWidth + wordWidth
else:
newWidth = currentWidth
#test to see if this frag is a line break. If it is we will only act on it
#if the current width is non-negative or the previous thing was a deliberate lineBreak
lineBreak = f._fkind==_FK_BREAK
if not lineBreak and newWidth>(maxWidth+spaceShrink) and not isinstance(w,_SplitFragH):
if attemptHyphenation:
hyOk = not getattr(f,'nobr',False)
hsw = _hyphenateFragWord(hyphenator if hyOk else None,
w,newWidth,maxWidth+spaceShrink,
uriWasteReduce if hyOk else False,
embeddedHyphenation and hyOk, hymwl)
if hsw:
_words[0:0] = hsw
FW.pop(-1) #remove this as we are doing this one again
self._hyphenations += 1
continue
if splitLongWords and not isinstance(w,_SplitFrag):
nmw = min(lineno,maxlineno)
if wordWidth>max(maxWidths[nmw:nmw+1]):
#a long word
_words[0:0] = _splitFragWord(w,maxWidth-spaceWidth-currentWidth,maxWidths,lineno)
FW.pop(-1) #remove this as we are doing this one again
self._splitLongWordCount += 1
continue
endLine = (newWidth>(maxWidth+spaceShrink) and n>0) or lineBreak
if not endLine:
if lineBreak: continue #throw it away
nText = w[1][1]
if nText: n += 1
fontSize = f.fontSize
if calcBounds:
if f._fkind==_FK_IMG:
descent,ascent = imgVRange(imgNormV(f.cbDefn.height,fontSize),f.cbDefn.valign,fontSize)
else:
ascent, descent = getAscentDescent(f.fontName,fontSize)
else:
ascent, descent = getAscentDescent(f.fontName,fontSize)
maxSize = max(maxSize,fontSize)
maxAscent = max(maxAscent,ascent)
minDescent = min(minDescent,descent)
if not words:
g = f.clone()
words = [g]
g.text = nText
elif not sameFrag(g,f):
if spaceWidth:
i = len(words)-1
while i>=0:
wi = words[i]
i -= 1
if wi._fkind==_FK_TEXT:
if not wi.text.endswith(' '):
wi.text += ' '
spaceShrink += dSpaceShrink
break
g = f.clone()
words.append(g)
g.text = nText
elif spaceWidth:
if not g.text.endswith(' '):
g.text += ' ' + nText
spaceShrink += dSpaceShrink
else:
g.text += nText
else:
g.text += nText
spaceWidth = stringWidth(' ',fontName,fontSize) if isinstance(w,_HSFrag) else 0 #of the space following this word
dSpaceShrink = spaceWidth*spaceShrinkage
ni = 0
for i in w[2:]:
g = i[0].clone()
g.text=i[1]
if g.text: ni = 1
words.append(g)
fontSize = g.fontSize
if calcBounds:
if g._fkind==_FK_IMG:
descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
else:
ascent, descent = getAscentDescent(g.fontName,fontSize)
else:
ascent, descent = getAscentDescent(g.fontName,fontSize)
maxSize = max(maxSize,fontSize)
maxAscent = max(maxAscent,ascent)
minDescent = min(minDescent,descent)
if not nText and ni:
#one bit at least of the word was real
n+=1
currentWidth = newWidth
else: #either it won't fit, or it's a lineBreak tag
if lineBreak:
g = f.clone()
#del g.lineBreak
words.append(g)
if currentWidth>self._width_max: self._width_max = currentWidth
#end of line
lines.append(FragLine(extraSpace=maxWidth-currentWidth, wordCount=n,
lineBreak=lineBreak and njlbv, words=words, fontSize=maxSize, ascent=maxAscent, descent=minDescent, maxWidth=maxWidth,
sFW=sFW))
sFW = len(FW)-1
#start new line
lineno += 1
maxWidth = maxWidths[min(maxlineno,lineno)]
if lineBreak:
words = []
continue
spaceWidth = stringWidth(' ',fontName,fontSize) if isinstance(w,_HSFrag) else 0 #of the space following this word
dSpaceShrink = spaceWidth*spaceShrinkage
currentWidth = wordWidth
n = 1
spaceShrink = 0
g = f.clone()
maxSize = g.fontSize
if calcBounds:
if g._fkind==_FK_IMG:
descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
else:
maxAscent, minDescent = getAscentDescent(g.fontName,maxSize)
else:
maxAscent, minDescent = getAscentDescent(g.fontName,maxSize)
words = [g]
g.text = w[1][1]
for i in w[2:]:
g = i[0].clone()
g.text=i[1]
words.append(g)
fontSize = g.fontSize
if calcBounds:
if g._fkind==_FK_IMG:
descent,ascent = imgVRange(imgNormV(g.cbDefn.height,fontSize),g.cbDefn.valign,fontSize)
else:
ascent, descent = getAscentDescent(g.fontName,fontSize)
else:
ascent, descent = getAscentDescent(g.fontName,fontSize)
maxSize = max(maxSize,fontSize)
maxAscent = max(maxAscent,ascent)
minDescent = min(minDescent,descent)
#deal with any leftovers on the final line
if words:
if currentWidth>self._width_max: self._width_max = currentWidth
lines.append(ParaLines(extraSpace=(maxWidth - currentWidth),wordCount=n,lineBreak=False,
words=words, fontSize=maxSize,ascent=maxAscent,descent=minDescent,maxWidth=maxWidth,sFW=sFW))
self.frags = FW
return ParaLines(kind=1, lines=lines)
def breakLinesCJK(self, maxWidths):
"""Initially, the dumbest possible wrapping algorithm.
Cannot handle font variations."""
if not isinstance(maxWidths,(list,tuple)): maxWidths = [maxWidths]
style = self.style
self.height = 0
#for bullets, work out width and ensure we wrap the right amount onto line one
_handleBulletWidth(self.bulletText, style, maxWidths)
frags = self.frags
nFrags = len(frags)
if nFrags==1 and not hasattr(frags[0],'cbDefn') and not style.endDots:
f = frags[0]
if hasattr(self,'blPara') and getattr(self,'_splitpara',0):
return f.clone(kind=0, lines=self.blPara.lines)
#single frag case
lines = []
lineno = 0
if hasattr(f,'text'):
text = f.text
else:
text = ''.join(getattr(f,'words',[]))
from reportlab.lib.textsplit import wordSplit
lines = wordSplit(text, maxWidths, f.fontName, f.fontSize)
#the paragraph drawing routine assumes multiple frags per line, so we need an
#extra list like this
# [space, [text]]
#
wrappedLines = [(sp, [line]) for (sp, line) in lines]
return f.clone(kind=0, lines=wrappedLines, ascent=f.fontSize, descent=-0.2*f.fontSize)
elif nFrags<=0:
return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName,
textColor=style.textColor, lines=[],ascent=style.fontSize,descent=-0.2*style.fontSize)
#general case nFrags>1 or special
if hasattr(self,'blPara') and getattr(self,'_splitpara',0):
return self.blPara
autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
calcBounds = autoLeading not in ('','off')
return cjkFragSplit(frags, maxWidths, calcBounds)
def beginText(self, x, y):
return self.canv.beginText(x, y)
def drawPara(self,debug=0):
"""Draws a paragraph according to the given style.
Returns the final y position at the bottom. Not safe for
paragraphs without spaces e.g. Japanese; wrapping
algorithm will go infinite."""
#stash the key facts locally for speed
canvas = self.canv
style = self.style
blPara = self.blPara
lines = blPara.lines
leading = style.leading
autoLeading = getattr(self,'autoLeading',getattr(style,'autoLeading',''))
#work out the origin for line 1
leftIndent = style.leftIndent
cur_x = leftIndent
if debug:
bw = 0.5
bc = Color(1,1,0)
bg = Color(0.9,0.9,0.9)
else:
bw = getattr(style,'borderWidth',None)
bc = getattr(style,'borderColor',None)
bg = style.backColor
#if has a background or border, draw it
if bg or (bc and bw):
canvas.saveState()
op = canvas.rect
kwds = dict(fill=0,stroke=0)
if bc and bw:
canvas.setStrokeColor(bc)
canvas.setLineWidth(bw)
kwds['stroke'] = 1
br = getattr(style,'borderRadius',0)
if br and not debug:
op = canvas.roundRect
kwds['radius'] = br
if bg:
canvas.setFillColor(bg)
kwds['fill'] = 1
bp = getattr(style,'borderPadding',0)
tbp, rbp, bbp, lbp = normalizeTRBL(bp)
op(leftIndent - lbp,
-bbp,
self.width - (leftIndent+style.rightIndent) + lbp+rbp,
self.height + tbp+bbp,
**kwds)
canvas.restoreState()
nLines = len(lines)
bulletText = self.bulletText
if nLines > 0:
_offsets = getattr(self,'_offsets',[0])
_offsets += (nLines-len(_offsets))*[_offsets[-1]]
canvas.saveState()
#canvas.addLiteral('%% %s.drawPara' % _className(self))
alignment = style.alignment
offset = style.firstLineIndent+_offsets[0]
lim = nLines-1
noJustifyLast = not getattr(self,'_JustifyLast',False)
jllwc = style.justifyLastLine
if blPara.kind==0:
if alignment == TA_LEFT:
dpl = _leftDrawParaLine
elif alignment == TA_CENTER:
dpl = _centerDrawParaLine
elif self.style.alignment == TA_RIGHT:
dpl = _rightDrawParaLine
elif self.style.alignment == TA_JUSTIFY:
dpl = _justifyDrawParaLine
f = blPara
if paraFontSizeHeightOffset:
cur_y = self.height - f.fontSize
else:
cur_y = self.height - getattr(f,'ascent',f.fontSize)
if bulletText:
offset = _drawBullet(canvas,offset,cur_y,bulletText,style,rtl=style.wordWrap=='RTL' and self._wrapWidths or False)
#set up the font etc.
canvas.setFillColor(f.textColor)
tx = self.beginText(cur_x, cur_y)
tx.preformatted = 'preformatted' in self.__class__.__name__.lower()
if autoLeading=='max':
leading = max(leading,blPara.ascent-blPara.descent)
elif autoLeading=='min':
leading = blPara.ascent-blPara.descent
# set the paragraph direction
tx.direction = self.style.wordWrap
#now the font for the rest of the paragraph
tx.setFont(f.fontName, f.fontSize, leading)
ws = lines[0][0]
words = lines[0][1]
lastLine = noJustifyLast and nLines==1
if lastLine and jllwc and len(words)>jllwc:
lastLine=False
t_off = dpl( tx, offset, ws, words, lastLine)
if f.us_lines or f.link or style.endDots:
tx._do_line = MethodType(_do_line,tx)
tx.xs = xs = tx.XtraState = ABag()
tx._defaultLineWidth = canvas._lineWidth
xs.cur_y = cur_y
xs.f = f
xs.style = style
xs.lines = lines
xs.link=f.link
xs.textColor = f.textColor
xs.backColors = []
dx = t_off+leftIndent
if dpl!=_justifyDrawParaLine: ws = 0
if f.us_lines:
_do_under_line(0, dx, ws, tx, f.us_lines)
if f.link: _do_link_line(0, dx, ws, tx)
if lastLine and style.endDots and dpl!=_rightDrawParaLine: _do_dots(0, dx, ws, xs, tx, dpl)
#now the middle of the paragraph, aligned with the left margin which is our origin.
for i in xrange(1, nLines):
ws = lines[i][0]
words = lines[i][1]
lastLine = noJustifyLast and i==lim
if lastLine and jllwc and len(words)>jllwc:
lastLine=False
t_off = dpl( tx, _offsets[i], ws, words, lastLine)
dx = t_off+leftIndent
if dpl!=_justifyDrawParaLine: ws = 0
if f.us_lines:
_do_under_line(i, t_off, ws, tx, f.us_lines)
if f.link: _do_link_line(i, dx, ws, tx)
if lastLine and style.endDots and dpl!=_rightDrawParaLine: _do_dots(i, dx, ws, xs, tx, dpl)
else:
for i in xrange(1, nLines):
words = lines[i][1]
lastLine = noJustifyLast and i==lim
if lastLine and jllwc and len(words)>jllwc:
lastLine=False
dpl( tx, _offsets[i], lines[i][0], words, lastLine)
else:
if self.style.wordWrap == 'RTL':
for line in lines:
line.words = line.words[::-1]
f = lines[0]
if paraFontSizeHeightOffset:
cur_y = self.height - f.fontSize
else:
cur_y = self.height - getattr(f,'ascent',f.fontSize)
# default?
dpl = _leftDrawParaLineX
if bulletText:
oo = offset
offset = _drawBullet(canvas,offset,cur_y,bulletText,style, rtl=style.wordWrap=='RTL' and self._wrapWidths or False)
if alignment == TA_LEFT:
dpl = _leftDrawParaLineX
elif alignment == TA_CENTER:
dpl = _centerDrawParaLineX
elif self.style.alignment == TA_RIGHT:
dpl = _rightDrawParaLineX
elif self.style.alignment == TA_JUSTIFY:
dpl = _justifyDrawParaLineX
else:
raise ValueError("bad align %s" % repr(alignment))
#set up the font etc.
tx = self.beginText(cur_x, cur_y)
tx.preformatted = 'preformatted' in self.__class__.__name__.lower()
tx._defaultLineWidth = canvas._lineWidth
tx._underlineWidth = getattr(style,'underlineWidth','')
tx._underlineOffset = getattr(style,'underlineOffset','') or '-0.125f'
tx._strikeWidth = getattr(style,'strikeWidth','')
tx._strikeOffset = getattr(style,'strikeOffset','') or '0.25f'
tx._do_line = MethodType(_do_line,tx)
# set the paragraph direction
tx.direction = self.style.wordWrap
xs = tx.XtraState=ABag()
xs.textColor=None
xs.backColor=None
xs.rise=0
xs.backColors=[]
xs.us_lines = {}
xs.links = {}
xs.link={}
xs.leading = style.leading
xs.leftIndent = leftIndent
tx._leading = None
tx._olb = None
xs.cur_y = cur_y
xs.f = f
xs.style = style
xs.autoLeading = autoLeading
xs.paraWidth = self.width
tx._fontname,tx._fontsize = None, None
line = lines[0]
lastLine = noJustifyLast and nLines==1
if lastLine and jllwc and line.wordCount>jllwc:
lastLine=False
dpl( tx, offset, line, lastLine)
_do_post_text(tx)
#now the middle of the paragraph, aligned with the left margin which is our origin.
for i in xrange(1, nLines):
line = lines[i]
lastLine = noJustifyLast and i==lim
if lastLine and jllwc and line.wordCount>jllwc:
lastLine=False
dpl( tx, _offsets[i], line, lastLine)
_do_post_text(tx)
canvas.drawText(tx)
canvas.restoreState()
def getPlainText(self,identify=None):
"""Convenience function for templates which want access
to the raw text, without XML tags. """
frags = getattr(self,'frags',None)
if frags:
plains = []
plains_append = plains.append
if _processed_frags(frags):
for word in frags:
for style,text in word[1:]:
plains_append(text)
if isinstance(word,_HSFrag):
plains_append(' ')
else:
for frag in frags:
if hasattr(frag, 'text'):
plains_append(frag.text)
return ''.join(plains)
elif identify:
text = getattr(self,'text',None)
if text is None: text = repr(self)
return text
else:
return ''
def getActualLineWidths0(self):
"""Convenience function; tells you how wide each line
actually is. For justified styles, this will be
the same as the wrap width; for others it might be
useful for seeing if paragraphs will fit in spaces."""
assert hasattr(self, 'width'), "Cannot call this method before wrap()"
if self.blPara.kind:
func = lambda frag, w=self.width: w - frag.extraSpace
else:
func = lambda frag, w=self.width: w - frag[0]
return list(map(func,self.blPara.lines))
@staticmethod
def dumpFrags(frags,indent=4,full=False):
R = ['[']
aR = R.append
for i,f in enumerate(frags):
if full:
aR(' [%r,' % f[0])
for fx in f[1:]:
aR(' (%s,)' % repr(fx[0]))
aR(' %r),' % fx[1])
aR(' ], #%d %s' % (i,f.__class__.__name__))
aR(' ]')
else:
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__))
i = indent*' '
return i + ('\n'+i).join(R)
if __name__=='__main__': #NORUNTESTS
def dumpParagraphLines(P):
print('dumpParagraphLines(<Paragraph @ %d>)' % id(P))
lines = P.blPara.lines
outw = sys.stdout.write
for l,line in enumerate(lines):
line = lines[l]
if hasattr(line,'words'):
words = line.words
else:
words = line[1]
nwords = len(words)
outw('line%d: %d(%s)\n ' % (l,nwords,str(getattr(line,'wordCount','Unknown'))))
for w in xrange(nwords):
outw(" %d:'%s'"%(w,getattr(words[w],'text',words[w])))
print()
def fragDump(w):
R= ["'%s'" % w[1]]
for a in ('fontName', 'fontSize', 'textColor', 'rise', 'underline', 'strike', 'link', 'cbDefn','lineBreak'):
if hasattr(w[0],a):
R.append('%s=%r' % (a,getattr(w[0],a)))
return ', '.join(R)
def dumpParagraphFrags(P):
print('dumpParagraphFrags(<Paragraph @ %d>) minWidth() = %.2f' % (id(P), P.minWidth()))
frags = P.frags
n =len(frags)
for l in xrange(n):
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])))
outw = sys.stdout.write
l = 0
cum = 0
for W in _getFragWords(frags,360):
cum += W[0]
outw("fragword%d: cum=%3d size=%d" % (l, cum, W[0]))
for w in W[1:]:
outw(' (%s)' % fragDump(w))
print()
l += 1
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
import sys
TESTS = sys.argv[1:]
if TESTS==[]: TESTS=['4']
def flagged(i,TESTS=TESTS):
return 'all' in TESTS or '*' in TESTS or str(i) in TESTS
styleSheet = getSampleStyleSheet()
B = styleSheet['BodyText']
style = ParagraphStyle("discussiontext", parent=B)
style.fontName= 'Helvetica'
if flagged(1):
text='''The <font name=courier color=green>CMYK</font> or subtractive method follows the way a printer
mixes three pigments (cyan, magenta, and yellow) to form colors.
Because mixing chemicals is more difficult than combining light there
is a fourth parameter for darkness. For example a chemical
combination of the <font name=courier color=green>CMY</font> pigments generally never makes a perfect
black -- instead producing a muddy color -- so, to get black printers
don't use the <font name=courier color=green>CMY</font> pigments but use a direct black ink. Because
<font name=courier color=green>CMYK</font> maps more directly to the way printer hardware works it may
be the case that &| & | colors specified in <font name=courier color=green>CMYK</font> will provide better fidelity
and better control when printed.
'''
P=Paragraph(text,style)
dumpParagraphFrags(P)
aW, aH = 456.0, 42.8
w,h = P.wrap(aW, aH)
dumpParagraphLines(P)
S = P.split(aW,aH)
for s in S:
s.wrap(aW,aH)
dumpParagraphLines(s)
aH = 500
if flagged(2):
P=Paragraph("""Price<super><font color="red">*</font></super>""", styleSheet['Normal'])
dumpParagraphFrags(P)
w,h = P.wrap(24, 200)
dumpParagraphLines(P)
if flagged(3):
text = """Dieses Kapitel bietet eine schnelle <b><font color=red>Programme :: starten</font></b>
<onDraw name=myIndex label="Programme :: starten">
<b><font color=red>Eingabeaufforderung :: (>>>)</font></b>
<onDraw name=myIndex label="Eingabeaufforderung :: (>>>)">
<b><font color=red>>>> (Eingabeaufforderung)</font></b>
<onDraw name=myIndex label=">>> (Eingabeaufforderung)">
Einführung in Python <b><font color=red>Python :: Einführung</font></b>
<onDraw name=myIndex label="Python :: Einführung">.
Das Ziel ist, die grundlegenden Eigenschaften von Python darzustellen, ohne
sich zu sehr in speziellen Regeln oder Details zu verstricken. Dazu behandelt
dieses Kapitel kurz die wesentlichen Konzepte wie Variablen, Ausdrücke,
Kontrollfluss, Funktionen sowie Ein- und Ausgabe. Es erhebt nicht den Anspruch,
umfassend zu sein."""
P=Paragraph(text, styleSheet['Code'])
dumpParagraphFrags(P)
w,h = P.wrap(6*72, 9.7*72)
dumpParagraphLines(P)
if flagged(4):
text='''Die eingebaute Funktion <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üllt sie mit Werten <font name=Courier>k</font>, für die gilt: <font name=Courier>i <= k < 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üllt einen ähnlichen Zweck, gibt aber eine unveränderliche Sequenz vom Typ <font name=Courier>XRangeType</font><onDraw name=myIndex label="XRangeType"> zurück. Anstatt alle Werte in der Liste abzuspeichern, berechnet diese Liste ihre Werte, wann immer sie angefordert werden. Das ist sehr viel speicherschonender, wenn mit sehr langen Listen von Ganzzahlen gearbeitet wird. <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.'''
aW = 420
aH = 64.4
P=Paragraph(text, B)
dumpParagraphFrags(P)
w,h = P.wrap(aW,aH)
print('After initial wrap',w,h)
dumpParagraphLines(P)
S = P.split(aW,aH)
dumpParagraphFrags(S[0])
w0,h0 = S[0].wrap(aW,aH)
print('After split wrap',w0,h0)
dumpParagraphLines(S[0])
if flagged(5):
text = '<para> %s <![CDATA[</font></b>& %s < >]]></para>' % (chr(163),chr(163))
P=Paragraph(text, styleSheet['Code'])
dumpParagraphFrags(P)
w,h = P.wrap(6*72, 9.7*72)
dumpParagraphLines(P)
if flagged(6):
for text in ['''Here comes <FONT FACE="Helvetica" SIZE="14pt">Helvetica 14</FONT> with <STRONG>strong</STRONG> <EM>emphasis</EM>.''',
'''Here comes <font face="Helvetica" size="14pt">Helvetica 14</font> with <Strong>strong</Strong> <em>emphasis</em>.''',
'''Here comes <font face="Courier" size="3cm">Courier 3cm</font> and normal again.''',
]:
P=Paragraph(text, styleSheet['Normal'], caseSensitive=0)
dumpParagraphFrags(P)
w,h = P.wrap(6*72, 9.7*72)
dumpParagraphLines(P)
if flagged(7):
text = """<para align="CENTER" fontSize="24" leading="30"><b>Generated by:</b>Dilbert</para>"""
P=Paragraph(text, styleSheet['Code'])
dumpParagraphFrags(P)
w,h = P.wrap(6*72, 9.7*72)
dumpParagraphLines(P)
if flagged(8):
text ="""- bullet 0<br/>- bullet 1<br/>- bullet 2<br/>- bullet 3<br/>- bullet 4<br/>- bullet 5"""
P=Paragraph(text, styleSheet['Normal'])
dumpParagraphFrags(P)
w,h = P.wrap(6*72, 9.7*72)
dumpParagraphLines(P)
S = P.split(6*72,h/2.0)
print(len(S))
dumpParagraphFrags(S[0])
dumpParagraphLines(S[0])
S[1].wrap(6*72, 9.7*72)
dumpParagraphFrags(S[1])
dumpParagraphLines(S[1])
if flagged(9):
text="""Furthermore, the fundamental error of
regarding <img src="../docs/images/testimg.gif" width="3" height="7"/> functional notions as
categorial delimits a general
convention regarding the forms of the<br/>
grammar. I suggested that these results
would follow from the assumption that"""
P=Paragraph(text,ParagraphStyle('aaa',parent=styleSheet['Normal'],align=TA_JUSTIFY))
dumpParagraphFrags(P)
w,h = P.wrap(6*cm-12, 9.7*72)
dumpParagraphLines(P)
if flagged(10):
text="""a b c\xc2\xa0d e f"""
P=Paragraph(text,ParagraphStyle('aaa',parent=styleSheet['Normal'],align=TA_JUSTIFY))
dumpParagraphFrags(P)
w,h = P.wrap(6*cm-12, 9.7*72)
dumpParagraphLines(P)
if flagged(11):
text="""This page tests out a number of attributes of the <b>paraStyle</b><onDraw name="_indexAdd" label="paraStyle"/> tag.
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.
It should be a normal<onDraw name="_indexAdd" label="normal"/> paragraph, set in Courier (not bold).
It should be a normal<onDraw name="_indexAdd" label="normal"/> paragraph, set in Courier 12 pt."""
P=Paragraph(text,style=ParagraphStyle('style1',fontName="Courier",fontSize=10))
dumpParagraphFrags(P)
w,h = P.wrap(6.27*72-12,10000)
dumpParagraphLines(P)