How to set the position of an image in reportlab using pdfrw?
Asked Answered
R

0

2

I'm using reportlab to generate reports. I can define the creation process in four steps: 1) get the data via API, 2) filter the data, 3) generate the graphics with matplotlib and 4) insert information in PDF with reportlab.

I found in this (thanks Patrick Maupin!) and in this (thanks Larry Meyn!) answer a flowable matplotlib for ReportLab. I made some changes and I copy below the part of the code that interests:

import os
from matplotlib import pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from reportlab.platypus import Paragraph, SimpleDocTemplate, Flowable
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfgen import canvas

from pdfrw import PdfReader, PdfDict
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


try:
    from cStringIO import StringIO as BytesIO
except ImportError:
    from io import BytesIO

styles = getSampleStyleSheet()
style = styles['Normal']

#Info if you want to run the script
div = ['may/18', 'jun/18', 'jul/18', 'aug/18', 'sep/18', 'oct/18', 'nov/18', \
 'dec/18', 'jan/19', 'feb/19', 'mar/19', 'apr/19']

mes = [0, 15, 149, 0, 59, 0, 0, 101, 45, 25, 751.07, 5358.3]
acc = [0, 15, 164, 164, 223, 223, 223, 324, 369, 394, 1145.07, 6503.37]

class PdfImage(Flowable):
    """
    Generates a reportlab image flowable for matplotlib figures. It is initialized
    with either a matplotlib figure or a pointer to a list of pagexobj objects and
    an index for the pagexobj to be used.
    """
    def __init__(self, fig=None, width=200, height=200, cache=None, cacheindex=0):
        self.img_width = width
        self.img_height = height
        if fig is None and cache is None:
            raise ValueError("Either 'fig' or 'cache' must be provided")
        if fig is not None:
            imgdata = BytesIO()
            fig.savefig(imgdata, format='pdf')
            imgdata.seek(0)
            page, = PdfReader(imgdata).pages
            image = pagexobj(page)
            self.img_data = image
        else:
            self.img_data = None
        self.cache = cache
        self.cacheindex = cacheindex

    def wrap(self, width, height):
        return self.img_width, self.img_height

    def drawOn(self, canv, x, y, _sW=0):
        if _sW > 0 and hasattr(self, 'hAlign'):
            a = self.hAlign
            if a in ('CENTER', 'CENTRE', TA_CENTER):
                x += 0.5*_sW
            elif a in ('RIGHT', TA_RIGHT):
                x += _sW
            elif a not in ('LEFT', TA_LEFT):
                raise ValueError("Bad hAlign value " + str(a))
        canv.saveState()
        if self.img_data is not None:
            img = self.img_data
        else:
            img = self.cache[self.cacheindex]
        if isinstance(img, PdfDict):
            xscale = self.img_width / img.BBox[2]
            yscale = self.img_height / img.BBox[3]
            canv.translate(x, y)
            canv.scale(xscale, yscale)
            canv.doForm(makerl(canv, img))
        else:
            canv.drawImage(img, x, y, self.img_width, self.img_height)
        canv.restoreState()


class PdfImageCache(object):
    """
    Saves matplotlib figures to a temporary multi-page PDF file using the 'savefig'
    method. When closed the images are extracted and saved to the attribute 'cache'.
    The temporary PDF file is then deleted. The 'savefig' returns a PdfImage object
    with a pointer to the 'cache' list and an index for the figure. Use of this
    cache reduces duplicated resources in the reportlab generated PDF file.

    Use is similar to matplotlib's PdfPages object. When not used as a context
    manager, the 'close()' method must be explictly called before the reportlab
    document is built.
    """
    def __init__(self):
        self.pdftempfile = '_temporary_pdf_image_cache_.pdf'
        self.pdf = PdfPages(self.pdftempfile)
        self.cache = []
        self.count = 0

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def close(self, *args):
        self.pdf.close()
        pages = PdfReader(self.pdftempfile).pages
        pages = [pagexobj(x) for x in pages]
        self.cache.extend(pages)
        os.remove(self.pdftempfile)

    def savefig(self, fig, width=200, height=200):
        self.pdf.savefig(fig)
        index = self.count
        self.count += 1
        return PdfImage(width=width, height=height, cache=self.cache, cacheindex=index)



def make_report_cached_figs(outfn):
    """
    Makes a dummy report with nfig matplotlib plots using PdfImageCache
    to reduce PDF file size.
    """

    doc = SimpleDocTemplate(outfn)
    style = styles["Normal"]
    story = []

    with PdfImageCache() as pdfcache:

            fig = plt.figure(figsize=(5, 5))
            plt.bar(div, acc, width = 0.8, color = 'silver', label = 'Year')
            plt.bar(div, mes, width = 0.4, color = 'k', label = 'Month', alpha = 0.5)
            plt.legend()
            plt.xticks(rotation=60)
            plt.close()
            img0 = pdfcache.savefig(fig, width=185, height=135)
            img0.drawOn(doc, 0, 100)

            story.append(img0)

    doc.build(story)

make_report_cached_figs("Test.pdf")

My problem is in the drawOn method, more specifically in the first argument. In the above code I get the following error:

AttributeError: 'SimpleDocTemplate' object has no attribute 'saveState'

If I replace doc = SimpleDocTemplate(outfn) with doc = canvas.Canvas(outfn) (I know that if I'm going to use canvas I should make some other minor changes) I get another error:

img = self.cache[self.cacheindex]
IndexError: list index out of range

I have searched for information in the official documentation and in the source code but have not been successful. So, how do I set the position of the image? (using the drawOn method or not)

Ross answered 24/5, 2019 at 12:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.