Posts mit dem Label PDF werden angezeigt. Alle Posts anzeigen
Posts mit dem Label PDF werden angezeigt. Alle Posts anzeigen

Sonntag, 26. Juli 2015

Python: Tabellen in ReportLab

ReportLab ist die wohl leistungsfähigste Bibliothek für Python, wenn es um das Erzeugen von PDF-Dateien geht. Von High-Level Zugriffen via Platypus (=Page Layout and Typography Using Scripts) bis zu Low-Level mit direktem Zugriff auf den Canvas bietet ReportLab alles.

Tabellen unterstützt Platypus mit Hilfe der Table-Klasse (welche ein Flowable darstellt) direkt und die Verwendung ist auch erst Mal nicht weiter schwierig.

Es soll ein einfaches PDF-Dokument mit Hilfe von ReportLab gebaut werden. Das Dokument besteht aus einem Absatz Text, einer Tabelle und einem Absatz Text nach der Tabelle.

Der Code dafür sieht im einfachsten Fall so aus:

from reportlab.platypus import Paragraph, Table, SimpleDocTemplate
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import A4


my_data = [['foo', 'bar', 'spam'],
           ['egg', 123, 'python'],
           ['a bit longer', 666, '9B']]
          
styles = getSampleStyleSheet()
doc = SimpleDocTemplate('table_test.pdf', pagesize=A4)
story = []
story.append(Paragraph('Text vor Tabelle', styles['Normal']))
story.append(Table(my_data))
story.append(Paragraph('Text nach Tabelle', styles['Normal']))
doc.build(story)


my_data enthält die Daten für die Tabelle, der Rest ist weitestgehend selbsterklärend.

Nur das das Dokument nicht gerade "hübsch". Das Ergebnis ist im folgenden Screenshot zu sehen:

Standardmäßig werden für die Tabelle keine Rahmen gezeichnet und die Tabelle ist horizontal auf der Seite zentriert. Außerdem ist der Text sehr nah an der Tabelle.

Das ist im folgenden Code korrigiert:

from reportlab.platypus import Paragraph, Table, TableStyle, SimpleDocTemplate
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors

my_data = [['foo', 'bar', 'spam'],
           ['egg', 123, 'python'],
           ['a bit longer', 666, '9B']]
          
styles = getSampleStyleSheet()
doc = SimpleDocTemplate('table_test.pdf', pagesize=A4)
story = []
story.append(Paragraph('Text vor Tabelle', styles['Normal']))
t = Table(my_data)
t.hAlign = 'LEFT'
t.spaceBefore =  10
t.spaceAfter = 10
t.setStyle(TableStyle(

    [('BOX', (0,0), (-1,-1), 0.5, colors.black),
     ('INNERGRID', (0,0), (-1,-1), 0.5, colors.black)]))
story.append(t)
story.append(Paragraph('Text nach Tabelle', styles['Normal']))
doc.build(story)


Das ganze liefert als Ergebnis folgendes:
Über das Attribute hAlign wird die Tabelle die Ausrichtung der Tabelle bestimmt, der Wert LEFT richtet sie linksbündig aus. Mit den Attributen spaceBefore und SpaceAfter wird zusätzlicher Leerraum (hier: 10 px) eingefügt.

Die Tabelle an sich wird über die setStyle-Methode formatiert, welche als Wert die TableStyle-Klasse übergeben bekommt. Diese erlaubt einen detaillierten Eingriff in die Formatierung der Tabelle.

Die Werte für TableStyle werden als Liste von Tupeln übergeben. Im obigen Beispiel wird mit BOX ein Rahmen um die Tabelle gezeichnet und INNERGRID zeichnet den inneren Rahmen, beide in schwarz.
Die Tupel (0, 0), (-1, -1) definieren den Geltungsbereich der Formatierung. (0,0) ist dabei die Zelle links oben, (-1, -1) die Zelle rechts unten - somit ist also die ganze Tabelle abgedeckt.

Möchte man z.B. das innere Gitter mur für einen Teil der Zellen zeichnen und in der mittleren Zelle soll die Schrift rot sein, so ist die Codezeile wie folgt anzupassen:

t.setStyle(TableStyle(
    [('BOX', (0,0), (-1,-1), 0.5, colors.black
     ('INNERGRID', (0,0), (1,1), 0.5, colors.black),
     ('TEXTCOLOR', (1,1), (1,1), colors.red)]))

Und das Ergebnis sieht dann so aus:


TableStyle bietet eine Vielzahl weiterer Möglichkeiten, dass Aussehen der Tabelle zu beeinflussen. Eine komplette Übersicht ist im Kapitel 7 der ReportLab Dokumentation zu finden.

So ist es z.B. auch möglich, Zellen innerhalb einer Spalte oder Zeile zusammenzufassen. Möchte man z.B. die drei Zellen der ersten Zeile zusammenfassen, so ist der obiger Code wie folgt zu ändern:

...
my_data = [['Überschrift', '', ''],
           ['egg', 123, 'python'],
           ['a bit longer', 666, '9B']]

....
t.setStyle(TableStyle(
    [('BOX', (0,0), (-1,-1), 0.5, colors.black),
     ('INNERGRID', (0,0), (-1,-1), 0.5, colors.black),
     ('SPAN', (0, 0), (-1, 0))]))


Das sieht im Ergebnis so aus:

Die bisher generierten Tabellen ermitteln die Breite der Spalten und Höhe der Zeilen automatisch, aber auch das kann natürlich beeinflusst werden. Möchte man z.B. eine fixe Spaltenbreite  von 100 px haben, so ist im Code die Zeile wie folgt zu ändern:

t = Table(my_data, colWidths=100)

Weitere Möglichkeit sind ebenfalls im Kapitel 7 der Dokumentation zu finden.

In den obigen Beispielen wird der Table-Klasse einfach eine Liste von Strings bzw. Integern übergeben. Man kann aber auch eine Liste von Flowables übergeben, was sich anbieten, wenn man z.B. längeren Text oder Grafiken in der Tabelle unterbringen möchte.

Des Weiteren gibt es noch die Klasse LongTable, welche anstellen von Table verwendet werden kann. Das Ergebnis ist laut Dokumentation das gleiche, allerdings soll LongTable bei sehr langen Tabelle etwas performanter sein.

Wie hier gezeigt wird, ist das Generieren einer Tabelle in einem mit Platypus gebauten PDF-Dokuments in ReportLab nicht weiter schwierig. Wie bei ReportLab üblich kann man auf Wunsch aber auch sehr detailliert in die Gestaltung und das Layout der Tabelle eingreifen.

Alle Beispiele aus diesem Blogbeitrag sind unter Python 3.4 und ReportLab 3.0 erstellt.

Samstag, 18. Oktober 2014

Python: ListFlowables in ReportLab

ReportLab ist die wohl leistungsfähigste Bibliothek für Python, wenn es um das Erzeugen von PDF-Dateien geht. Von High-Level Zugriffen via Platypus (=Page Layout and Typography Using Scripts) bis zu Low-Level mit direktem Zugriff auf den Canvas bietet ReportLab alles.

Allerdings hat es doch bis zu ReportLab Version 2.6 (erschienen im Oktober 2012) gedauert, bis das direkte Erzeugen von nummerierten und unnummerierten Listen möglich war. Dies war zwar vorher prinzipiell auch möglich, aber nur über Umwege (z.B. bei unnummerierten Listen durch das manuelle hinzufügen eines Bullet Points vor dem Text).

Mit dem seit ReportLab 2.6 vorhandenen Flowable namens "ListFlowable" geht das aber wesentlich einfacher.

Neulich, bei der Überarbeitung eines meiner älteren Python-Skripte, welches auch ReportLab einsetzt, habe ich auch auf diese Flowable umgestellt.

Da die Doku von ReportLab zu diesem Thema - leider - etwas dünn ist, habe ich hier in diesem Blogebeitrag mal ein paar Sachen dazu aufgeschrieben.

Grundsätzlich ist die Sache recht simpel. Nach dem Import der benötigten Module erstellt man ein ListFlowable, welches die Punkte der Liste als ListItems enthält. Der Quelltext für eine Liste mit drei Punkten sieht so aus:

from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import A4
from reportlab.platypus import Paragraph, SimpleDocTemplate, ListFlowable, \
                               ListItem

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

t1 = ListFlowable([ListItem(Paragraph('foo', style)),
                   ListItem(Paragraph('bar', style)),
                   ListItem(Paragraph('spamegg', style))])
story.append(t1)
doc = SimpleDocTemplate('testdoc.pdf', size=A4)
doc.build(story)

So wird eine nummerierte Liste erzeugt, der Standard des ListFlowables, wenn keine weiteren Vorgaben gemacht werden.

Möchte man eine unnummerierte Liste erzeugen, muss dem ListFlowable das Argument bulletType und start mitgegeben werden:

t1 = ListFlowable([ListItem(Paragraph('foo', style)),
                   ListItem(Paragraph('bar', style)),
                   ListItem(Paragraph('spamegg', style))],
                   bulletType='bullet',
                   start='circle')

Möchte man statt Kreisen als Bullet Points z.B. Quadrate haben, dann ersetzt man einfach start='circle' durch start='square'.

Wer aus den obigen Beispielen ein PDF generiert wird feststellen, dass sowohl die Zahl als auch die Bullet Points ziemlich groß sind, größer als der Text, formatiert mit dem Beispiel-Stylesheet aus ReportLab. Fügt man noch das Argument bulletFontSize hinzu, gefolgt von der gewünschten Schriftgröße (also z.B. bulletFontSize=8), dann werden Schrift bzw. Bullet Points in der angegebenen Schriftgröße dargestellt.
Alternativ kann man für unnummerierten Listen auch folgendes angeben: start='bulletchar'. Dann wird als Zeichen standardmäßig ein kleiner Punkt (HTML: ·) verwendet.

Bei nummerierten Listen kann aber ReportLab aber nicht nur mit "normalen" Zahlen zählen, sondern auch:
bulletType='i' # römische Zahlen
bulletType='a' # Zählung: a, b , c, ...
bulletType='A' # Zählung: A, B, C, ...

Mit dem Argument start kann  man den Beginn der Zählung bei nummerierten Listen den Beginn der Zahlung festlegen. bulletType='A', start='10' würde z..B. die Zählung der Liste bei J starten lassen.

Weiterhin werden standardmäßig alle Listen linksbündig ausgerichtet. Möchte man die Listen gegenüber dem Text etwas einrücken, dann muss man die für die Aufzählungszeichen und den zugehörigen Text separat machen, wie z.B.:

p1 = Paragraph('etwas Text', style)
t1 = ListFlowable([ListItem(Paragraph('foo', style), leftIndent=25),
                   ListItem(Paragraph('bar', style), leftIndent=25),
                   ListItem(Paragraph('spamegg', style),leftIndent=25)],
                   bulletType='bullet',
                   start='bulletchar',
                   leftIdent=10)
story.append(p1)
story.append(t1)
story.append(p1)

Und das ListFlowable kann natürlich auch verschachtelt werden, für verschachtelte Liste. Eine Liste mit zwei Ebenen sieht z.B. so aus:

t1 = ListFlowable([ListItem(Paragraph('foo', style)),
                   ListItem(Paragraph('bar', style)),
                   ListFlowable([ListItem(Paragraph('spam', style)),
                                 ListItem(Paragraph('egg', style))],
                                 bulletType='a'),
                   ListItem(Paragraph('more', style))])

Ein paar weiter Tipps zum ListFlowable und dessen Formatierungsmöglichkeiten gibt es auch in einem Thread bei stackoverflow.