Есть ли matplotlib, пригодный для использования в ReportLab?

Я хочу встроить диаграммы matplotlib в PDF-файлы, сгенерированные непосредственно ReportLab - т.е. не сохранять сначала как PNG, а затем встраивать PNG в PDF (думаю, я получу более качественный вывод).

Кто-нибудь знает, есть ли Matplotlib текучий для ReportLab?

Спасибо

6 ответов

Вот решение с использованием pdfrw:

#!/usr/bin/env python
# encoding: utf-8
"""matplotlib_example.py
   An simple example of how to insert matplotlib generated figures
   into a ReportLab platypus document.
"""

import matplotlib
matplotlib.use('PDF')
import matplotlib.pyplot as plt
import cStringIO

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

from reportlab.platypus import Flowable
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.rl_config import defaultPageSize
from reportlab.lib.units import inch

PAGE_HEIGHT=defaultPageSize[1]; PAGE_WIDTH=defaultPageSize[0]
styles = getSampleStyleSheet()

class PdfImage(Flowable):
    """PdfImage wraps the first page from a PDF file as a Flowable
which can be included into a ReportLab Platypus document.
Based on the vectorpdf extension in rst2pdf (http://code.google.com/p/rst2pdf/)"""

    def __init__(self, filename_or_object, width=None, height=None, kind='direct'):
        from reportlab.lib.units import inch
        # If using StringIO buffer, set pointer to begining
        if hasattr(filename_or_object, 'read'):
            filename_or_object.seek(0)
        page = PdfReader(filename_or_object, decompress=False).pages[0]
        self.xobj = pagexobj(page)
        self.imageWidth = width
        self.imageHeight = height
        x1, y1, x2, y2 = self.xobj.BBox

        self._w, self._h = x2 - x1, y2 - y1
        if not self.imageWidth:
            self.imageWidth = self._w
        if not self.imageHeight:
            self.imageHeight = self._h
        self.__ratio = float(self.imageWidth)/self.imageHeight
        if kind in ['direct','absolute'] or width==None or height==None:
            self.drawWidth = width or self.imageWidth
            self.drawHeight = height or self.imageHeight
        elif kind in ['bound','proportional']:
            factor = min(float(width)/self._w,float(height)/self._h)
            self.drawWidth = self._w*factor
            self.drawHeight = self._h*factor

    def wrap(self, aW, aH):
        return self.drawWidth, self.drawHeight

    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))

        xobj = self.xobj
        xobj_name = makerl(canv._doc, xobj)

        xscale = self.drawWidth/self._w
        yscale = self.drawHeight/self._h

        x -= xobj.BBox[0] * xscale
        y -= xobj.BBox[1] * yscale

        canv.saveState()
        canv.translate(x, y)
        canv.scale(xscale, yscale)
        canv.doForm(xobj_name)
        canv.restoreState()

Title = "Hello world"
pageinfo = "platypus example"
def myFirstPage(canvas, doc):
    canvas.saveState()
    canvas.setFont('Times-Bold',16)
    canvas.drawCentredString(PAGE_WIDTH/2.0, PAGE_HEIGHT-108, Title)
    canvas.setFont('Times-Roman',9)
    canvas.drawString(inch, 0.75 * inch, "First Page / %s" % pageinfo)
    canvas.restoreState()


def myLaterPages(canvas, doc):
    canvas.saveState()
    canvas.setFont('Times-Roman',9)
    canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, pageinfo))
    canvas.restoreState()

def go():
    fig = plt.figure(figsize=(4, 3))
    plt.plot([1,2,3,4])
    plt.ylabel('some numbers')
    imgdata = cStringIO.StringIO()
    fig.savefig(imgdata,format='PDF')
    doc = SimpleDocTemplate("document.pdf")
    Story = [Spacer(1,2*inch)]
    style = styles["Normal"]
    for i in range(5):
        bogustext = ("This is Paragraph number %s.  " % i) *20
        p = Paragraph(bogustext, style)
        Story.append(p)
        Story.append(Spacer(1,0.2*inch))
        pi = PdfImage(imgdata)
        Story.append(pi)
        Story.append(Spacer(1,0.2*inch))
    doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages)


if __name__ == '__main__':
    go()

Patrick Maupin Мопин, автор pdfrw, дал более простой и менее сложный ответ на другой вопрос. (Я благодарю его за добрые слова о моем предыдущем ответе.) Он также упомянул, что сохранение рисунков matplotlib в многостраничный PDF перед использованием pdfrw для их извлечения уменьшит размер итогового PDF файла отчета за счет сокращения дублирующихся ресурсов. Итак, вот модификация его примера кода, который демонстрирует, как размер файла PDF уменьшается путем записи сначала в многостраничный файл matplotlib PDF. В этом примере размер файла уменьшается примерно на 80%.

Примечание: это специально для использования с рисунками matplotlib.

import os
from matplotlib import pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Flowable
from reportlab.lib.units import inch
from reportlab.lib.styles import getSampleStyleSheet

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']


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(outfn, nfig=5):
    """
    Makes a dummy report with nfig matplotlib plots.
    """

    doc = SimpleDocTemplate(outfn)
    style = styles["Normal"]
    story = [Spacer(0, inch)]

    for j in range(nfig):

        fig = plt.figure(figsize=(4, 3))
        plt.plot([1, 2, 3, 4], [1, 4, 9, 26])
        plt.ylabel('some numbers')
        plt.title('My Figure %i' % (j+1))
        img = PdfImage(fig, width=400, height=400)
        plt.close()

        for i in range(10):
            bogustext = ("Paragraph number %s. " % i)
            p = Paragraph(bogustext, style)
            story.append(p)
            story.append(Spacer(1, 0.2*inch))

        story.append(img)

        for i in range(10):
            bogustext = ("Paragraph number %s. " % i)
            p = Paragraph(bogustext, style)
            story.append(p)
            story.append(Spacer(1, 0.2*inch))

    doc.build(story)


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

    doc = SimpleDocTemplate(outfn)
    style = styles["Normal"]
    story = [Spacer(0, inch)]

    with PdfImageCache() as pdfcache:
        for j in range(nfig):

            fig = plt.figure(figsize=(4, 3))
            plt.plot([1, 2, 3, 4], [1, 4, 9, 26])
            plt.ylabel('some numbers')
            plt.title('My Figure %i' % (j+1))
            img = pdfcache.savefig(fig, width=400, height=400)
            plt.close()

            for i in range(10):
                bogustext = ("Paragraph number %s. " % i)
                p = Paragraph(bogustext, style)
                story.append(p)
                story.append(Spacer(1, 0.2*inch))

            story.append(img)

            for i in range(10):
                bogustext = ("Paragraph number %s. " % i)
                p = Paragraph(bogustext, style)
                story.append(p)
                story.append(Spacer(1, 0.2*inch))

    doc.build(story)


make_report("hello_pdf.pdf", 50)
make_report_cached_figs("hello_pdf_cached_figs.pdf", 50)

Поскольку PdfPages matplotlib принимает только путь к файлу в качестве входных данных, объект PdfImageCache записывает многостраничный PDF-файл во временный файл. Попытка сделать это в памяти потребовала бы намного больше работы.

Решение для Python 3 и встраивание фигуры matplotlib в векторное изображение (без растеризации)

import matplotlib.pyplot as plt
from io import BytesIO
from reportlab.pdfgen import canvas
from reportlab.graphics import renderPDF
from svglib.svglib import svg2rlg

fig = plt.figure(figsize=(4, 3))
plt.plot([1,2,3,4])
plt.ylabel('some numbers')

imgdata = BytesIO()
fig.savefig(imgdata, format='svg')
imgdata.seek(0)  # rewind the data

drawing=svg2rlg(imgdata)

c = canvas.Canvas('test2.pdf')
renderPDF.draw(drawing,c, 10, 40)
c.drawString(10, 300, "So nice it works")
c.showPage()
c.save()

svglib доступно от Конда-Фордж.

Это не так, но в моем собственном использовании MatPlotLib с ReportLab я создаю PNG, а затем внедряю PNG, поэтому мне не нужно также использовать PIL. Однако, если вы используете PIL, я считаю, что вы сможете создавать и встраивать EPS, используя MatPlotLib и ReportLab.

Бесплатная библиотека PDFRW подходит для размещения PDF-файлов в reportlab. На самом деле в гостевой книге PDFRW есть заметка от того, кто решил именно эту проблему с помощью Matplotlib PDFs. Возможно это был ты?

Я создал пример Flowable для использования с коммерческим языком шаблонов RML ReportLab. Приведенные здесь примеры чрезвычайно полезны, но для работы в качестве элемента RML plugInFlow требуется небольшая доработка.

Рабочий пример доступен на GitHub.

Вот сам Flowable:

      class SvgFlowable(Flowable):
    """Convert byte stream containing SVG into a Reportlab Flowable."""

    def __init__(self, svg: BytesIO) -> None:
        """Convert SVG to RML drawing on initializtion."""
        svg.seek(0)
        self.drawing: Drawing = svg2rlg(svg)
        self.width: int = self.drawing.minWidth()
        self.height: int = self.drawing.height
        self.drawing.setProperties({"vAlign": "CENTER", "hAlign": "CENTER"})

    def wrap(self, *_args):
        """Return diagram size."""
        return (self.width, self.height)

    def draw(self) -> None:
        """Render the chart."""
        renderPDF.draw(self.drawing, self.canv, 0, 0)

Другие вопросы по тегам