reportlab/platypus/layout.py
author rgbecker
Tue, 16 May 2000 16:15:16 +0000
changeset 221 3d71b66b14c6
parent 212 b04e22acb4fa
child 226 a9acb67cdef7
permissions -rwxr-xr-x
Changes related to removal of SimpleFlowDocument

###############################################################################
#
#	ReportLab Public License Version 1.0
#
#	Except for the change of names the spirit and intention of this
#	license is the same as that of Python
#
#	(C) Copyright ReportLab Inc. 1998-2000.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted, provided
# that the above copyright notice appear in all copies and that both that
# copyright notice and this permission notice appear in supporting
# documentation, and that the name of ReportLab not be used
# in advertising or publicity pertaining to distribution of the software
# without specific, written prior permission.
#
#
# Disclaimer
#
# ReportLab Inc. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS,
# IN NO EVENT SHALL ReportLab BE LIABLE FOR ANY SPECIAL, INDIRECT
# OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
###############################################################################
#	$Log: layout.py,v $
#	Revision 1.26  2000/05/16 16:15:16  rgbecker
#	Changes related to removal of SimpleFlowDocument
#
#	Revision 1.25  2000/05/15 13:36:11  rgbecker
#	Splitting changes
#	
#	Revision 1.24  2000/05/12 12:52:00  rgbecker
#	Fixes to BasicFrame split method
#	
#	Revision 1.23  2000/05/11 14:02:14  rgbecker
#	Removed usage of spaceBefore/After in wrap methods
#	
#	Revision 1.22  2000/05/10 13:04:35  rgbecker
#	Added softspace handling
#	
#	Revision 1.21  2000/05/10 09:54:40  rgbecker
#	Flowable.split should return list
#	
#	Revision 1.20  2000/04/28 13:39:12  rgbecker
#	Fix _doNothing argument name
#	
#	Revision 1.19  2000/04/26 11:07:15  andy_robinson
#	Tables changed to use reportlab.lib.colors instead of
#	the six hard-coded color strings there previously.
#	
#	Revision 1.18  2000/04/25 15:42:04  rgbecker
#	Factored out BasicFrame from SimpleFrame
#	
#	Revision 1.17  2000/04/14 16:12:11  rgbecker
#	Debugging xml changes
#	
#	Revision 1.16  2000/04/14 11:54:57  rgbecker
#	Splitting layout.py
#	
#	Revision 1.15  2000/04/14 08:56:20  rgbecker
#	Drawable ==> Flowable
#	
#	Revision 1.14  2000/04/13 17:06:40  rgbecker
#	Fixed SimpleFrame.add
#	
#	Revision 1.13  2000/04/13 14:48:41  rgbecker
#	<para> tag added in layout.py paraparser.py
#	
#	Revision 1.12  2000/04/12 16:26:51  rgbecker
#	XML Tagged Paragraph parser changes
#	
#	Revision 1.11  2000/04/10 14:08:19  rgbecker
#	fixes related to offset
#	
#	Revision 1.10  2000/04/10 12:25:10  rgbecker
#	Typo fixes for justified paras
#	
#	Revision 1.9  2000/04/07 12:31:29  rgbecker
#	Color fixes/changes
#	
#	Revision 1.8  2000/04/06 09:52:02  andy_robinson
#	Removed some old comments; tweaks to experimental Outline methods.
#	
#	Revision 1.7  2000/03/08 13:06:39  andy_robinson
#	Moved inch and cm definitions to reportlab.lib.units and amended all demos
#	
#	Revision 1.6  2000/02/23 10:53:33  rgbecker
#	GMCM's memleak fixed
#	
#	Revision 1.5  2000/02/17 02:09:05  rgbecker
#	Docstring & other fixes
#	
#	Revision 1.4  2000/02/16 09:42:50  rgbecker
#	Conversion to reportlab package
#	
#	Revision 1.3  2000/02/15 17:55:59  rgbecker
#	License text fixes
#	
#	Revision 1.2  2000/02/15 15:47:09  rgbecker
#	Added license, __version__ and Logi comment
#	
__version__=''' $Id: layout.py,v 1.26 2000/05/16 16:15:16 rgbecker Exp $ '''
__doc__="""
Page Layout And TYPography Using Scripts
a page layout API on top of PDFgen
currently working on paragraph wrapping stuff.
"""

# 200-10-13 gmcm
#	packagizing
#	rewrote grid stuff - now in tables.py

import string

from reportlab.pdfgen import canvas
from reportlab.lib.units import inch
from reportlab.lib.colors import red

from reportlab.lib.pagesizes import DEFAULT_PAGE_SIZE
PAGE_HEIGHT = DEFAULT_PAGE_SIZE[1]

#############################################################
#	Flowable Objects - a base class and a few examples.
#	One is just a box to get some metrics.	We also have
#	a paragraph, an image and a special 'page break'
#	object which fills the space.
#############################################################
class Flowable:
	"""Abstract base class for things to be drawn.	Key concepts:
	1. It knows its size
	2. It draws in its own coordinate system (this requires the
		base API to provide a translate() function.
	"""
	def __init__(self):
		self.width = 0
		self.height = 0
		self.wrapped = 0

	def drawOn(self, canvas, x, y):
		"Tell it to draw itself on the canvas.	Do not override"
		self.canv = canvas
		self.canv.saveState()
		self.canv.translate(x, y)

		self.draw()   #this is the bit you overload

		self.canv.restoreState()
		del self.canv


	def wrap(self, availWidth, availHeight):
		"""This will be called by the enclosing frame before objects
		are asked their size, drawn or whatever.  It returns the
		size actually used."""
		return (self.width, self.height)

	def split(self, availWidth, availheight):
		"""This will be called by more sophisticated frames when
		wrap fails. Stupid flowables should return []. Clever flowables
		should split themselves and return a list of flowables"""
		return []

	def getSpaceAfter(self):
		if hasattr(self,'spaceAfter'): return self.spaceAfter
		elif hasattr(self,'style') and hasattr(self.style,'spaceAfter'): return self.style.spaceAfter
		else: return 0

	def getSpaceBefore(self):
		if hasattr(self,'spaceBefore'): return self.spaceBefore
		elif hasattr(self,'style') and hasattr(self.style,'spaceBefore'): return self.style.spaceBefore
		else: return 0

class XBox(Flowable):
	"""Example flowable - a box with an x through it and a caption.
	This has a known size, so does not need to respond to wrap()."""
	def __init__(self, width, height, text = 'A Box'):
		Flowable.__init__(self)
		self.width = width
		self.height = height
		self.text = text

	def draw(self):
		self.canv.rect(0, 0, self.width, self.height)
		self.canv.line(0, 0, self.width, self.height)
		self.canv.line(0, self.height, self.width, 0)

		#centre the text
		self.canv.setFont('Times-Roman',12)
		self.canv.drawCentredString(0.5*self.width, 0.5*self.height, self.text)

class Preformatted(Flowable):
	"""This is like the HTML <PRE> tag.  The line breaks are exactly where you put
	them, and it will not be wrapped.  So it is much simpler to implement!"""
	def __init__(self, text, style, bulletText = None, dedent=0):
		self.style = style
		self.bulletText = bulletText

		#tidy up text - carefully, it is probably code.  If people want to
		#indent code within a source script, you can supply an arg to dedent
		#and it will chop off that many character, otherwise it leaves
		#left edge intact.

		templines = string.split(text, '\n')
		self.lines = []
		for line in templines:
			line = string.rstrip(line[dedent:])
			self.lines.append(line)
		#don't want the first or last to be empty
		while string.strip(self.lines[0]) == '':
			self.lines = self.lines[1:]
		while string.strip(self.lines[-1]) == '':
			self.lines = self.lines[:-1]

	def wrap(self, availWidth, availHeight):
		self.width = availWidth
		self.height = self.style.leading*len(self.lines)
		return (self.width, self.height)

	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.

		cur_x = self.style.leftIndent
		cur_y = self.height - self.style.fontSize
		self.canv.addLiteral('%PreformattedPara')

		tx = self.canv.beginText(cur_x, cur_y)
		#set up the font etc.
		tx.setFont(self.style.fontName,
				   self.style.fontSize,
				   self.style.leading)

		for text in self.lines:
			tx.textLine(text)
		self.canv.drawText(tx)

class Image(Flowable):
	def __init__(self, filename, width=None, height=None):
		"""If size to draw at not specified, get it from the image."""
		import Image  #this will raise an error if they do not have PIL.
		self.filename = filename
		print 'Creating Image for', filename
		img = Image.open(filename)
		(self.imageWidth, self.imageHeight) = img.size
		if width:
			self.drawWidth = width
		else:
			self.drawWidth = self.imageWidth
		if height:
			self.drawHeight = height
		else:
			self.drawHeight = self.imageHeight

	def wrap(self, availWidth, availHeight):
		#the caller may decide it does not fit.
		self.availWidth = availWidth
		return (self.drawWidth, self.drawHeight)

	def draw(self):
		#center it
		startx = 0.5 * (self.availWidth - self.drawWidth)
		self.canv.drawInlineImage(self.filename,
								  startx,
								  0,
								  self.drawWidth,
								  self.drawHeight
								  )
class Spacer(Flowable):
	"""A spacer just takes up space and doesn't draw anything - it can
	ensure a gap between objects."""
	def __init__(self, width, height):
		self.width = width
		self.height = height

	def wrap(self, availWidth, availHeight):
		return (self.width, self.height)

	def draw(self):
		pass

class PageBreak(Flowable):
	"""This works by consuming all remaining space in the frame!"""

	def wrap(self, availWidth, availHeight):
		self.width = availWidth
		self.height = availHeight
		return (availWidth,availHeight)  #step back a point

	def draw(self):
		pass

class Macro(Flowable):
	"""This is not actually drawn (i.e. it has zero height)
	but is executed when it would fit in the frame.  Allows direct
	access to the canvas through the object 'canvas'"""
	def __init__(self, command):
		self.command = command
	def wrap(self, availWidth, availHeight):
		return (0,0)
	def draw(self):
		exec self.command in globals(), {'canvas':self.canv}

class BasicFrame:
	'''Abstraction for the definitional part of a Frame

                width                    x2,y2
    	+---------------------------------+
    	| l  top padding                r | h
    	| e +-------------------------+ i | e
    	| f |                         | g | i
    	| t |                         | h | g
    	|   |                         | t | h
    	| p |                         |   | t
    	| a |                         | p |
    	| d |                         | a |
    	|   |                         | d |
    	|   +-------------------------+   |
    	|    bottom padding				  |
    	+---------------------------------+
    	(x1,y1)
	'''
	def __init__(self, x1, y1, width,height, leftPadding=6, bottomPadding=6,
			rightPadding=6, topPadding=6, id=None, showBoundary=0):

		self.id = id

		#these say where it goes on the page
		self.x1 = x1
		self.y1 = y1
		self.x2 = x1 + width
		self.y2 = y1 + height

		#these create some padding.
		self.leftPadding = leftPadding
		self.bottomPadding = bottomPadding
		self.rightPadding = rightPadding
		self.topPadding = topPadding

		#efficiency
		self.y1p = self.y1 + bottomPadding

		# if we want a boundary to be shown
		self.showBoundary = showBoundary

		self._reset()

	def	_reset(self):
		#work out the available space
		self.width = self.x2 - self.x1 - self.leftPadding - self.rightPadding
		self.height = self.y2 - self.y1 - self.topPadding - self.bottomPadding
		#drawing starts at top left
		self.x = self.x1 + self.leftPadding
		self.y = self.y2 - self.topPadding
		self.atTop = 1

	def _add(self, flowable, canv, trySplit=0):
		""" Draws the flowable at the current position.
		Returns 1 if successful, 0 if it would not fit.
		Raises a LayoutError if the object is too wide,
		or if it is too high for a totally empty frame,
		to avoid infinite loops"""
		y = self.y
		p = self.y1p
		s = self.atTop and 0 or flowable.getSpaceBefore()
		h = y - p - s
		if h>0:
			w, h = flowable.wrap(self.width, h)
		else:
			return 0

		h = h + s
		y = y - h

		if y < p:
			if ((h > self.height and not trySplit) or w > self.width):
				raise "LayoutError", "Flowable (%dx%d points) too large for frame (%dx%d points)." % (w,h, self.width,self.height)
			return 0
		else:
			#now we can draw it, and update the current point.
			flowable.drawOn(canv, self.x, y)
			y = y - flowable.getSpaceAfter()
			self.atTop = 0
			self.y = y
			return 1

	add = _add

	def split(self,flowable):
		'''calls split on the flowable'''
		y = self.y
		p = self.y1p
		s = self.atTop and 0 or flowable.getSpaceBefore()
		return flowable.split(self.width, y-p-s)


#############################################################
#	A Frame, and a Document Model
#############################################################
FrameFullError = "FrameFullError"
LayoutError = "LayoutError"

class SimpleFrame(BasicFrame):
	"""A region into which flowable objects are to be packed.
	Flows downwards.  A more general solution is needed which
	will allow flows in any direction, including 'across and then
	down' for small objects, but this is useful for
	many languages now, as long as each object is 'full-width'
	(i.e. a paragraph and not a word)."""
	def __init__(self, canvas, x1, y1, width,height):
		BasicFrame.__init__(self, x1, y1, width,height, leftPadding=6, bottomPadding=6,
			rightPadding=6, topPadding=6, id=None, showBoundary=0)
		self.canvas = canvas
		self.objects = []	#it keeps a list of objects

	def add(self, flowable):
		r = self._add(flowable, self.canvas)
		if r: self.objects.append(flowable)
		return r

	def addFromList(self, drawlist):
		"""Consumes objects from the front of the list until the
		frame is full.	If it cannot fit one object, raises
		an exception."""

		if self.showBoundary:
			self.canvas.rect(
						self.x1,
						self.y1,
						self.x2 - self.x1,
						self.y2 - self.y1
						)

		while len(drawlist) > 0:
			head = drawlist[0]
			if self.add(head):
				del drawlist[0]
			else:
				#leave it in the list for later
				break

class Sequencer:
	"""Something to make it easy to number paragraphs, sections,
	images and anything else. Usage:
		>>> seq = layout.Sequencer()
		>>> seq.next('Bullets')
		1
		>>> seq.next('Bullets')
		2
		>>> seq.next('Bullets')
		3
		>>> seq.reset('Bullets')
		>>> seq.next('Bullets')
		1
		>>> seq.next('Figures')
		1
		>>>
	I plan to add multi-level linkages, so that Head2 could be reet
	"""
	def __init__(self):
		self.dict = {}

	def next(self, category):
		if self.dict.has_key(category):
			self.dict[category] = self.dict[category] + 1
		else:
			self.dict[category] = 1
		return self.dict[category]

	def reset(self, category):
		self.dict[category] = 0

def _doNothing(canvas, doc):
	"Dummy callback for onPage"
	pass

##########################################################
#
#
#
##########################################################
class SimpleFlowDocument:
	"""A sample document that uses a single frame on each page.
	The intention is for programmers to create their own document
	models as needed.  This one accepts a list of flowables. You
	can provide callbacks to decorate the first page and
	subsequent pages; these should do headers, footers, sidebars
	as needed."""
	def __init__(self, filename, pagesize, showBoundary=0):
		self.filename = filename
		self.pagesize = pagesize
		self.showBoundary=showBoundary
		#sensibel defaults; override if you wish
		self.leftMargin =  inch
		self.bottomMargin = inch
		self.rightMargin = self.pagesize[0] - inch
		self.topMargin = self.pagesize[1] - inch

		# 1-based counting is friendlier for readers
		self.page = 1
		#set these to drawing procedures of your own
		self.onFirstPage = _doNothing
		self.onNewPage = _doNothing


	def build(self, flowables):
		canv = canvas.Canvas(self.filename)
		#canv.setPageTransition('Dissolve')

		# do page 1
		self.onFirstPage(canv, self)
		frame1 = SimpleFrame(
					canv,
					self.leftMargin,
					self.bottomMargin,
					self.rightMargin - self.leftMargin,
					self.topMargin - inch - self.bottomMargin
							)
		frame1.showBoundary = self.showBoundary
		frame1.addFromList(flowables)
		#print 'drew page %d, %d objects remaining' % (self.page, len(flowables))
		# do subsequent pages
		while len(flowables) > 0:
			canv.showPage()
			self.page = self.page + 1
			self.onNewPage(canv, self)
			frame = SimpleFrame(
					canv,
					self.leftMargin,
					self.bottomMargin,
					self.rightMargin - self.leftMargin,
					self.topMargin - self.bottomMargin
							)
			frame.showBoundary = self.showBoundary
			frame.addFromList(flowables)

			#print 'drew page %d, %d objects remaining' % (self.page, len(flowables))

		canv.save()