Draw images with canvas and use SimpleDocTemplate

slothy picture slothy · Mar 18, 2011 · Viewed 13.5k times · Source

I'm writing pdfs with reportlab inside a django view, they are very simple, the header, the contents and the footer.

I'm using SimpleDocTemplate wich fits very well, to draw tables in the contents, the footer and the header are drwan using:

build([data], onFirstPage=drawPageFrame, onLaterPages=drawPageFrame).

My question is, How can I draw a image like using Canvas.drawImage(...)? I need a "floating" image... positioned over the text where I want, and with SimpleDocTemplate I don't have a Canvas object to do this.

Searching I have found this:

The platypus layout stuff uses flowables. Packers normally set the attribute canv onto each flowable when it is being wrapped, split or drawn ie around the wrap, split and draw methods. Inside those methods you have access to the canvas using the canv attribute of self.

How can this be used?

Ummmm, more stuff to test:

flowables.Macro
flowables.CallerMacro
# -*- coding: utf-8 -*-
from reportlab.lib.pagesizes import A4, landscape, portrait
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Table, Flowable, SimpleDocTemplate, Paragraph, Spacer, Image
from reportlab.lib import randomtext
from reportlab import platypus

import os,random

styles = getSampleStyleSheet()
path = os.path.realpath(os.path.dirname(__file__))

def drawPageFrame(canvas, doc):
    canvas.saveState()
    canvas.drawImage(path+"/ujiPDF.jpg",50,50,57,57)
    canvas.restoreState()

doc = SimpleDocTemplate("salida.pdf",pagesize=A4)

elementos = []

com = 'canvas.drawImage("'+path+'/ujiPDF.jpg",100,100,57,57)'
print com
elementos.append(platypus.flowables.Macro('canvas.saveState()'))
print platypus.flowables.Macro(com)
elementos.append(platypus.flowables.Macro(com))
elementos.append(platypus.flowables.Macro('canvas.restoreState()'))

para = Paragraph(randomtext.randomText(randomtext.PYTHON,20), styles["Normal"])
elementos.append(para)

doc.build(elementos,onFirstPage=drawPageFrame, onLaterPages=drawPageFrame)

This is the Macro approach...clean exit but without the second image.

Answer

Ezekiel Kruglick picture Ezekiel Kruglick · Feb 8, 2015

You don't need to subclass the whole document template if I understand you right, you just want a flowable that you can put INTO a SimpleDocTemplate. You can achieve that with a very easy subclass of Flowable itself.

-> In particular since people often ask about how to put matplotlib objects into reportlab, I'll show how to generate a plot with matplotlib and then use the modified flowable to put that plot into a SimpleDocTemplate (without saving the file to disk). The concept applies to any file or anything you can feed into a cStringIO

-> The below DOES allow you to put the figure over the text (change the height from negative to positive to push it above the imaginary line at the top of the section the plot goes into)

The key concept is that each Flowable does, itself, also contain a canvas we can draw onto.

import matplotlib.pyplot as plt
import cStringIO
from reportlab.lib.units import inch, cm
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
import reportlab.lib, reportlab.platypus

class flowable_fig(reportlab.platypus.Flowable):
    def __init__(self, imgdata):
        reportlab.platypus.Flowable.__init__(self)
        self.img = reportlab.lib.utils.ImageReader(imgdata)

    def draw(self):
        self.canv.drawImage(self.img, 0, 0, height = -2*inch, width=4*inch)
        # http://www.reportlab.com/apis/reportlab/2.4/pdfgen.html
                            
doc = SimpleDocTemplate(("report.pdf"),pagesize=letter,
                    rightMargin=72,leftMargin=72,
                    topMargin=72,bottomMargin=18)
Story=[]
styles=getSampleStyleSheet()
ptext = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi massa dolor, vulputate quis elit sed, sagittis consectetur erat. Sed lobortis nisi eros, eu maximus enim iaculis ac. Vestibulum sagittis urna nec interdum aliquam. Pellentesque ornare velit ut ante ullamcorper, vulputate accumsan nisi vulputate. Fusce consectetur dolor quam. Phasellus hendrerit, ligula vel consectetur pretium, lorem neque dapibus eros, ornare suscipit ipsum dolor id nisl. Sed vel orci id leo efficitur lobortis sit amet id risus. Nullam euismod, ipsum a posuere scelerisque, ante lorem ultrices nibh, ut feugiat metus ex congue enim. Nam lobortis, metus id pellentesque feugiat, arcu orci rutrum felis, quis luctus urna nisl at nulla. Donec eu eros pharetra dolor congue facilisis at ac magna. Nullam eu ultricies metus. Sed sodales, libero viverra pellentesque tempus, magna purus convallis nibh, eu condimentum tortor erat tincidunt turpis. Vestibulum scelerisque tincidunt egestas. Nullam commodo diam nisl, sed consequat ex sagittis eu.'
Story.append(Paragraph(ptext, styles["Normal"]))

fig = plt.figure(figsize=(10, 3))
plt.plot([1,2,3,4])
plt.ylabel('This is a boring plot')
imgdata = cStringIO.StringIO()
fig.savefig(imgdata, format='png')
imgdata.seek(0)  # rewind the data

pic = flowable_fig(imgdata)
Story.append(pic)
doc.build(Story)

You can expand this minimum code snipped to accomplish whatever you like and you can add as many pieces to the Story variable (which is just a list of flowables really). The magic is just that we're handing the doc builder a Flowable that uses the Flowable's own canvas to draw the figure.

EDIT: I almost forgot, because this gives us full access to drawImage, we can also make that picture or plot have transparency so other things can be seen through it. The docs on drawImage have the details.