#copyright ReportLab Inc. 2000
#see license.txt for license details
#history http://cvs.sourceforge.net/cgi-bin/cvsweb.cgi/reportlab/platypus/paragraph.py?cvsroot=reportlab
#$Header: /tmp/reportlab/reportlab/platypus/paragraph.py,v 1.25 2000/10/25 08:57:45 rgbecker Exp $
__version__=''' $Id: paragraph.py,v 1.25 2000/10/25 08:57:45 rgbecker Exp $ '''
import string
from types import StringType, ListType
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.platypus.paraparser import ParaParser, ParaFrag
from reportlab.platypus.flowables import Flowable
from reportlab.lib.colors import Color
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.lib.utils import _className
from copy import deepcopy
#our one and only parser
# XXXXX if the parser has any internal state using only one is probably a BAD idea!
_parser=ParaParser()
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."""
stripped = string.strip(text)
lines = string.split(stripped, '\n')
trimmed_lines = map(string.lstrip, lines)
return string.join(trimmed_lines, joiner)
def _leftDrawParaLine( tx, offset, extraspace, words, last=0):
tx.setXPos(offset)
tx._textOut(string.join(words),1)
def _centerDrawParaLine( tx, offset, extraspace, words, last=0):
m = offset + 0.5 * extraspace
tx.setXPos(m)
tx._textOut(string.join(words),1)
def _rightDrawParaLine( tx, offset, extraspace, words, last=0):
m = offset + extraspace
tx.setXPos(m)
tx._textOut(string.join(words),1)
def _justifyDrawParaLine( tx, offset, extraspace, words, last=0):
tx.setXPos(offset)
text = string.join(words)
if last:
#last one, left align
tx._textOut(text,1)
else:
tx.setWordSpace(extraspace / float(len(words)-1))
tx._textOut(text,1)
tx.setWordSpace(0)
def _putFragLine(tx,words):
for f in words:
if (tx._fontname,tx._fontsize)!=(f.fontName,f.fontSize):
tx._setFont(f.fontName, f.fontSize)
if tx.XtraState.textColor!=f.textColor:
tx.XtraState.textColor = f.textColor
tx.setFillColor(f.textColor)
if tx.XtraState.rise!=f.rise:
tx.XtraState.rise=f.rise
tx.setRise(f.rise)
tx._textOut(f.text,f is words[-1]) # cheap textOut
def _leftDrawParaLineX( tx, offset, line, last=0):
tx.setXPos(offset)
_putFragLine(tx, line.words)
def _centerDrawParaLineX( tx, offset, line, last=0):
m = offset+0.5*line.extraSpace
tx.setXPos(m)
_putFragLine(tx, line.words)
def _rightDrawParaLineX( tx, offset, line, last=0):
m = offset+line.extraSpace
tx.setXPos(m)
_putFragLine(tx, line.words)
def _justifyDrawParaLineX( tx, offset, line, last=0):
if last:
#last one, left align
tx.setXPos(offset)
_putFragLine(tx, line.words)
else:
tx.setXPos(offset)
tx.setWordSpace(line.extraSpace / float(line.wordCount-1))
_putFragLine(tx, line.words)
tx.setWordSpace(0)
def _sameFrag(f,g):
'returns 1 if two frags map out the same'
for a in ('fontName', 'fontSize', 'textColor', 'rise'):
if getattr(f,a)!=getattr(g,a): return 0
return 1
def _getFragWords(frags):
''' given a fragment list return a list of lists
[[size, (f00,w00), ..., (f0n,w0n)],....,[size, (fm0,wm0), ..., (f0n,wmn)]]
each pair f,w represents a style and some string
each sublist represents a word
'''
R = []
W = []
n = 0
for f in frags:
text = f.text
#del f.text # we can't do this until we sort out splitting
# of paragraphs
if text!='':
S = string.split(text,' ')
if S[-1]=='': del S[-1]
if W!=[] and text[0] in [' ','\t']:
W.insert(0,n)
R.append(W)
W = []
n = 0
for w in S[:-1]:
W.append((f,w))
n = n + stringWidth(w, f.fontName, f.fontSize)
W.insert(0,n)
R.append(W)
W = []
n = 0
w = S[-1]
W.append((f,w))
n = n + stringWidth(w, f.fontName, f.fontSize)
if text[-1] in [' ','\t']:
W.insert(0,n)
R.append(W)
W = []
n = 0
if W!=[]:
W.insert(0,n)
R.append(W)
return R
def _split_bfragSimple(bfrag,start,stop):
f = bfrag.clone()
for a in ('lines', 'kind', 'text'):
if hasattr(f,a): delattr(f,a)
f.words = []
for l in bfrag.lines[start:stop]:
for w in l[1]:
f.words.append(w)
return [f]
def _split_bfragHard(bfrag,start,stop):
f = []
lines = bfrag.lines[start:stop]
for l in lines:
for w in l.words:
f.append(w)
if l is not lines[-1]:
f[-1].text = f[-1].text+' '
return f
def _drawBullet(canvas, offset, cur_y, bulletText, style):
'''draw a bullet text could be a simple string or a frag list'''
tx2 = canvas.beginText(style.bulletIndent, cur_y)
tx2.setFont(style.bulletFontName, style.bulletFontSize)
tx2.setFillColor(hasattr(style,'bulletColor') and style.bulletColor or style.textColor)
if type(bulletText) is StringType:
tx2.textOut(bulletText)
else:
for f in bulletText:
tx2.setFont(f.fontName, f.fontSize)
tx2.setFillColor(f.textColor)
tx2.textOut(f.text)
bulletEnd = tx2.getX()
offset = max(offset, bulletEnd - style.leftIndent)
canvas.drawText(tx2)
return offset
def _handleBulletWidth(bulletText,style,maxWidths):
'''work out bullet width and adjust maxWidths[0] if neccessary
'''
if bulletText <> None:
if type(bulletText) is StringType:
bulletWidth = stringWidth(
bulletText,
style.bulletFontName, style.bulletFontSize)
else:
#it's a list of fragments
bulletWidth = 0
for f in bulletText:
bulletWidth = bulletWidth + stringWidth(f.text, f.fontName, f.fontSize)
bulletRight = style.bulletIndent + bulletWidth
if bulletRight > style.firstLineIndent:
#..then it overruns, and we have less space available on line 1
maxWidths[0] = maxWidths[0] - (bulletRight - style.firstLineIndent)
class Paragraph(Flowable):
""" Paragraph(text, style, bulletText=None)
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.
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
<i> ... </i> - italics
<u> ... </u> - underline
<super> ... </super> - superscript
<sub> ... </sub> - subscript
<font name=fontfamily/fontname color=colorname size=float>
The whole may be surrounded by <para> </para> tags
It will also be able to handle any MathML specified Greek characters.
"""
def __init__(self, text, style, bulletText = None, frags=None):
self._setup(text, style, bulletText, frags, cleanBlockQuotedText)
def _setup(self, text, style, bulletText, frags, cleaner):
if frags is None:
text = cleaner(text)
style, frags, bFrags = _parser.parse(text,style)
if frags is None:
raise "xml parser error (%s) in paragraph beginning\n'%s'"\
% (_parser.errors[0],text[:min(30,len(text))])
if bFrags: bulletText = bFrags
#AR hack
self.text = text
self.frags = frags
self.style = style
self.bulletText = bulletText
self.debug = 0 #turn this on to see a pretty one with all the margins etc.
def wrap(self, availWidth, availHeight):
# work out widths array for breaking
self.width = availWidth
first_line_width = availWidth - self.style.firstLineIndent - self.style.rightIndent
later_widths = availWidth - self.style.leftIndent - self.style.rightIndent
self.bfrags = self.breakLines([first_line_width, later_widths])
self.height = len(self.bfrags.lines) * self.style.leading
#estimate the size
return (self.width, self.height)
def _get_split_bFragFunc(self):
return self.bfrags.kind==0 and _split_bfragSimple or _split_bfragHard
def split(self,availWidth, availHeight):
if len(self.frags)<=0: return []
#the split information is all inside self.bfrags
if not hasattr(self,'bfrags'):
self.wrap(availWidth,availHeight)
bfrags = self.bfrags
style = self.style
leading = style.leading
lines = bfrags.lines
n = len(lines)
s = int(availHeight/leading)
if s<=1: return []
if n<=s: return [self]
func = self._get_split_bFragFunc()
P1=self.__class__(None,style,bulletText=self.bulletText,frags=func(bfrags,0,s))
P1._JustifyLast = 1
if style.firstLineIndent != style.leftIndent:
style = deepcopy(style)
style.firstLineIndent = style.leftIndent
P2=self.__class__(None,style,bulletText=None,frags=func(bfrags,s,n))
return [P1,P2]
def draw(self):
#call another method for historical reasons. Besides, I
#suspect I will be playing with alternate drawing routines
#so not doing it here makes it easier to switch.
self.drawPara(self.debug)
def breakLines(self, width):
"""
Returns a broken line structure. There are two cases
A) For the simple case of a single formatting input fragment the output is
A fragment specifier with
kind = 0
fontName, fontSize, leading, textColor
lines= A list of lines
Each line has two items.
1) unused width in points
2) word list
B) When there is more than one input formatting fragment the out put is
A fragment specifier with
kind = 1
lines= A list of fragments each having fields
extraspace (needed for justified)
fontSize
words=word list
each word is itself a fragment with
various settings
This structure can be used to easily draw paragraphs with the various alignments.
You can supply either a single width or a list of widths; the latter will have its
last item repeated until necessary. A 2-element list is useful when there is a
different first line indent; a longer list could be created to facilitate custom wraps
around irregular objects."""
if type(width) <> ListType: maxWidths = [width]
else: maxWidths = width
lines = []
lineno = 0
maxWidth = maxWidths[lineno]
style = self.style
fFontSize = float(style.fontSize)
#for bullets, work out width and ensure we wrap the right amount onto line one
_handleBulletWidth(self.bulletText,style,maxWidths)
self.height = 0
frags = self.frags
nFrags= len(frags)
if nFrags==1:
f = frags[0]
fontSize = f.fontSize
fontName = f.fontName
words = hasattr(f,'text') and string.split(f.text, ' ') or f.words
spaceWidth = stringWidth(' ', fontName, fontSize)
cLine = []
currentWidth = - spaceWidth # hack to get around extra space for word 1
for word in words:
wordWidth = stringWidth(word, fontName, fontSize)
space_available = maxWidth - (currentWidth + spaceWidth + wordWidth)
if space_available > 0:
# fit one more on this line
cLine.append(word)
currentWidth = currentWidth + spaceWidth + wordWidth
else:
#end of line
lines.append((maxWidth - currentWidth, cLine))
cLine = [word]
currentWidth = wordWidth
lineno = lineno + 1
try:
maxWidth = maxWidths[lineno]
except IndexError:
maxWidth = maxWidths[-1] # use the last one
#deal with any leftovers on the final line
if cLine!=[]: lines.append((maxWidth - currentWidth, cLine))
return f.clone(kind=0, lines=lines)
elif nFrags<=0:
return ParaFrag(kind=0, fontSize=style.fontSize, fontName=style.fontName,
textColor=style.textColor, lines=[])
else:
n = 0
for w in _getFragWords(frags):
spaceWidth = stringWidth(' ',w[-1][0].fontName, w[-1][0].fontSize)
if n==0:
currentWidth = -spaceWidth # hack to get around extra space for word 1
words = []
maxSize = 0
wordWidth = w[0]
f = w[1][0]
space_available = maxWidth - (currentWidth + spaceWidth + wordWidth)
if space_available > 0:
# fit one more on this line
n = n + 1
maxSize = max(maxSize,f.fontSize)
nText = w[1][1]
if words==[]:
words = [f.clone()]
words[-1].text = nText
elif not _sameFrag(words[-1],f):
if nText!='' and nText[0]!=' ':
words[-1].text = words[-1].text + ' '
words.append(f.clone())
words[-1].text = nText
else:
if nText!='' and nText[0]!=' ':
words[-1].text = words[-1].text + ' ' + nText
for i in w[2:]:
f = i[0].clone()
f.text=i[1]
words.append(f)
maxSize = max(maxSize,f.fontSize)
currentWidth = currentWidth + spaceWidth + wordWidth
else:
#end of line
lines.append(ParaFrag(extraSpace=(maxWidth - currentWidth),wordCount=n,
words=words, fontSize=maxSize))
#start new line
lineno = lineno + 1
try:
maxWidth = maxWidths[lineno]
except IndexError:
maxWidth = maxWidths[-1] # use the last one
currentWidth = wordWidth
n = 1
maxSize = f.fontSize
words = [f.clone()]
words[-1].text = w[1][1]
for i in w[2:]:
f = i[0].clone()
f.text=i[1]
words.append(f)
maxSize = max(maxSize,f.fontSize)
#deal with any leftovers on the final line
if words<>[]:
lines.append(ParaFrag(extraSpace=(maxWidth - currentWidth),wordCount=n,
words=words, fontSize=maxSize))
return ParaFrag(kind=1, lines=lines)
return lines
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
bfrags = self.bfrags
lines = bfrags.lines
#work out the origin for line 1
cur_x = style.leftIndent
if debug:
# This boxes and shades stuff to show how the paragraph
# uses its space. Useful for self-documentation so
# the debug code stays!
# box the lot
canvas.rect(0, 0, self.width, self.height)
#left and right margins
canvas.saveState()
canvas.setFillColor(Color(0.9,0.9,0.9))
canvas.rect(0, 0, style.leftIndent, self.height)
canvas.rect(self.width - style.rightIndent, 0, style.rightIndent, self.height)
# shade above and below
canvas.setFillColor(Color(1.0,1.0,0.0))
canvas.restoreState()
#self.drawLine(x + style.leftIndent, y, x + style.leftIndent, cur_y)
nLines = len(lines)
bulletText = self.bulletText
if nLines > 0:
canvas.saveState()
canvas.addLiteral('%% %s.drawPara' % _className(self))
alignment = style.alignment
offset = style.firstLineIndent - style.leftIndent
lim = nLines-1
noJustifyLast = not (hasattr(self,'_JustifyLast') and self._JustifyLast)
if bfrags.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 = bfrags
cur_y = self.height - f.fontSize
if bulletText <> None:
offset = _drawBullet(canvas,offset,cur_y,bulletText,style)
#set up the font etc.
canvas._code.append('%s %s %s rg' % (f.textColor.red, f.textColor.green, f.textColor.blue))
tx = canvas.beginText(cur_x, cur_y)
#now the font for the rest of the paragraph
tx.setFont(f.fontName, f.fontSize, style.leading)
dpl( tx, offset, lines[0][0], lines[0][1], noJustifyLast and nLines==1)
#now the middle of the paragraph, aligned with the left margin which is our origin.
for i in range(1, nLines):
dpl( tx, 0, lines[i][0], lines[i][1], noJustifyLast and i==lim)
else:
f = lines[0]
cur_y = self.height - f.fontSize
if bulletText <> None:
offset = _drawBullet(canvas,offset,cur_y,bulletText,style)
if alignment == TA_LEFT:
dpl = _leftDrawParaLineX
elif alignment == TA_CENTER:
dpl = _centerDrawParaLineX
elif self.style.alignment == TA_RIGHT:
dpl = _rightDrawParaLineX
elif self.style.alignment == TA_JUSTIFY:
dpl = _justifyDrawParaLineX
#set up the font etc.
tx = canvas.beginText(cur_x, cur_y)
tx.XtraState=ParaFrag()
tx.XtraState.textColor=None
tx.XtraState.rise=0
tx.setLeading(style.leading)
f = lines[0].words[0]
tx._setFont(f.fontName, f.fontSize)
dpl( tx, offset, lines[0], noJustifyLast and nLines==1)
#now the middle of the paragraph, aligned with the left margin which is our origin.
for i in range(1, nLines):
f = lines[i]
dpl( tx, 0, f, noJustifyLast and i==lim)
canvas.drawText(tx)
canvas.restoreState()
def getPlainText(self):
"""Convenience function for templates which want access
to the raw text, without XML tags. """
plains = []
for frag in self.frags:
plains.append(frag.text)
return string.join(plains, '')
if __name__=='__main__': #NORUNTESTS
def dumpParagraphLines(P):
print 'dumpParagraphLines(%s)' % str(P)
lines = P.bfrags.lines
n =len(lines)
for l in range(n):
line = lines[l]
words = line.words
nwords = len(words)
print 'line%d: %d(%d)\n ' % (l,nwords,line.wordCount),
for w in range(nwords):
print "%d:'%s'"%(w,words[w].text),
print
def dumpParagraphFrags(P):
print 'dumpParagraphLines(%s)' % str(P)
frags = P.frags
n =len(frags)
for l in range(n):
print "frag%d: '%s'" % (l, frags[l].text)
l = 0
for W in _getFragWords(frags):
print "fragword%d: size=%d" % (l, W[0]),
for w in W[1:]:
print "'%s'" % w[1],
print
l = l + 1
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
styleSheet = getSampleStyleSheet()
B = styleSheet['BodyText']
style = ParagraphStyle("discussiontext", parent=B)
style.fontName= 'Helvetica'
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
P=Paragraph("""Price<super><font color="red">*</font></super>""", styleSheet['Normal'])
dumpParagraphFrags(P)
w,h = P.wrap(24, 200)
dumpParagraphLines(P)