odoo/bin/reportlab/platypus/para.py

2395 lines
92 KiB
Python

"""new experimental paragraph implementation
Intended to allow support for paragraphs in paragraphs, hotlinks,
embedded flowables, and underlining. The main entry point is the
function
def Paragraph(text, style, bulletText=None, frags=None)
Which is intended to be plug compatible with the "usual" platypus
paragraph except that it supports more functionality.
In this implementation you may embed paragraphs inside paragraphs
to create hierarchically organized documents.
This implementation adds the following paragraph-like tags (which
support the same attributes as paragraphs, for font specification, etc).
- Unnumberred lists (ala html):
<ul>
<li>first one</li>
<li>second one</li>
</ul>
Also <ul type="disc"> (default) or <ul type="circle">, <ul type="square">.
- Numberred lists (ala html):
<ol>
<li>first one</li>
<li>second one</li>
</ol>
Also <ul type="1"> (default) or <ul type="a">, <ul type="A">.
- Display lists (ala HTML):
For example
<dl bulletFontName="Helvetica-BoldOblique" spaceBefore="10" spaceAfter="10">
<dt>frogs</dt> <dd>Little green slimy things. Delicious with <b>garlic</b></dd>
<dt>kittens</dt> <dd>cute, furry, not edible</dd>
<dt>bunnies</dt> <dd>cute, furry,. Delicious with <b>garlic</b></dd>
</dl>
ALSO the following additional internal paragraph markup tags are supported
<u>underlined text</u>
<a href="http://www.reportlab.com">hyperlinked text</a>
<a href="http://www.reportlab.com" color="blue">hyperlinked text</a>
<link destination="end" >Go to the end (go to document internal destination)</link>
<link destination="start" color="cyan">Go to the beginning</link>
<setLink destination="start" color="magenta">This is the document start
(define document destination inside paragraph, color is optional)</setLink>
"""
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.lib.utils import fp_str
from reportlab.platypus.flowables import Flowable
from reportlab.lib import colors
from types import StringType, UnicodeType, InstanceType, TupleType, ListType, FloatType
# SET THIS TO CAUSE A VIEWING BUG WITH ACROREAD 3 (for at least one input)
# CAUSEERROR = 0
debug = 0
DUMPPROGRAM = 0
TOOSMALLSPACE = 1e-5
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
# indent changes effect the next line
# align changes effect the current line
# need to fix spacing questions... if ends with space then space may be inserted
# NEGATIVE SPACE SHOULD NEVER BE EXPANDED (IN JUSTIFICATION, EG)
class paragraphEngine:
# text origin of 0,0 is upper left corner
def __init__(self, program = None):
from reportlab.lib.colors import black
if program is None:
program = []
self.lineOpHandlers = [] # for handling underlining and hyperlinking, etc
self.program = program
self.indent = self.rightIndent = 0.0
self.baseindent = 0.0 # adjust this to add more indentation for bullets, eg
self.fontName = "Helvetica"
self.fontSize = 10
self.leading = 12
self.fontColor = black
self.x = self.y = self.rise = 0.0
from reportlab.lib.enums import TA_LEFT
self.alignment = TA_LEFT
self.textStateStack = []
TEXT_STATE_VARIABLES = ("indent", "rightIndent", "fontName", "fontSize",
"leading", "fontColor", "lineOpHandlers", "rise",
"alignment")
#"textStateStack")
def pushTextState(self):
state = []
for var in self.TEXT_STATE_VARIABLES:
val = getattr(self, var)
state.append(val)
#self.textStateStack.append(state)
self.textStateStack = self.textStateStack+[state] # fresh copy
#print "push", self.textStateStack
#print "push", len(self.textStateStack), state
return state
def popTextState(self):
state = self.textStateStack[-1]
self.textStateStack = self.textStateStack[:-1]
#print "pop", self.textStateStack
state = state[:] # copy for destruction
#print "pop", len(self.textStateStack), state
#print "handlers before", self.lineOpHandlers
for var in self.TEXT_STATE_VARIABLES:
val = state[0]
del state[0]
setattr(self, var, val)
def format(self, maxwidth, maxheight, program, leading=0):
"return program with line operations added if at least one line fits"
# note: a generated formatted segment should not be formatted again
startstate = self.__dict__.copy()
#remainder = self.cleanProgram(program)
remainder = program[:]
#program1 = remainder[:] # debug only
lineprogram = []
#if maxheight<TOOSMALLSPACE:
# raise ValueError, "attempt to format inside too small a height! "+repr(maxheight)
heightremaining = maxheight
if leading: self.leading = leading
room = 1
cursorcount = 0 # debug
while remainder and room: #heightremaining>=self.leading and remainder:
#print "getting line with statestack", len(self.textStateStack)
#heightremaining = heightremaining - self.leading
indent = self.indent
rightIndent = self.rightIndent
linewidth = maxwidth - indent - rightIndent
beforelinestate = self.__dict__.copy()
if linewidth<TOOSMALLSPACE:
raise ValueError, "indents %s %s too wide for space %s" % (self.indent, self.rightIndent, \
maxwidth)
try:
(lineIsFull, line, cursor, currentLength, \
usedIndent, maxLength, justStrings) = self.fitLine(remainder, maxwidth)
except:
## print "failed to fit line near", cursorcount # debug
## for i in program1[max(0,cursorcount-10): cursorcount]:
## print
## print i,
## print "***" *8
## for i in program1[cursorcount:cursorcount+20]:
## print i
raise
cursorcount = cursorcount+cursor # debug
leading = self.leading
if heightremaining>leading:
heightremaining = heightremaining-leading
else:
room = 0
#self.resetState(beforelinestate)
self.__dict__.update(beforelinestate)
break # no room for this line
## if debug:
## print "line", line
## if lineIsFull: print "is full"
## else: print "is partially full"
## print "consumes", cursor, "elements"
## print "covers", currentLength, "of", maxwidth
alignment = self.alignment # last declared alignment for this line used
# recompute linewidth using the used indent
#linewidth = maxwidth - usedIndent - rightIndent
remainder = remainder[cursor:]
if not remainder:
# trim off the extra end of line
del line[-1]
# do justification if any
#line = self.shrinkWrap(line
if alignment==TA_LEFT:
#if debug:
# print "ALIGN LEFT"
if justStrings:
line = stringLine(line, currentLength)
else:
line = self.shrinkWrap(line)
pass
elif alignment==TA_CENTER:
#if debug:
# print "ALIGN CENTER"
if justStrings:
line = stringLine(line, currentLength)
else:
line = self.shrinkWrap(line)
line = self.centerAlign(line, currentLength, maxLength)
elif alignment==TA_RIGHT:
#if debug:
# print "ALIGN RIGHT"
if justStrings:
line = stringLine(line, currentLength)
else:
line = self.shrinkWrap(line)
line = self.rightAlign(line, currentLength, maxLength)
elif alignment==TA_JUSTIFY:
#if debug:
# print "JUSTIFY"
if remainder and lineIsFull:
if justStrings:
line = simpleJustifyAlign(line, currentLength, maxLength)
else:
line = self.justifyAlign(line, currentLength, maxLength)
else:
if justStrings:
line = stringLine(line, currentLength)
else:
line = self.shrinkWrap(line)
if debug:
print "no justify because line is not full or end of para"
else:
raise ValueError, "bad alignment "+repr(alignment)
if not justStrings:
line = self.cleanProgram(line)
lineprogram.extend(line)
laststate = self.__dict__.copy()
#self.resetState(startstate)
self.__dict__.update(startstate)
heightused = maxheight - heightremaining
return (lineprogram, remainder, laststate, heightused)
def getState(self):
# inlined
return self.__dict__.copy()
def resetState(self, state):
# primarily inlined
self.__dict__.update(state)
## def sizeOfWord(self, word):
## inlineThisFunctionForEfficiency
## return float(stringWidth(word, self.fontName, self.fontSize))
def fitLine(self, program, totalLength):
"fit words (and other things) onto a line"
# assuming word lengths and spaces have not been yet added
# fit words onto a line up to maxlength, adding spaces and respecting extra space
from reportlab.pdfbase.pdfmetrics import stringWidth
usedIndent = self.indent
maxLength = totalLength - usedIndent - self.rightIndent
done = 0
line = []
cursor = 0
lineIsFull = 0
currentLength = 0
maxcursor = len(program)
needspace = 0
first = 1
terminated = None
fontName = self.fontName
fontSize = self.fontSize
spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ")
justStrings = 1
while not done and cursor<maxcursor:
opcode = program[cursor]
#if debug: print "opcode", cursor, opcode
topcode = type(opcode)
if topcode in (StringType, UnicodeType, InstanceType):
lastneedspace = needspace
needspace = 0
if topcode is InstanceType:
justStrings = 0
width = opcode.width(self)
needspace = 0
else:
saveopcode = opcode
opcode = opcode.strip()
if opcode:
width = stringWidth(opcode, fontName, fontSize)
else:
width = 0
if saveopcode and (width or currentLength):
# ignore white space at margin
needspace = (saveopcode[-1]==" ")
else:
needspace = 0
fullwidth = width
if lastneedspace:
#spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ")
fullwidth = width + spacewidth
newlength = currentLength+fullwidth
if newlength>maxLength and not first: # always do at least one thing
# this word won't fit
#if debug:
# print "WORD", opcode, "wont fit, width", width, "fullwidth", fullwidth
# print " currentLength", currentLength, "newlength", newlength, "maxLength", maxLength
done = 1
lineIsFull = 1
else:
# fit the word: add a space then the word
if lastneedspace:
line.append( spacewidth ) # expandable space: positive
if opcode:
line.append( opcode )
if abs(width)>TOOSMALLSPACE:
line.append( -width ) # non expanding space: negative
currentLength = newlength
#print line
#stop
first = 0
elif topcode is FloatType:
justStrings = 0
aopcode = abs(opcode) # negative means non expanding
if aopcode>TOOSMALLSPACE:
nextLength = currentLength+aopcode
if nextLength>maxLength and not first: # always do at least one thing
#if debug: print "EXPLICIT spacer won't fit", maxLength, nextLength, opcode
done = 1
else:
if aopcode>TOOSMALLSPACE:
currentLength = nextLength
line.append(opcode)
first = 0
elif topcode is TupleType:
justStrings = 0
indicator = opcode[0]
#line.append(opcode)
if indicator=="nextLine":
# advance to nextLine
#(i, endallmarks) = opcode
line.append(opcode)
cursor = cursor+1 # consume this element
terminated = done = 1
#if debug:
# print "nextLine encountered"
elif indicator=="color":
# change fill color
oldcolor = self.fontColor
(i, colorname) = opcode
#print "opcode", opcode
if type(colorname) in (StringType, UnicodeType):
color = self.fontColor = getattr(colors, colorname)
else:
color = self.fontColor = colorname # assume its something sensible :)
line.append(opcode)
elif indicator=="face":
# change font face
(i, fontname) = opcode
fontName = self.fontName = fontname
spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ")
line.append(opcode)
elif indicator=="size":
# change font size
(i, fontsize) = opcode
size = abs(float(fontsize))
if type(fontsize) in (StringType, UnicodeType):
if fontsize[:1]=="+":
fontSize = self.fontSize = self.fontSize + size
elif fontsize[:1]=="-":
fontSize = self.fontSize = self.fontSize - size
else:
fontSize = self.fontSize = size
else:
fontSize = self.fontSize = size
spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ")
line.append(opcode)
elif indicator=="leading":
# change font leading
(i, leading) = opcode
self.leading = leading
line.append(opcode)
elif indicator=="indent":
# increase the indent
(i, increment) = opcode
indent = self.indent = self.indent + increment
if first:
usedIndent = max(indent, usedIndent)
maxLength = totalLength - usedIndent - self.rightIndent
line.append(opcode)
elif indicator=="push":
self.pushTextState()
line.append(opcode)
elif indicator=="pop":
try:
self.popTextState()
except:
## print "stack fault near", cursor
## for i in program[max(0, cursor-10):cursor+10]:
## if i==cursor:
## print "***>>>",
## print i
raise
fontName = self.fontName
fontSize = self.fontSize
spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ")
line.append(opcode)
elif indicator=="bullet":
(i, bullet, indent, font, size) = opcode
# adjust for base indent (only at format time -- only execute once)
indent = indent + self.baseindent
opcode = (i, bullet, indent, font, size)
if not first:
raise ValueError, "bullet not at beginning of line"
bulletwidth = float(stringWidth(bullet, font, size))
spacewidth = float(stringWidth(" ", font, size))
bulletmin = indent+spacewidth+bulletwidth
# decrease the line size to allow bullet
usedIndent = max(bulletmin, usedIndent)
if first:
maxLength = totalLength - usedIndent - self.rightIndent
line.append(opcode)
elif indicator=="rightIndent":
# increase the right indent
(i, increment) = opcode
self.rightIndent = self.rightIndent+increment
if first:
maxLength = totalLength - usedIndent - self.rightIndent
line.append(opcode)
elif indicator=="rise":
(i, rise) = opcode
newrise = self.rise = self.rise+rise
line.append(opcode)
elif indicator=="align":
(i, alignment) = opcode
#if debug:
# print "SETTING ALIGNMENT", alignment
self.alignment = alignment
line.append(opcode)
elif indicator=="lineOperation":
(i, handler) = opcode
line.append(opcode)
self.lineOpHandlers = self.lineOpHandlers + [handler] # fresh copy
elif indicator=="endLineOperation":
(i, handler) = opcode
h = self.lineOpHandlers[:] # fresh copy
h.remove(handler)
self.lineOpHandlers = h
line.append(opcode)
else:
raise ValueError, "at format time don't understand indicator "+repr(indicator)
else:
raise ValueError, "op must be string, float, instance, or tuple "+repr(opcode)
if not done:
cursor = cursor+1
#first = 0
## if debug:
## if done:
## print "DONE FLAG IS SET"
## if cursor>=maxcursor:
## print "AT END OF PROGRAM"
if not terminated:
line.append( ("nextLine", 0) )
#print "fitline", line
return (lineIsFull, line, cursor, currentLength, usedIndent, maxLength, justStrings)
def centerAlign(self, line, lineLength, maxLength):
diff = maxLength-lineLength
shift = diff/2.0
if shift>TOOSMALLSPACE:
return self.insertShift(line, shift)
return line
def rightAlign(self, line, lineLength, maxLength):
shift = maxLength-lineLength
#die
if shift>TOOSMALLSPACE:
return self.insertShift(line, shift)
return line
def insertShift(self, line, shift):
# insert shift just before first visible element in line
result = []
first = 1
for e in line:
te = type(e)
if first and (te in (StringType, UnicodeType, InstanceType)):
result.append(shift)
first = 0
result.append(e)
return result
def justifyAlign(self, line, lineLength, maxLength):
diff = maxLength-lineLength
# count EXPANDABLE SPACES AFTER THE FIRST VISIBLE
spacecount = 0
visible = 0
for e in line:
te = type(e)
if te is FloatType and e>TOOSMALLSPACE and visible:
spacecount = spacecount+1
elif te in (StringType, UnicodeType, InstanceType):
visible = 1
#if debug: print "diff is", diff, "wordcount", wordcount #; die
if spacecount<1:
return line
shift = diff/float(spacecount)
if shift<=TOOSMALLSPACE:
#if debug: print "shift too small", shift
return line
first = 1
visible = 0
result = []
cursor = 0
nline = len(line)
while cursor<nline:
e = line[cursor]
te = type(e)
result.append(e)
if (te in (StringType, UnicodeType, InstanceType)):
visible = 1
elif te is FloatType and e>TOOSMALLSPACE and visible:
expanded = e+shift
result[-1] = expanded
cursor = cursor+1
return result
## if not first:
## #if debug: print "shifting", shift, e
## #result.append(shift)
## # add the shift in result before any start markers before e
## insertplace = len(result)-1
## done = 0
## myshift = shift
## while insertplace>0 and not done:
## beforeplace = insertplace-1
## beforething = result[beforeplace]
## thingtype = type(beforething)
## if thingtype is TupleType:
## indicator = beforething[0]
## if indicator=="endLineOperation":
## done = 1
## elif debug:
## print "adding shift before", beforething
## elif thingtype is FloatType:
## myshift = myshift + beforething
## del result[beforeplace]
## else:
## done = 1
## if not done:
## insertplace = beforeplace
## result.insert(insertplace, myshift)
## first = 0
## cursor = cursor+1
## return result
def shrinkWrap(self, line):
# for non justified text, collapse adjacent text/shift's into single operations
result = []
index = 0
maxindex = len(line)
while index<maxindex:
e = line[index]
te = type(e)
if te in (StringType, UnicodeType) and index<maxindex-1:
# collect strings and floats
thestrings = [e]
thefloats = 0.0
index = index+1
nexte = line[index]
tnexte = type(nexte)
while index<maxindex and (tnexte in (FloatType, StringType, UnicodeType)):
# switch to expandable space if appropriate
if tnexte is FloatType:
if thefloats<0 and nexte>0:
thefloats = -thefloats
if nexte<0 and thefloats>0:
nexte = -nexte
thefloats = thefloats + nexte
elif tnexte in (StringType, UnicodeType):
thestrings.append(nexte)
index = index+1
if index<maxindex:
nexte = line[index]
tnexte = type(nexte)
# wrap up the result
s = ' '.join(thestrings)
result.append(s)
result.append(float(thefloats))
# back up for unhandled element
index = index-1
else:
result.append(e)
index = index+1
return result
def cleanProgram(self, line):
"collapse adjacent spacings"
#return line # for debugging
result = []
last = 0
for e in line:
if type(e) is FloatType:
# switch to expandable space if appropriate
if last<0 and e>0:
last = -last
if e<0 and last>0:
e = -e
last = float(last)+e
else:
if abs(last)>TOOSMALLSPACE:
result.append(last)
result.append(e)
last = 0
if last:
result.append(last)
# now go backwards and delete all floats occurring after all visible elements
## count = len(result)-1
## done = 0
## while count>0 and not done:
## e = result[count]
## te = type(e)
## if te is StringType or te is InstanceType or te is TupleType:
## done = 1
## elif te is FloatType:
## del result[count]
## count = count-1
# move end operations left and start operations left up to visibles
change = 1
rline = range(len(result)-1)
while change:
#print line
change = 0
for index in rline:
nextindex = index+1
this = result[index]
next = result[nextindex]
doswap = 0
tthis = type(this)
tnext = type(next)
# don't swap visibles
if tthis in (StringType, UnicodeType) or \
tnext in (StringType, UnicodeType) or \
this is InstanceType or tnext is InstanceType:
doswap = 0
# only swap two tuples if the second one is an end operation and the first is something else
elif tthis is TupleType:
thisindicator = this[0]
if tnext is TupleType:
nextindicator = next[0]
doswap = 0
if (nextindicator=="endLineOperation" and thisindicator!="endLineOperation"
and thisindicator!="lineOperation"):
doswap = 1 # swap nonend<>end
elif tnext==FloatType:
if thisindicator=="lineOperation":
doswap = 1 # begin <> space
if doswap:
#print "swap", line[index],line[nextindex]
result[index] = next
result[nextindex] = this
change = 1
return result
def runOpCodes(self, program, canvas, textobject):
"render the line(s)"
escape = canvas._escape
code = textobject._code
startstate = self.__dict__.copy()
font = None
size = None
# be sure to set them before using them (done lazily below)
#textobject.setFont(self.fontName, self.fontSize)
textobject.setFillColor(self.fontColor)
xstart = self.x
thislineindent = self.indent
thislinerightIndent = self.rightIndent
indented = 0
for opcode in program:
topcode = type(opcode)
if topcode in (StringType, UnicodeType, InstanceType):
if not indented:
if abs(thislineindent)>TOOSMALLSPACE:
#if debug: print "INDENTING", thislineindent
#textobject.moveCursor(thislineindent, 0)
code.append('%s Td' % fp_str(thislineindent, 0))
self.x = self.x + thislineindent
for handler in self.lineOpHandlers:
#handler.end_at(x, y, self, canvas, textobject) # finish, eg, underlining this line
handler.start_at(self.x, self.y, self, canvas, textobject) # start underlining the next
indented = 1
# lazily set font (don't do it again if not needed)
if font!=self.fontName or size!=self.fontSize:
font = self.fontName
size = self.fontSize
textobject.setFont(font, size)
if topcode in (StringType, UnicodeType):
#textobject.textOut(opcode)
text = escape(opcode)
code.append('(%s) Tj' % text)
else:
# drawable thing
opcode.execute(self, textobject, canvas)
elif topcode is FloatType:
# use abs value (ignore expandable marking)
opcode = abs(opcode)
if opcode>TOOSMALLSPACE:
#textobject.moveCursor(opcode, 0)
code.append('%s Td' % fp_str(opcode, 0))
self.x = self.x + opcode
elif topcode is TupleType:
indicator = opcode[0]
if indicator=="nextLine":
# advance to nextLine
(i, endallmarks) = opcode
x = self.x
y = self.y
newy = self.y = self.y-self.leading
newx = self.x = xstart
thislineindent = self.indent
thislinerightIndent = self.rightIndent
indented = 0
for handler in self.lineOpHandlers:
handler.end_at(x, y, self, canvas, textobject) # finish, eg, underlining this line
#handler.start_at(newx, newy, self, canvas, textobject)) # start underlining the next
textobject.setTextOrigin(newx, newy)
elif indicator=="color":
# change fill color
oldcolor = self.fontColor
(i, colorname) = opcode
#print "opcode", opcode
if type(colorname) in (StringType, UnicodeType):
color = self.fontColor = getattr(colors, colorname)
else:
color = self.fontColor = colorname # assume its something sensible :)
#if debug:
# print color.red, color.green, color.blue
# print dir(color)
#print "color is", color
#from reportlab.lib.colors import green
#if color is green: print "color is green"
if color!=oldcolor:
textobject.setFillColor(color)
elif indicator=="face":
# change font face
(i, fontname) = opcode
self.fontName = fontname
#textobject.setFont(self.fontName, self.fontSize)
elif indicator=="size":
# change font size
(i, fontsize) = opcode
size = abs(float(fontsize))
if type(fontsize) in (StringType, UnicodeType):
if fontsize[:1]=="+":
fontSize = self.fontSize = self.fontSize + size
elif fontsize[:1]=="-":
fontSize = self.fontSize = self.fontSize - size
else:
fontSize = self.fontSize = size
else:
fontSize = self.fontSize = size
#(i, fontsize) = opcode
self.fontSize = fontSize
textobject.setFont(self.fontName, self.fontSize)
elif indicator=="leading":
# change font leading
(i, leading) = opcode
self.leading = leading
elif indicator=="indent":
# increase the indent
(i, increment) = opcode
indent = self.indent = self.indent + increment
thislineindent = max(thislineindent, indent)
elif indicator=="push":
self.pushTextState()
elif indicator=="pop":
oldcolor = self.fontColor
oldfont = self.fontName
oldsize = self.fontSize
self.popTextState()
#if CAUSEERROR or oldfont!=self.fontName or oldsize!=self.fontSize:
# textobject.setFont(self.fontName, self.fontSize)
if oldcolor!=self.fontColor:
textobject.setFillColor(self.fontColor)
elif indicator=="wordSpacing":
(i, ws) = opcode
textobject.setWordSpace(ws)
elif indicator=="bullet":
(i, bullet, indent, font, size) = opcode
if abs(self.x-xstart)>TOOSMALLSPACE:
raise ValueError, "bullet not at beginning of line"
bulletwidth = float(stringWidth(bullet, font, size))
spacewidth = float(stringWidth(" ", font, size))
bulletmin = indent+spacewidth+bulletwidth
# decrease the line size to allow bullet as needed
if bulletmin > thislineindent:
#if debug: print "BULLET IS BIG", bullet, bulletmin, thislineindent
thislineindent = bulletmin
textobject.moveCursor(indent, 0)
textobject.setFont(font, size)
textobject.textOut(bullet)
textobject.moveCursor(-indent, 0)
#textobject.textOut("M")
textobject.setFont(self.fontName, self.fontSize)
elif indicator=="rightIndent":
# increase the right indent
(i, increment) = opcode
self.rightIndent = self.rightIndent+increment
elif indicator=="rise":
(i, rise) = opcode
newrise = self.rise = self.rise+rise
textobject.setRise(newrise)
elif indicator=="align":
(i, alignment) = opcode
self.alignment = alignment
elif indicator=="lineOperation":
(i, handler) = opcode
handler.start_at(self.x, self.y, self, canvas, textobject)
#self.lineOpHandlers.append(handler)
#if debug: print "adding", handler, self.lineOpHandlers
self.lineOpHandlers = self.lineOpHandlers + [handler] # fresh copy!
elif indicator=="endLineOperation":
(i, handler) = opcode
handler.end_at(self.x, self.y, self, canvas, textobject)
newh = self.lineOpHandlers = self.lineOpHandlers[:] # fresh copy
#if debug: print "removing", handler, self.lineOpHandlers
if handler in newh:
self.lineOpHandlers.remove(handler)
else:
pass
#print "WARNING: HANDLER", handler, "NOT IN", newh
else:
raise ValueError, "don't understand indicator "+repr(indicator)
else:
raise ValueError, "op must be string float or tuple "+repr(opcode)
laststate = self.__dict__.copy()
#self.resetState(startstate)
self.__dict__.update(startstate)
return laststate
def stringLine(line, length):
"simple case: line with just strings and spacings which can be ignored"
strings = []
for x in line:
if type(x) in (StringType, UnicodeType):
strings.append(x)
text = ' '.join(strings)
result = [text, float(length)]
nextlinemark = ("nextLine", 0)
if line and line[-1]==nextlinemark:
result.append( nextlinemark )
return result
def simpleJustifyAlign(line, currentLength, maxLength):
"simple justification with only strings"
strings = []
for x in line[:-1]:
if type(x) in (StringType, UnicodeType):
strings.append(x)
nspaces = len(strings)-1
slack = maxLength-currentLength
text = ' '.join(strings)
if nspaces>0 and slack>0:
wordspacing = slack/float(nspaces)
result = [("wordSpacing", wordspacing), text, maxLength, ("wordSpacing", 0)]
else:
result = [text, currentLength, ("nextLine", 0)]
nextlinemark = ("nextLine", 0)
if line and line[-1]==nextlinemark:
result.append( nextlinemark )
return result
from reportlab.lib.colors import black
def readBool(text):
if text.upper() in ("Y", "YES", "TRUE", "1"):
return 1
elif text.upper() in ("N", "NO", "FALSE", "0"):
return 0
else:
raise RMLError, "true/false attribute has illegal value '%s'" % text
def readAlignment(text):
up = text.upper()
if up == 'LEFT':
return TA_LEFT
elif up == 'RIGHT':
return TA_RIGHT
elif up in ['CENTER', 'CENTRE']:
return TA_CENTER
elif up == 'JUSTIFY':
return TA_JUSTIFY
def readLength(text):
"""Read a dimension measurement: accept "3in", "5cm",
"72 pt" and so on."""
text = text.strip()
try:
return float(text)
except ValueError:
text = text.lower()
numberText, units = text[:-2],text[-2:]
numberText = numberText.strip()
try:
number = float(numberText)
except ValueError:
raise ValueError, "invalid length attribute '%s'" % text
try:
multiplier = {
'in':72,
'cm':28.3464566929, #72/2.54; is this accurate?
'mm':2.83464566929,
'pt':1
}[units]
except KeyError:
raise RMLError, "invalid length attribute '%s'" % text
return number * multiplier
def lengthSequence(s, converter=readLength):
"""from "(2, 1)" or "2,1" return [2,1], for example"""
s = s.strip()
if s[:1]=="(" and s[-1:]==")":
s = s[1:-1]
sl = s.split(',')
sl = [s.strip() for s in sl]
sl = [converter(s) for s in sl]
return sl
def readColor(text):
"""Read color names or tuples, RGB or CMYK, and return a Color object."""
if not text:
return None
from reportlab.lib import colors
from string import letters
if text[0] in letters:
return colors.__dict__[text]
tup = lengthSequence(text)
msg = "Color tuple must have 3 (or 4) elements for RGB (or CMYC)."
assert 3 <= len(tup) <= 4, msg
msg = "Color tuple must have all elements <= 1.0."
for i in range(len(tup)):
assert tup[i] <= 1.0, msg
if len(tup) == 3:
colClass = colors.Color
elif len(tup) == 4:
colClass = colors.CMYKColor
return apply(colClass, tup)
class StyleAttributeConverters:
fontSize=[readLength]
leading=[readLength]
leftIndent=[readLength]
rightIndent=[readLength]
firstLineIndent=[readLength]
alignment=[readAlignment]
spaceBefore=[readLength]
spaceAfter=[readLength]
bulletFontSize=[readLength]
bulletIndent=[readLength]
textColor=[readColor]
backColor=[readColor]
class SimpleStyle:
"simplified paragraph style without all the fancy stuff"
name = "basic"
fontName='Times-Roman'
fontSize=10
leading=12
leftIndent=0
rightIndent=0
firstLineIndent=0
alignment=TA_LEFT
spaceBefore=0
spaceAfter=0
bulletFontName='Times-Roman'
bulletFontSize=10
bulletIndent=0
textColor=black
backColor=None
def __init__(self, name, parent=None, **kw):
mydict = self.__dict__
if parent:
for (a,b) in parent.__dict__.items():
mydict[a]=b
for (a,b) in kw.items():
mydict[a] = b
def addAttributes(self, dictionary):
for key in dictionary.keys():
value = dictionary[key]
if value is not None:
if hasattr(StyleAttributeConverters, key):
converter = getattr(StyleAttributeConverters, key)[0]
value = converter(value)
setattr(self, key, value)
DEFAULT_ALIASES = {
"h1.defaultStyle": "Heading1",
"h2.defaultStyle": "Heading2",
"h3.defaultStyle": "Heading3",
"h4.defaultStyle": "Heading4",
"h5.defaultStyle": "Heading5",
"h6.defaultStyle": "Heading6",
"title.defaultStyle": "Title",
"subtitle.defaultStyle": "SubTitle",
"para.defaultStyle": "Normal",
"pre.defaultStyle": "Code",
"ul.defaultStyle": "UnorderedList",
"ol.defaultStyle": "OrderedList",
"li.defaultStyle": "Definition",
}
class FastPara(Flowable):
"paragraph with no special features (not even a single ampersand!)"
def __init__(self, style, simpletext):
#if debug:
# print "FAST", id(self)
if "&" in simpletext:
raise ValueError, "no ampersands please!"
self.style = style
self.simpletext = simpletext
self.lines = None
def wrap(self, availableWidth, availableHeight):
simpletext = self.simpletext
self.availableWidth = availableWidth
style = self.style
text = self.simpletext
rightIndent = style.rightIndent
leftIndent = style.leftIndent
leading = style.leading
font = style.fontName
size = style.fontSize
firstindent = style.firstLineIndent
#textcolor = style.textColor
words = simpletext.split()
lines = []
from reportlab.pdfbase.pdfmetrics import stringWidth
spacewidth = stringWidth(" ", font, size)
currentline = []
currentlength = 0
firstmaxlength = availableWidth - rightIndent - firstindent
maxlength = availableWidth - rightIndent - leftIndent
if maxlength<spacewidth:
return (spacewidth+rightIndent+firstindent, availableHeight) # need something wider than this!
if availableHeight<leading:
return (availableWidth, leading) # need something longer
if self.lines is None:
heightused = 0
cursor = 0
nwords = len(words)
done = 0
#heightused = leading # ???
while cursor<nwords and not done:
thismaxlength = maxlength
if not lines:
thismaxlength = firstmaxlength
thisword = words[cursor]
thiswordsize = stringWidth(thisword, font, size)
if currentlength:
thiswordsize = thiswordsize+spacewidth
nextlength = currentlength + thiswordsize
if not currentlength or nextlength<maxlength:
# add the word
cursor = cursor+1
currentlength = nextlength
currentline.append(thisword)
#print "currentline", currentline
else:
# emit the line
lines.append( (' '.join(currentline), currentlength, len(currentline)) )
currentline = []
currentlength = 0
heightused = heightused+leading
if heightused+leading>availableHeight:
done = 1
if currentlength and not done:
lines.append( (' '.join(currentline), currentlength, len(currentline) ))
heightused = heightused+leading
self.lines = lines
self.height = heightused
remainder = self.remainder = ' '.join(words[cursor:])
#print "lines", lines
#print "remainder is", remainder
else:
remainder = None
heightused = self.height
lines = self.lines
if remainder:
result = (availableWidth, availableHeight+leading) # need to split
else:
result = (availableWidth, heightused)
#if debug: print "wrap is", (availableWidth, availableHeight), result, len(lines)
return result
def split(self, availableWidth, availableHeight):
style = self.style
leading = style.leading
if availableHeight<leading:
return [] # not enough space for split
lines = self.lines
if lines is None:
raise ValueError, "must wrap before split"
remainder = self.remainder
if remainder:
next = FastPara(style, remainder)
return [self,next]
else:
return [self]
def draw(self):
style = self.style
lines = self.lines
rightIndent = style.rightIndent
leftIndent = style.leftIndent
leading = style.leading
font = style.fontName
size = style.fontSize
alignment = style.alignment
firstindent = style.firstLineIndent
c = self.canv
escape = c._escape
#if debug:
# print "FAST", id(self), "page number", c.getPageNumber()
height = self.height
#if debug:
# c.rect(0,0,-1, height-size, fill=1, stroke=1)
c.translate(0, height-size)
textobject = c.beginText()
code = textobject._code
#textobject.setTextOrigin(0,firstindent)
textobject.setFont(font, size)
if style.textColor:
textobject.setFillColor(style.textColor)
first = 1
y = 0
basicWidth = self.availableWidth - rightIndent
count = 0
nlines = len(lines)
while count<nlines:
(text, length, nwords) = lines[count]
count = count+1
thisindent = leftIndent
if first:
thisindent = firstindent
if alignment==TA_LEFT:
x = thisindent
elif alignment==TA_CENTER:
extra = basicWidth - length
x = thisindent + extra/2.0
elif alignment==TA_RIGHT:
extra = basicWidth - length
x = thisindent + extra
elif alignment==TA_JUSTIFY:
x = thisindent
if count<nlines and nwords>1:
# patch from doug@pennatus.com, 9 Nov 2002, no extraspace on last line
textobject.setWordSpace((basicWidth-length)/(nwords-1.0))
else:
textobject.setWordSpace(0.0)
textobject.setTextOrigin(x,y)
text = escape(text)
code.append('(%s) Tj' % text)
#textobject.textOut(text)
y = y-leading
c.drawText(textobject)
def getSpaceBefore(self):
#if debug:
# print "got space before", self.spaceBefore
return self.style.spaceBefore
def getSpaceAfter(self):
#print "got space after", self.spaceAfter
return self.style.spaceAfter
def defaultContext():
result = {}
from reportlab.lib.styles import getSampleStyleSheet
styles = getSampleStyleSheet()
for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items():
result[stylenamekey] = styles[stylenamevalue]
return result
def buildContext(stylesheet=None):
result = {}
from reportlab.lib.styles import getSampleStyleSheet
if stylesheet is not None:
# Copy styles with the same name as aliases
for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items():
if stylesheet.has_key(stylenamekey):
result[stylenamekey] = stylesheet[stylenamekey]
# Then make aliases
for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items():
if stylesheet.has_key(stylenamevalue):
result[stylenamekey] = stylesheet[stylenamevalue]
styles = getSampleStyleSheet()
# Then, fill in defaults if they were not filled yet.
for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items():
if not result.has_key(stylenamekey) and styles.has_key(stylenamevalue):
result[stylenamekey] = styles[stylenamevalue]
return result
class Para(Flowable):
spaceBefore = 0
spaceAfter = 0
def __init__(self, style, parsedText=None, bulletText=None, state=None, context=None, baseindent=0):
#print id(self), "para", parsedText
self.baseindent = baseindent
self.context = buildContext(context)
self.parsedText = parsedText
self.bulletText = bulletText
self.style1 = style # make sure Flowable doesn't use this unless wanted! call it style1 NOT style
#self.spaceBefore = self.spaceAfter = 0
self.program = [] # program before layout
self.formattedProgram = [] # after layout
self.remainder = None # follow on paragraph if any
self.state = state # initial formatting state (for completions)
if not state:
self.spaceBefore = style.spaceBefore
self.spaceAfter = style.spaceAfter
#self.spaceBefore = "invalid value"
#if hasattr(self, "spaceBefore") and debug:
# print "spaceBefore is", self.spaceBefore, self.parsedText
self.bold = 0
self.italic = 0
self.face = style.fontName
self.size = style.fontSize
def getSpaceBefore(self):
#if debug:
# print "got space before", self.spaceBefore
return self.spaceBefore
def getSpaceAfter(self):
#print "got space after", self.spaceAfter
return self.spaceAfter
def wrap(self, availableWidth, availableHeight):
if debug:
print "WRAPPING", id(self), availableWidth, availableHeight
print " ", self.formattedProgram
print " ", self.program
self.availableHeight = availableHeight
self.myengine = p = paragraphEngine()
p.baseindent = self.baseindent # for shifting bullets as needed
parsedText = self.parsedText
formattedProgram = self.formattedProgram
state = self.state
if state:
leading = state["leading"]
else:
leading = self.style1.leading
program = self.program
self.cansplit = 1 # until proven otherwise
if state:
p.resetState(state)
p.x = 0
p.y = 0
needatleast = state["leading"]
else:
needatleast = self.style1.leading
if availableHeight<=needatleast:
self.cansplit = 0
#if debug:
# print "CANNOT COMPILE, NEED AT LEAST", needatleast, 'AVAILABLE', availableHeight
return (availableHeight+1, availableWidth) # cannot split
if parsedText is None and program is None:
raise ValueError, "need parsedText for formatting"
if not program:
self.program = program = self.compileProgram(parsedText)
if not self.formattedProgram:
(formattedProgram, remainder, \
laststate, heightused) = p.format(availableWidth, availableHeight, program, leading)
self.formattedProgram = formattedProgram
self.height = heightused
self.laststate = laststate
self.remainderProgram = remainder
else:
heightused = self.height
remainder = None
# too big if there is a remainder
if remainder:
# lie about the height: it must be split anyway
#if debug:
# print "I need to split", self.formattedProgram
# print "heightused", heightused, "available", availableHeight, "remainder", len(remainder)
height = availableHeight + 1
#print "laststate is", laststate
#print "saving remainder", remainder
self.remainder = Para(self.style1, parsedText=None, bulletText=None, \
state=laststate, context=self.context)
self.remainder.program = remainder
self.remainder.spaceAfter = self.spaceAfter
self.spaceAfter = 0
else:
self.remainder = None # no extra
height = heightused
if height>availableHeight:
height = availableHeight-0.1
#if debug:
# print "giving height", height, "of", availableHeight, self.parsedText
result = (availableWidth, height)
if debug:
(w, h) = result
if abs(availableHeight-h)<0.2:
print "exact match???" + repr(availableHeight, h)
print "wrap is", (availableWidth, availableHeight), result
return result
def split(self, availableWidth, availableHeight):
#if debug:
# print "SPLITTING", id(self), availableWidth, availableHeight
if availableHeight<=0 or not self.cansplit:
#if debug:
# print "cannot split", availableWidth, "too small"
return [] # wrap failed to find a split
self.availableHeight = availableHeight
formattedProgram = self.formattedProgram
#print "formattedProgram is", formattedProgram
if formattedProgram is None:
raise ValueError, "must call wrap before split"
elif not formattedProgram:
# no first line in self: fail to split
return []
remainder = self.remainder
if remainder:
#print "SPLITTING"
result= [self, remainder]
else:
result= [self]
#if debug: print "split is", result
return result
def draw(self):
p = self.myengine #paragraphEngine()
formattedProgram = self.formattedProgram
if formattedProgram is None:
raise ValueError, "must call wrap before draw"
state = self.state
laststate = self.laststate
if state:
p.resetState(state)
p.x = 0
p.y = 0
c = self.canv
#if debug:
# print id(self), "page number", c.getPageNumber()
height = self.height
if state:
leading = state["leading"]
else:
leading = self.style1.leading
#if debug:
# c.rect(0,0,-1, height-self.size, fill=1, stroke=1)
c.translate(0, height-self.size)
t = c.beginText()
#t.setTextOrigin(0,0)
if DUMPPROGRAM or debug:
print "="*44, "now running program"
for x in formattedProgram:
print x
print "-"*44
laststate = p.runOpCodes(formattedProgram, c, t)
#print laststate["x"], laststate["y"]
c.drawText(t)
def compileProgram(self, parsedText, program=None):
style = self.style1
# standard parameters
#program = self.program
if program is None:
program = []
a = program.append
fn = style.fontName
# add style information if there was no initial state
a( ("face", fn ) )
from reportlab.lib.fonts import ps2tt
(self.face, self.bold, self.italic) = ps2tt(fn)
a( ("size", style.fontSize ) )
self.size = style.fontSize
a( ("align", style.alignment ) )
a( ("indent", style.leftIndent ) )
if style.firstLineIndent:
a( ("indent", style.firstLineIndent ) ) # must be undone later
a( ("rightIndent", style.rightIndent ) )
a( ("leading", style.leading) )
if style.textColor:
a( ("color", style.textColor) )
#a( ("nextLine", 0) ) # clear for next line
if self.bulletText:
self.do_bullet(self.bulletText, program)
self.compileComponent(parsedText, program)
# now look for a place where to insert the unindent after the first line
if style.firstLineIndent:
count = 0
for x in program:
count = count+1
tx = type(x)
if tx in (StringType, UnicodeType, InstanceType):
break
program.insert( count, ("indent", -style.firstLineIndent ) ) # defaults to end if no visibles
#print "="*8, id(self), "program is"
#for x in program:
# print x
## print "="*11
## # check pushes and pops
## stackcount = 0
## dump = 0
## for x in program:
## if dump:
## print "dump:", x
## if type(x) is TupleType:
## i = x[0]
## if i=="push":
## stackcount = stackcount+1
## print " "*stackcount, "push", stackcount
## if i=="pop":
## stackcount = stackcount-1
## print " "*stackcount, "pop", stackcount
## if stackcount<0:
## dump=1
## print "STACK UNDERFLOW!"
## if dump: stop
return program
def linearize(self, program = None, parsedText=None):
#print "LINEARIZING", self
#program = self.program = []
if parsedText is None:
parsedText = self.parsedText
style = self.style1
if program is None:
program = []
program.append( ("push",) )
if style.spaceBefore:
program.append( ("leading", style.spaceBefore+style.leading) )
else:
program.append( ("leading", style.leading) )
program.append( ("nextLine", 0) )
program = self.compileProgram(parsedText, program=program)
program.append( ("pop",) )
# go to old margin
program.append( ("push",) )
if style.spaceAfter:
program.append( ("leading", style.spaceAfter) )
else:
program.append( ("leading", 0) )
program.append( ("nextLine", 0) )
program.append( ("pop",) )
def compileComponent(self, parsedText, program):
import types
ttext = type(parsedText)
#program = self.program
if ttext in (StringType, UnicodeType):
# handle special characters here...
# short cut
if parsedText:
stext = parsedText.strip()
if not stext:
program.append(" ") # contract whitespace to single space
else:
handleSpecialCharacters(self, parsedText, program)
elif ttext is ListType:
for e in parsedText:
self.compileComponent(e, program)
elif ttext is TupleType:
(tagname, attdict, content, extra) = parsedText
if not attdict:
attdict = {}
compilername = "compile_"+tagname
compiler = getattr(self, compilername, None)
if compiler is not None:
compiler(attdict, content, extra, program)
else:
# just pass the tag through
if debug:
L = [ "<" + tagname ]
a = L.append
if not attdict: attdict = {}
for (k, v) in attdict.items():
a(" %s=%s" % (k,v))
if content:
a(">")
a(str(content))
a("</%s>" % tagname)
else:
a("/>")
t = ''.join(L)
handleSpecialCharacters(self, t, program)
else:
raise ValueError, "don't know how to handle tag " + repr(tagname)
def shiftfont(self, program, face=None, bold=None, italic=None):
oldface = self.face
oldbold = self.bold
olditalic = self.italic
oldfontinfo = (oldface, oldbold, olditalic)
if face is None: face = oldface
if bold is None: bold = oldbold
if italic is None: italic = olditalic
self.face = face
self.bold = bold
self.italic = italic
from reportlab.lib.fonts import tt2ps
font = tt2ps(face,bold,italic)
oldfont = tt2ps(oldface,oldbold,olditalic)
if font!=oldfont:
program.append( ("face", font ) )
return oldfontinfo
def compile_(self, attdict, content, extra, program):
# "anonymous" tag: just do the content
for e in content:
self.compileComponent(e, program)
#compile_para = compile_ # at least for now...
def compile_pageNumber(self, attdict, content, extra, program):
program.append(PageNumberObject())
def compile_b(self, attdict, content, extra, program):
(f,b,i) = self.shiftfont(program, bold=1)
for e in content:
self.compileComponent(e, program)
self.shiftfont(program, bold=b)
def compile_i(self, attdict, content, extra, program):
(f,b,i) = self.shiftfont(program, italic=1)
for e in content:
self.compileComponent(e, program)
self.shiftfont(program, italic=i)
def compile_u(self, attdict, content, extra, program):
# XXXX must eventually add things like alternative colors
#program = self.program
program.append( ('lineOperation', UNDERLINE) )
for e in content:
self.compileComponent(e, program)
program.append( ('endLineOperation', UNDERLINE) )
def compile_sub(self, attdict, content, extra, program):
size = self.size
self.size = newsize = size * 0.7
rise = size*0.5
#program = self.program
program.append( ('size', newsize) )
self.size = size
program.append( ('rise', -rise) )
for e in content:
self.compileComponent(e, program)
program.append( ('size', size) )
program.append( ('rise', rise) )
def compile_ul(self, attdict, content, extra, program, tagname="ul"):
# by transformation
#print "compile", tagname, attdict
atts = attdict.copy()
bulletmaker = bulletMaker(tagname, atts, self.context)
# now do each element as a separate paragraph
for e in content:
te = type(e)
if te in (StringType, UnicodeType):
if e.strip():
raise ValueError, "don't expect CDATA between list elements"
elif te is TupleType:
(tagname, attdict1, content1, extra) = e
if tagname!="li":
raise ValueError, "don't expect %s inside list" % repr(tagname)
newatts = atts.copy()
if attdict1:
newatts.update(attdict1)
bulletmaker.makeBullet(newatts)
self.compile_para(newatts, content1, extra, program)
def compile_ol(self, attdict, content, extra, program):
return self.compile_ul(attdict, content, extra, program, tagname="ol")
def compile_dl(self, attdict, content, extra, program):
# by transformation
#print "compile", tagname, attdict
atts = attdict.copy()
# by transformation
#print "compile", tagname, attdict
atts = attdict.copy()
bulletmaker = bulletMaker("dl", atts, self.context)
# now do each element as a separate paragraph
contentcopy = list(content) # copy for destruction
bullet = ""
while contentcopy:
e = contentcopy[0]
del contentcopy[0]
te = type(e)
if te in (StringType, UnicodeType):
if e.strip():
raise ValueError, "don't expect CDATA between list elements"
elif not contentcopy:
break # done at ending whitespace
else:
continue # ignore intermediate whitespace
elif te is TupleType:
(tagname, attdict1, content1, extra) = e
if tagname!="dd" and tagname!="dt":
raise ValueError, "don't expect %s here inside list, expect 'dd' or 'dt'" % \
repr(tagname)
if tagname=="dt":
if bullet:
raise ValueError, "dt will not be displayed unless followed by a dd: "+repr(bullet)
if content1:
self.compile_para(attdict1, content1, extra, program)
# raise ValueError, \
# "only simple strings supported in dd content currently: "+repr(content1)
elif tagname=="dd":
newatts = atts.copy()
if attdict1:
newatts.update(attdict1)
bulletmaker.makeBullet(newatts, bl=bullet)
self.compile_para(newatts, content1, extra, program)
bullet = "" # don't use this bullet again
if bullet:
raise ValueError, "dt will not be displayed unless followed by a dd"+repr(bullet)
def compile_super(self, attdict, content, extra, program):
size = self.size
self.size = newsize = size * 0.7
rise = size*0.5
#program = self.program
program.append( ('size', newsize) )
program.append( ('rise', rise) )
for e in content:
self.compileComponent(e, program)
program.append( ('size', size) )
self.size = size
program.append( ('rise', -rise) )
def compile_font(self, attdict, content, extra, program):
#program = self.program
program.append( ("push",) ) # store current data
if attdict.has_key("face"):
face = attdict["face"]
from reportlab.lib.fonts import tt2ps
try:
font = tt2ps(face,self.bold,self.italic)
except:
font = face # better work!
program.append( ("face", font ) )
if attdict.has_key("color"):
colorname = attdict["color"]
program.append( ("color", colorname) )
if attdict.has_key("size"):
#size = float(attdict["size"]) # really should convert int, cm etc here!
size = attdict["size"]
program.append( ("size", size) )
for e in content:
self.compileComponent(e, program)
program.append( ("pop",) ) # restore as before
def compile_a(self, attdict, content, extra, program):
url = attdict["href"]
colorname = attdict.get("color", "blue")
#program = self.program
Link = HotLink(url)
program.append( ("push",) ) # store current data
program.append( ("color", colorname) )
program.append( ('lineOperation', Link) )
program.append( ('lineOperation', UNDERLINE) )
for e in content:
self.compileComponent(e, program)
program.append( ('endLineOperation', UNDERLINE) )
program.append( ('endLineOperation', Link) )
program.append( ("pop",) ) # restore as before
def compile_link(self, attdict, content, extra, program):
dest = attdict["destination"]
colorname = attdict.get("color", None)
#program = self.program
Link = InternalLink(dest)
program.append( ("push",) ) # store current data
if colorname:
program.append( ("color", colorname) )
program.append( ('lineOperation', Link) )
program.append( ('lineOperation', UNDERLINE) )
for e in content:
self.compileComponent(e, program)
program.append( ('endLineOperation', UNDERLINE) )
program.append( ('endLineOperation', Link) )
program.append( ("pop",) ) # restore as before
def compile_setLink(self, attdict, content, extra, program):
dest = attdict["destination"]
colorname = attdict.get("color", "blue")
#program = self.program
Link = DefDestination(dest)
program.append( ("push",) ) # store current data
if colorname:
program.append( ("color", colorname) )
program.append( ('lineOperation', Link) )
if colorname:
program.append( ('lineOperation', UNDERLINE) )
for e in content:
self.compileComponent(e, program)
if colorname:
program.append( ('endLineOperation', UNDERLINE) )
program.append( ('endLineOperation', Link) )
program.append( ("pop",) ) # restore as before
#def compile_p(self, attdict, content, extra, program):
# # have to be careful about base indent here!
# not finished
def compile_bullet(self, attdict, content, extra, program):
### eventually should allow things like images and graphics in bullets too XXXX
if len(content)!=1 or type(content[0]) not in (StringType, UnicodeType):
raise ValueError, "content for bullet must be a single string"
text = content[0]
self.do_bullet(text, program)
def do_bullet(self, text, program):
style = self.style1
#program = self.program
indent = style.bulletIndent + self.baseindent
font = style.bulletFontName
size = style.bulletFontSize
program.append( ("bullet", text, indent, font, size) )
def compile_tt(self, attdict, content, extra, program):
(f,b,i) = self.shiftfont(program, face="Courier")
for e in content:
self.compileComponent(e, program)
self.shiftfont(program, face=f)
def compile_greek(self, attdict, content, extra, program):
self.compile_font({"face": "symbol"}, content, extra, program)
def compile_evalString(self, attdict, content, extra, program):
program.append( EvalStringObject(attdict, content, extra, self.context) )
def compile_name(self, attdict, content, extra, program):
program.append( NameObject(attdict, content, extra, self.context) )
def compile_getName(self, attdict, content, extra, program):
program.append( GetNameObject(attdict, content, extra, self.context) )
def compile_seq(self, attdict, content, extra, program):
program.append( SeqObject(attdict, content, extra, self.context) )
def compile_seqReset(self, attdict, content, extra, program):
program.append( SeqResetObject(attdict, content, extra, self.context) )
def compile_seqDefault(self, attdict, content, extra, program):
program.append( SeqDefaultObject(attdict, content, extra, self.context) )
def compile_para(self, attdict, content, extra, program, stylename = "para.defaultStyle"):
if attdict is None:
attdict = {}
context = self.context
stylename = attdict.get("style", stylename)
style = context[stylename]
newstyle = SimpleStyle(name="rml2pdf internal embedded style", parent=style)
newstyle.addAttributes(attdict)
bulletText = attdict.get("bulletText", None)
mystyle = self.style1
thepara = Para(newstyle, content, context=context, bulletText=bulletText)
# possible ref loop on context, break later
# now compile it and add it to the program
mybaseindent = self.baseindent
self.baseindent = thepara.baseindent = mystyle.leftIndent + self.baseindent
thepara.linearize(program=program)
program.append( ("nextLine", 0) )
self.baseindent = mybaseindent
class bulletMaker:
def __init__(self, tagname, atts, context):
self.tagname = tagname
#print "context is", context
style = "li.defaultStyle"
self.style = style = atts.get("style", style)
typ = {"ul": "disc", "ol": "1", "dl": None}[tagname]
#print tagname, "bulletmaker type is", typ
self.typ =typ = atts.get("type", typ)
#print tagname, "bulletmaker type is", typ
if not atts.has_key("leftIndent"):
# get the style so you can choose an indent length
thestyle = context[style]
from reportlab.pdfbase.pdfmetrics import stringWidth
size = thestyle.fontSize
indent = stringWidth("XXX", "Courier", size)
atts["leftIndent"] = str(indent)
self.count = 0
def makeBullet(self, atts, bl=None):
count = self.count = self.count+1
typ = self.typ
tagname = self.tagname
#print "makeBullet", tagname, typ, count
# forget space before for non-first elements
if count>1:
atts["spaceBefore"] = "0"
if bl is None:
if tagname=="ul":
if typ=="disc": bl = chr(109)
elif typ=="circle": bl = chr(108)
elif typ=="square": bl = chr(110)
else:
raise ValueError, "unordered list type %s not implemented" % repr(typ)
if not atts.has_key("bulletFontName"):
atts["bulletFontName"] = "ZapfDingbats"
elif tagname=="ol":
if typ=="1": bl = repr(count)
elif typ=="a":
theord = ord("a")+count-1
bl = chr(theord)
elif typ=="A":
theord = ord("A")+count-1
bl = chr(theord)
else:
raise ValueError, "ordered bullet type %s not implemented" % repr(typ)
else:
raise ValueError, "bad tagname "+repr(tagname)
if not atts.has_key("bulletText"):
atts["bulletText"] = bl
if not atts.has_key("style"):
atts["style"] = self.style
class EvalStringObject:
"this will only work if rml2pdf is present"
tagname = "evalString"
def __init__(self, attdict, content, extra, context):
if not attdict:
attdict = {}
self.attdict = attdict
self.content = content
self.context = context
self.extra = extra
def getOp(self, tuple, engine):
from rlextra.rml2pdf.rml2pdf import Controller
#print "tuple", tuple
op = self.op = Controller.processTuple(tuple, self.context, {})
return op
def width(self, engine):
from reportlab.pdfbase.pdfmetrics import stringWidth
content = self.content
if not content:
content = []
tuple = (self.tagname, self.attdict, content, self.extra)
op = self.op = self.getOp(tuple, engine)
#print op.__class__
#print op.pcontent
#print self
s = str(op)
return stringWidth(s, engine.fontName, engine.fontSize)
def execute(self, engine, textobject, canvas):
textobject.textOut(str(self.op))
class SeqObject(EvalStringObject):
def getOp(self, tuple, engine):
from reportlab.lib.sequencer import getSequencer
globalsequencer = getSequencer()
attr = self.attdict
#if it has a template, use that; otherwise try for id;
#otherwise take default sequence
if attr.has_key('template'):
templ = attr['template']
op = self.op = templ % globalsequencer
return op
elif attr.has_key('id'):
id = attr['id']
else:
id = None
op = self.op = globalsequencer.nextf(id)
return op
class NameObject(EvalStringObject):
tagname = "name"
def execute(self, engine, textobject, canvas):
pass # name doesn't produce any output
class SeqDefaultObject(NameObject):
def getOp(self, tuple, engine):
from reportlab.lib.sequencer import getSequencer
globalsequencer = getSequencer()
attr = self.attdict
try:
default = attr['id']
except KeyError:
default = None
globalsequencer.setDefaultCounter(default)
self.op = ""
return ""
class SeqResetObject(NameObject):
def getOp(self, tuple, engine):
from reportlab.lib.sequencer import getSequencer
import math
globalsequencer = getSequencer()
attr = self.attdict
try:
id = attr['id']
except KeyError:
id = None
try:
base = math.atoi(attr['base'])
except:
base=0
globalsequencer.reset(id, base)
self.op = ""
return ""
class GetNameObject(EvalStringObject):
tagname = "getName"
class PageNumberObject:
def __init__(self, example="XXX"):
self.example = example # XXX SHOULD ADD THE ABILITY TO PASS IN EXAMPLES
def width(self, engine):
from reportlab.pdfbase.pdfmetrics import stringWidth
return stringWidth(self.example, engine.fontName, engine.fontSize)
def execute(self, engine, textobject, canvas):
n = canvas.getPageNumber()
textobject.textOut(str(n))
### this should be moved into rml2pdf
def EmbedInRml2pdf():
"make the para the default para implementation in rml2pdf"
from rlextra.rml2pdf.rml2pdf import MapNode, Controller # may not need to use superclass?
global paraMapper, theParaMapper, ulMapper
class paraMapper(MapNode):
#stylename = "para.defaultStyle"
def translate(self, nodetuple, controller, context, overrides):
(tagname, attdict, content, extra) = nodetuple
stylename = tagname+".defaultStyle"
stylename = attdict.get("style", stylename)
style = context[stylename]
mystyle = SimpleStyle(name="rml2pdf internal style", parent=style)
mystyle.addAttributes(attdict)
bulletText = attdict.get("bulletText", None)
# can we use the fast implementation?
import types
result = None
if not bulletText and len(content)==1:
text = content[0]
if type(text) in (StringType, UnicodeType) and "&" not in text:
result = FastPara(mystyle, text)
if result is None:
result = Para(mystyle, content, context=context, bulletText=bulletText) # possible ref loop on context, break later
return result
theParaMapper = paraMapper()
class ulMapper(MapNode):
# wrap in a default para and let the para do it
def translate(self, nodetuple, controller, context, overrides):
thepara = ("para", {}, [nodetuple], None)
return theParaMapper.translate(thepara, controller, context, overrides)
# override rml2pdf interpreters (should be moved to rml2pdf)
theListMapper = ulMapper()
Controller["ul"] = theListMapper
Controller["ol"] = theListMapper
Controller["dl"] = theListMapper
Controller["para"] = theParaMapper
Controller["h1"] = theParaMapper
Controller["h2"] = theParaMapper
Controller["h3"] = theParaMapper
Controller["title"] = theParaMapper
def handleSpecialCharacters(engine, text, program=None):
from paraparser import greeks, symenc
from string import whitespace, atoi, atoi_error
standard={'lt':'<', 'gt':'>', 'amp':'&'}
# add space prefix if space here
if text[0:1] in whitespace:
program.append(" ")
#print "handling", repr(text)
# shortcut
if 0 and "&" not in text:
result = []
for x in text.split():
result.append(x+" ")
if result:
last = result[-1]
if text[-1:] not in whitespace:
result[-1] = last.strip()
program.extend(result)
return program
if program is None:
program = []
amptext = text.split("&")
first = 1
lastfrag = amptext[-1]
for fragment in amptext:
if not first:
# check for special chars
semi = fragment.find(";")
if semi>0:
name = fragment[:semi]
if name[0]=='#':
try:
if name[1] == 'x':
n = atoi(name[2:], 16)
else:
n = atoi(name[1:])
except atoi_error:
n = -1
if 0<=n<=255: fragment = chr(n)+fragment[semi+1:]
elif symenc.has_key(n):
fragment = fragment[semi+1:]
(f,b,i) = engine.shiftfont(program, face="symbol")
program.append(symenc[n])
engine.shiftfont(program, face=f)
if fragment and fragment[0] in whitespace:
program.append(" ") # follow with a space
else:
fragment = "&"+fragment
elif standard.has_key(name):
fragment = standard[name]+fragment[semi+1:]
elif greeks.has_key(name):
fragment = fragment[semi+1:]
greeksub = greeks[name]
(f,b,i) = engine.shiftfont(program, face="symbol")
program.append(greeksub)
engine.shiftfont(program, face=f)
if fragment and fragment[0] in whitespace:
program.append(" ") # follow with a space
else:
# add back the &
fragment = "&"+fragment
else:
# add back the &
fragment = "&"+fragment
# add white separated components of fragment followed by space
sfragment = fragment.split()
for w in sfragment[:-1]:
program.append(w+" ")
# does the last one need a space?
if sfragment and fragment:
# reader 3 used to go nuts if you don't special case the last frag, but it's fixed?
if fragment[-1] in whitespace: # or fragment==lastfrag:
program.append( sfragment[-1]+" " )
else:
last = sfragment[-1].strip()
if last:
#print "last is", repr(last)
program.append( last )
first = 0
#print "HANDLED", program
return program
def Paragraph(text, style, bulletText=None, frags=None, context=None):
""" Paragraph(text, style, bulletText=None)
intended to be like a platypus Paragraph but better.
"""
# if there is no & or < in text then use the fast paragraph
if "&" not in text and "<" not in text:
return FastPara(style, simpletext=text)
else:
# use the fully featured one.
from reportlab.lib import rparsexml
parsedpara = rparsexml.parsexmlSimple(text,entityReplacer=None)
return Para(style, parsedText=parsedpara, bulletText=bulletText, state=None, context=context)
class UnderLineHandler:
def __init__(self, color=None):
self.color = color
def start_at(self, x,y, para, canvas, textobject):
self.xStart = x
self.yStart = y
def end_at(self, x, y, para, canvas, textobject):
offset = para.fontSize/8.0
canvas.saveState()
color = self.color
if self.color is None:
color = para.fontColor
canvas.setStrokeColor(color)
canvas.line(self.xStart, self.yStart-offset, x,y-offset)
canvas.restoreState()
UNDERLINE = UnderLineHandler()
class HotLink(UnderLineHandler):
def __init__(self, url):
self.url = url
def end_at(self, x, y, para, canvas, textobject):
fontsize = para.fontSize
rect = [self.xStart, self.yStart, x,y+fontsize]
if debug:
print "LINKING RECTANGLE", rect
#canvas.rect(self.xStart, self.yStart, x-self.xStart,y+fontsize-self.yStart, stroke=1)
self.link(rect, canvas)
def link(self, rect, canvas):
canvas.linkURL(self.url, rect, relative=1)
class InternalLink(HotLink):
def link(self, rect, canvas):
destinationname = self.url
contents = ""
canvas.linkRect(contents, destinationname, rect, Border="[0 0 0]")
class DefDestination(HotLink):
defined = 0
def link(self, rect, canvas):
destinationname = self.url
if not self.defined:
[x, y, x1, y1] = rect
canvas.bookmarkHorizontal(destinationname, x, y1) # use the upper y
self.defined = 1
def splitspace(text):
# split on spacing but include spaces at element ends
stext = text.split()
result = []
for e in stext:
result.append(e+" ")
return result
testparagraph = """
This is Text.
<b>This is bold text.</b>
This is Text.
<i>This is italic text.</i>
<ul>
<li> this is an element at 1
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
more text <tt>monospaced</tt> and back to normal
<ul>
<li> this is an element at 2
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
<ul>
<li> this is an element at 3
more text and even more text and on and on and so forth
<dl bulletFontName="Helvetica-BoldOblique" spaceBefore="10" spaceAfter="10">
<dt>frogs</dt> <dd>Little green slimy things. Delicious with <b>garlic</b></dd>
<dt>kittens</dt> <dd>cute, furry, not edible</dd>
<dt>bunnies</dt> <dd>cute, furry,. Delicious with <b>garlic</b></dd>
</dl>
more text and even more text and on and on and so forth
<ul>
<li> this is an element at 4
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
</li>
</ul>
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
</li>
</ul>
more text and even more text and on and on and so forth
more text and even more text and on and on and so forth
</li>
</ul>
<u><b>UNDERLINED</b> more text and even more text and on and on and so forth
more text and even more text and on and on and so forth</u>
<ol type="a">
<li>first element of the alpha list
<ul type="square">
<li>first element of the square unnumberred list</li>
<li>second element of the unnumberred list</li>
<li>third element of the unnumberred list
third element of the unnumberred list
third element of the unnumberred list
third element of the unnumberred list
third element of the unnumberred list
third element of the unnumberred list
third element of the unnumberred list
</li>
<li>fourth element of the unnumberred list</li>
</ul>
</li>
<li>second element of the alpha list</li>
<li>third element of the alpha list
third element of the unnumberred list &amp;#33; --> &#33;
third element of the unnumberred list &amp;#8704; --> &#8704;
third element of the unnumberred list &amp;exist; --> &exist;
third element of the unnumberred list
third element of the unnumberred list
third element of the unnumberred list
</li>
<li>fourth element of the alpha list</li>
</ol>
</li>
</ul>
"""
testparagraph1 = """
<a href="http://www.reportlab.com">goto www.reportlab.com</a>.
<para alignment="justify">
<font color="red" size="15">R</font>ed letter. thisisareallylongword andsoisthis andthisislonger
justified text paragraph example
justified text paragraph example
justified text paragraph example
</para>
<para alignment="center">
<font color="green" size="15">G</font>reen letter.
centered text paragraph example
centered text paragraph example
centered text paragraph example
</para>
<para alignment="right">
<font color="blue" size="15">B</font>lue letter.
right justified text paragraph example
right justified text paragraph example
right justified text paragraph example
</para>
<para alignment="left">
<font color="yellow" size="15">Y</font>ellow letter.
left justified text paragraph example
left justified text paragraph example
left justified text paragraph example
</para>
"""
def test2(canv,testpara):
#print test_program; return
from reportlab.lib.units import inch
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib import rparsexml
parsedpara = rparsexml.parsexmlSimple(testpara,entityReplacer=None)
S = ParagraphStyle("Normal", None)
P = Para(S, parsedpara)
(w, h) = P.wrap(5*inch, 10*inch)
print "wrapped as", (h,w)
canv.saveState()
canv.translate(1*inch, 1*inch)
canv.rect(0,0,5*inch,10*inch, fill=0, stroke=1)
P.canv = canv
canv.saveState()
P.draw()
canv.restoreState()
canv.setStrokeColorRGB(1, 0, 0)
#canv.translate(0, 3*inch)
canv.rect(0,0,w,h, fill=0, stroke=1)
canv.restoreState()
canv.showPage()
testlink = HotLink("http://www.reportlab.com")
test_program = [
('push',),
('indent', 100),
('rightIndent', 200),
('bullet', 'very long bullet', 50, 'Courier', 14),
('align', TA_CENTER),
('face', "Times-Roman"),
('size', 12),
('leading', 14),
] + splitspace("This is the first segment of the first paragraph.") + [
('lineOperation', testlink),
]+splitspace("HOTLINK This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [
('endLineOperation', testlink),
('nextLine', 0),
('align', TA_LEFT),
('bullet', 'Bullet', 10, 'Courier', 8),
('face', "Times-Roman"),
('size', 12),
('leading', 14),
] + splitspace("This is the SECOND!!! segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [
('nextLine', 0),
('align', TA_JUSTIFY),
('bullet', 'Bullet not quite as long this time', 50, 'Courier', 8),
('face', "Helvetica-Oblique"),
('size', 12),
('leading', 14),
('push',),
('color', 'red'),
] + splitspace("This is the THIRD!!! segment of the first paragraph."
) + [
('lineOperation', UNDERLINE),
] + splitspace("This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [
('endLineOperation', UNDERLINE),
('rise', 5),
"raised ", "text ",
('rise', -10),
"lowered ", "text ",
('rise', 5),
"normal ", "text ",
('pop',),
('indent', 100),
('rightIndent', 50),
('nextLine', 0),
('align', TA_RIGHT),
('bullet', 'O', 50, 'Courier', 14),
('face', "Helvetica"),
('size', 12),
('leading', 14),
] + splitspace("And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a ") + [
('pop',),
('nextLine', 0),]
def test():
from pprint import pprint
#print test_program; return
from reportlab.pdfgen import canvas
from reportlab.lib.units import inch
fn = "paratest0.pdf"
c = canvas.Canvas(fn)
test2(c,testparagraph)
test2(c,testparagraph1)
if 1:
remainder = test_program + test_program + test_program
laststate = {}
while remainder:
print "NEW PAGE"
c.translate(inch, 8*inch)
t = c.beginText()
t.setTextOrigin(0,0)
p = paragraphEngine()
p.resetState(laststate)
p.x = 0
p.y = 0
maxwidth = 7*inch
maxheight = 500
(formattedprogram, remainder, laststate, height) = p.format(maxwidth, maxheight, remainder)
if debug:
pprint( formattedprogram )#; return
laststate = p.runOpCodes(formattedprogram, c, t)
c.drawText(t)
c.showPage()
print "="*30, "x=", laststate["x"], "y=", laststate["y"]
c.save()
print fn
if __name__=="__main__":
test()