#Copyright ReportLab Europe Ltd. 2000-2017
#see license.txt for license details
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/renderPDF.py
# renderPDF - draws Drawings onto a canvas
__version__='3.3.0'
__doc__="""Render Drawing objects within others PDFs or standalone
Usage::
import renderpdf
renderpdf.draw(drawing, canvas, x, y)
Execute the script to see some test drawings.
changed
"""
from reportlab.graphics.shapes import *
from reportlab.pdfgen.canvas import Canvas
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.lib.utils import getBytesIO
from reportlab import ascii, rl_config
from reportlab.graphics.renderbase import Renderer, StateTracker, getStateDelta, renderScaledDrawing, STATE_DEFAULTS
# the main entry point for users...
def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_):
"""As it says"""
R = _PDFRenderer()
R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary)
class _PDFRenderer(Renderer):
"""This draws onto a PDF document. It needs to be a class
rather than a function, as some PDF-specific state tracking is
needed outside of the state info in the SVG model."""
def __init__(self):
self._stroke = 0
self._fill = 0
def drawNode(self, node):
"""This is the recursive method called for each node
in the tree"""
#print "pdf:drawNode", self
#if node.__class__ is Wedge: stop
if not (isinstance(node, Path) and node.isClipPath):
self._canvas.saveState()
#apply state changes
deltas = getStateDelta(node)
self._tracker.push(deltas)
self.applyStateChanges(deltas, {})
#draw the object, or recurse
self.drawNodeDispatcher(node)
self._tracker.pop()
if not (isinstance(node, Path) and node.isClipPath):
self._canvas.restoreState()
def drawRect(self, rect):
if rect.rx == rect.ry == 0:
#plain old rectangle
self._canvas.rect(
rect.x, rect.y,
rect.width, rect.height,
stroke=self._stroke,
fill=self._fill
)
else:
#cheat and assume ry = rx; better to generalize
#pdfgen roundRect function. TODO
self._canvas.roundRect(
rect.x, rect.y,
rect.width, rect.height, rect.rx,
fill=self._fill,
stroke=self._stroke
)
def drawImage(self, image):
path = image.path
# currently not implemented in other renderers
if path and (hasattr(path,'mode') or os.path.exists(image.path)):
self._canvas.drawInlineImage(
path,
image.x, image.y,
image.width, image.height
)
def drawLine(self, line):
if self._stroke:
self._canvas.line(line.x1, line.y1, line.x2, line.y2)
def drawCircle(self, circle):
self._canvas.circle(
circle.cx, circle.cy, circle.r,
fill=self._fill,
stroke=self._stroke,
)
def drawPolyLine(self, polyline):
if self._stroke:
assert len(polyline.points) >= 2, 'Polyline must have 2 or more points'
head, tail = polyline.points[0:2], polyline.points[2:],
path = self._canvas.beginPath()
path.moveTo(head[0], head[1])
for i in range(0, len(tail), 2):
path.lineTo(tail[i], tail[i+1])
self._canvas.drawPath(path)
def drawWedge(self, wedge):
if wedge.annular:
self.drawPath(wedge.asPolygon())
else:
centerx, centery, radius, startangledegrees, endangledegrees = \
wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees
yradius, radius1, yradius1 = wedge._xtraRadii()
if yradius is None: yradius = radius
angle = endangledegrees-startangledegrees
path = self._canvas.beginPath()
if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None):
path.moveTo(centerx, centery)
path.arcTo(centerx-radius, centery-yradius, centerx+radius, centery+yradius,
startangledegrees, angle)
else:
path.arc(centerx-radius, centery-yradius, centerx+radius, centery+yradius,
startangledegrees, angle)
path.arcTo(centerx-radius1, centery-yradius1, centerx+radius1, centery+yradius1,
endangledegrees, -angle)
path.close()
self._canvas.drawPath(path,
fill=self._fill,
stroke=self._stroke,
)
def drawEllipse(self, ellipse):
#need to convert to pdfgen's bounding box representation
x1 = ellipse.cx - ellipse.rx
x2 = ellipse.cx + ellipse.rx
y1 = ellipse.cy - ellipse.ry
y2 = ellipse.cy + ellipse.ry
self._canvas.ellipse(x1,y1,x2,y2,fill=self._fill,stroke=self._stroke)
def drawPolygon(self, polygon):
assert len(polygon.points) >= 2, 'Polyline must have 2 or more points'
head, tail = polygon.points[0:2], polygon.points[2:],
path = self._canvas.beginPath()
path.moveTo(head[0], head[1])
for i in range(0, len(tail), 2):
path.lineTo(tail[i], tail[i+1])
path.close()
self._canvas.drawPath(
path,
stroke=self._stroke,
fill=self._fill,
)
def drawString(self, stringObj):
if self._fill:
S = self._tracker.getState()
text_anchor, x, y, text, enc = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text, stringObj.encoding
if not text_anchor in ['start','inherited']:
font, font_size = S['fontName'], S['fontSize']
textLen = stringWidth(text, font, font_size, enc)
if text_anchor=='end':
x -= textLen
elif text_anchor=='middle':
x -= textLen*0.5
elif text_anchor=='numeric':
x -= numericXShift(text_anchor,text,textLen,font,font_size,enc)
else:
raise ValueError('bad value for textAnchor '+str(text_anchor))
t = self._canvas.beginText(x,y)
t.textLine(text)
self._canvas.drawText(t)
def drawPath(self, path):
from reportlab.graphics.shapes import _renderPath
pdfPath = self._canvas.beginPath()
drawFuncs = (pdfPath.moveTo, pdfPath.lineTo, pdfPath.curveTo, pdfPath.close)
autoclose = getattr(path,'autoclose','')
fill = self._fill
stroke = self._stroke
isClosed = _renderPath(path, drawFuncs, forceClose=fill and autoclose=='pdf')
dP = self._canvas.drawPath
cP = self._canvas.clipPath if path.isClipPath else dP
fillMode = getattr(path,'fillMode',None)
if autoclose=='svg':
if fill and stroke and not isClosed:
cP(pdfPath, fill=fill, stroke=0)
dP(pdfPath, stroke=stroke, fill=0, fillMode=fillMode)
else:
cP(pdfPath, fill=fill, stroke=stroke, fillMode=fillMode)
elif autoclose=='pdf':
cP(pdfPath, fill=fill, stroke=stroke, fillMode=fillMode)
else:
#our old broken default
if not isClosed:
fill = 0
cP(pdfPath, fill=fill, stroke=stroke, fillMode=fillMode)
def setStrokeColor(self,c):
self._canvas.setStrokeColor(c)
def setFillColor(self,c):
self._canvas.setFillColor(c)
def applyStateChanges(self, delta, newState):
"""This takes a set of states, and outputs the PDF operators
needed to set those properties"""
for key, value in (sorted(delta.items()) if rl_config.invariant else delta.items()):
if key == 'transform':
self._canvas.transform(value[0], value[1], value[2],
value[3], value[4], value[5])
elif key == 'strokeColor':
#this has different semantics in PDF to SVG;
#we always have a color, and either do or do
#not apply it; in SVG one can have a 'None' color
if value is None:
self._stroke = 0
else:
self._stroke = 1
self.setStrokeColor(value)
elif key == 'strokeWidth':
self._canvas.setLineWidth(value)
elif key == 'strokeLineCap': #0,1,2
self._canvas.setLineCap(value)
elif key == 'strokeLineJoin':
self._canvas.setLineJoin(value)
# elif key == 'stroke_dasharray':
# self._canvas.setDash(array=value)
elif key == 'strokeDashArray':
if value:
if isinstance(value,(list,tuple)) and len(value)==2 and isinstance(value[1],(tuple,list)):
phase = value[0]
value = value[1]
else:
phase = 0
self._canvas.setDash(value,phase)
else:
self._canvas.setDash()
elif key == 'fillColor':
#this has different semantics in PDF to SVG;
#we always have a color, and either do or do
#not apply it; in SVG one can have a 'None' color
if value is None:
self._fill = 0
else:
self._fill = 1
self.setFillColor(value)
elif key in ['fontSize', 'fontName']:
# both need setting together in PDF
# one or both might be in the deltas,
# so need to get whichever is missing
fontname = delta.get('fontName', self._canvas._fontname)
fontsize = delta.get('fontSize', self._canvas._fontsize)
self._canvas.setFont(fontname, fontsize)
elif key=='fillOpacity':
if value is not None:
self._canvas.setFillAlpha(value)
elif key=='strokeOpacity':
if value is not None:
self._canvas.setStrokeAlpha(value)
elif key=='fillOverprint':
self._canvas.setFillOverprint(value)
elif key=='strokeOverprint':
self._canvas.setStrokeOverprint(value)
elif key=='overprintMask':
self._canvas.setOverprintMask(value)
elif key=='fillMode':
self._canvas._fillMode = value
from reportlab.platypus import Flowable
class GraphicsFlowable(Flowable):
"""Flowable wrapper around a Pingo drawing"""
def __init__(self, drawing):
self.drawing = drawing
self.width = self.drawing.width
self.height = self.drawing.height
def draw(self):
draw(self.drawing, self.canv, 0, 0)
def drawToFile(d, fn, msg="", showBoundary=rl_config._unset_, autoSize=1, canvasKwds={}):
"""Makes a one-page PDF with just the drawing.
If autoSize=1, the PDF will be the same size as
the drawing; if 0, it will place the drawing on
an A4 page with a title above it - possibly overflowing
if too big."""
d = renderScaledDrawing(d)
for x in ('Name','Size'):
a = 'initialFont'+x
canvasKwds[a] = getattr(d,a,canvasKwds.pop(a,STATE_DEFAULTS['font'+x]))
c = Canvas(fn,**canvasKwds)
if msg:
c.setFont(rl_config.defaultGraphicsFontName, 36)
c.drawString(80, 750, msg)
c.setTitle(msg)
if autoSize:
c.setPageSize((d.width, d.height))
draw(d, c, 0, 0, showBoundary=showBoundary)
else:
#show with a title
c.setFont(rl_config.defaultGraphicsFontName, 12)
y = 740
i = 1
y = y - d.height
draw(d, c, 80, y, showBoundary=showBoundary)
c.showPage()
c.save()
if sys.platform=='mac' and not hasattr(fn, "write"):
try:
import macfs, macostools
macfs.FSSpec(fn).SetCreatorType("CARO", "PDF ")
macostools.touched(fn)
except:
pass
def drawToString(d, msg="", showBoundary=rl_config._unset_,autoSize=1,canvasKwds={}):
"Returns a PDF as a string in memory, without touching the disk"
s = getBytesIO()
drawToFile(d, s, msg=msg, showBoundary=showBoundary,autoSize=autoSize, canvasKwds=canvasKwds)
return s.getvalue()
#########################################################
#
# test code. First, define a bunch of drawings.
# Routine to draw them comes at the end.
#
#########################################################
def test(outDir='pdfout',shout=False):
from reportlab.graphics.shapes import _baseGFontName, _baseGFontNameBI
from reportlab.rl_config import verbose
import os
if not os.path.isdir(outDir):
os.mkdir(outDir)
fn = os.path.join(outDir,'renderPDF.pdf')
c = Canvas(fn)
c.setFont(_baseGFontName, 36)
c.drawString(80, 750, 'Graphics Test')
# print all drawings and their doc strings from the test
# file
#grab all drawings from the test module
from reportlab.graphics import testshapes
drawings = []
for funcname in dir(testshapes):
if funcname[0:10] == 'getDrawing':
func = getattr(testshapes,funcname)
drawing = func() #execute it
docstring = getattr(func,'__doc__','')
drawings.append((drawing, docstring))
#print in a loop, with their doc strings
c.setFont(_baseGFontName, 12)
y = 740
i = 1
for (drawing, docstring) in drawings:
assert (docstring is not None), "Drawing %d has no docstring!" % i
if y < 300: #allows 5-6 lines of text
c.showPage()
y = 740
# draw a title
y = y - 30
c.setFont(_baseGFontNameBI,12)
c.drawString(80, y, 'Drawing %d' % i)
c.setFont(_baseGFontName,12)
y = y - 14
textObj = c.beginText(80, y)
textObj.textLines(docstring)
c.drawText(textObj)
y = textObj.getY()
y = y - drawing.height
draw(drawing, c, 80, y)
i = i + 1
if y!=740: c.showPage()
c.save()
if shout or verbose>2:
print('saved %s' % ascii(fn))
if __name__=='__main__':
test(shout=True)
import sys
if len(sys.argv)>1:
outdir = sys.argv[1]
else:
outdir = 'pdfout'
test(outdir,shout=True)
#testFlowable()