Samstag, 15. August 2015

Python: PIL/Pillow - Bild anzeigen mit Image.show() ( xv vs. xli vs. command vs. imagemagick)

Möchte man sich mit Hilfe von Python ein Bild anzeigen lassen, dann geht das mit drei Zeilen Code und dem Pillow-Modul. Pillow ist ein aktiv entwickelter "friendly fork" von PIL (=Python Image Library). PIL bzw. Pillow sind der quasi-Standard unter Python, wenn es das Handling und Manipulieren von Grafik-Dateien geht.

Die folgenden drei Codezeilen zeigen das Bild "foo.png" an:

>>> from PIL import Image
>>> im = Image.open('foo.png')
>>> im.show()

So zumindest die Theorie. In der Praxis passiert aber zumindest unter Ubuntu 14.04 und auch unter Raspbian nichts. Nach dem Aufruf von im.show() wird sofort wieder der Prompt des Python-Interpreters angezeigt. Keine Fehlermeldung, aber auch kein Bild.

Ein Blick in die Doku von Pillow brachte dann ans Tageslicht, dass die show-Funktion den Bildbetrachter xv aufruft. Dieser war in den 90er Jahren des vorherigen Jahrhunderts wohl mal der de-facto Bildbetrachter auf Unix-System, ist heute jedoch (ziemlich) veraltet. Und damit auch in den meisten Distributionen wie Debian, Ubuntu etc. nicht mehr enthalten.

Um trotzdem Bilder mittels show() anzeigen zu können, gibt es zwei alternative Wege:

Einer ist, das Paket imagemagick zu installieren. Dieses ist für quasi alle Linux-Distributionen in den Paketquellen enthalten.
Danach funktioniert der Aufruf von show(), es wird display aus dem ImageMagick-Paket zur Anzeige des Bilds verwendet. Leider ist dieser Weg in der Doku von Pillow nicht dokumentiert. Ein Blick in den Quellcode von von Image zeigt aber, dass Pillow auf Linux-Systemen explizit nach display sucht und diese - sofern vorhanden - verwendet.

Eine andere Alternative ist im Raspberry Pi Forum zu finden. Hier wird also das Paket xli (xli="command line tool for viewing images in X11") installiert und ein Softlink von xv auf xli angelegt:

$ sudo apt-get install xli
$ cd /usr/local/bin
$ sudo ln -s /usr/bin/xli xv


Dies funktioniert sowohl unter Raspbian als auch unter Ubuntu (und vermutlich auch bei den meisten anderen Linux-Distros, die xli in den Paketquellen haben.

Die show-Funktion kennt das optionale Argument command=, mit dem der Bildbetrachter explizit vorgegeben werden. So steht es jedenfalls in der Dokumentation. Allerdings hat das auf keinem meiner Systeme funktioniert.

Montag, 3. August 2015

Python: Ausrichten von Paragraphen in Platypus / ReportLab

Paragraphen von Reportlabs Playtypus sind dazu da, "normalen" Text aufzunehmen und dann im PDF-Dokument darzustellen. Und wie in jeder anderen Textdarstellung auch hat der Text in diesen Paragraphen natürlich auch einen Ausrichtung. Diese ist per Voreinstellung linksbündig.

Nun gibt es aber auch genug Anwendungsfälle, wo man den Text nicht linksbündig ausrichten möchte. So bietet es sich bei längern Textpassagen an, den Text als Blocksatz zu formatieren, damit das rechte Textende nicht "flattert".

Das ist natürlich auch mit ReportLab / Platypus auch ohne Probleme möglich. Es werden alle vier gängigen Textformatierungen unterstützt: linksbündig, rechtsbündig, zentriert und Blocksatz.

Wie das geht, ist im folgenden Beispiel gezeigt. Für rechtsbündig, zentriert und Blocksatz wird einfach das bestehenden Stylesheet um neue Styles erweitert, die die entsprechenden Ausrichtung haben:

from reportlab.platypus import Paragraph, SimpleDocTemplate
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
from reportlab.lib.pagesizes import A4

text = '''Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita 
kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'''

text_short = 'Lorem ipsum dolor sit amet'

styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name='RightAlign',
                          parent=ParagraphStyle('Normal'),
                          alignment=TA_RIGHT))
styles.add(ParagraphStyle(name='CenterAlign',
                          parent=ParagraphStyle('Normal'),
                          alignment=TA_CENTER))
styles.add(ParagraphStyle(name='JustifyAlign',
                          parent=ParagraphStyle('Normal'),
                          alignment=TA_JUSTIFY))
doc = SimpleDocTemplate('align_test.pdf', pagesize=A4)
story = []
story.append(Paragraph(text_short, styles['Normal']))
story.append(Paragraph(text_short, styles['RightAlign']))
story.append(Paragraph(text_short, styles['CenterAlign']))
story.append(Paragraph(text, styles['JustifyAlign']))
doc.build(story)

Führt man den obigen Code aus, dann enthält dieser vier Paragraphen: der erste ist linksbündig ausgerichtet, der zweite rechtsbündig, der dritte zentriert und der vierte als Blocksatz.

Intern verwendet ReportLab für die Ausrichtung Integer-Werte, die importierten Konstanten TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY stellen diese einfach nur dar, wie man leicht im interaktiven Python-Interpreter sehen kann:

>>> from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
>>> TA_CENTER
1
>>> TA_LEFT
0
>>> TA_RIGHT
2
>>> TA_JUSTIFY
4

>>>

Im obigen Codebeispiel könnte man also statt

styles.add(ParagraphStyle(name='JustifyAlign',
                          parent=ParagraphStyle('Normal'),
                          alignment=TA_JUSTIFY))
auch schreiben:

styles.add(ParagraphStyle(name='JustifyAlign',
                          parent=ParagraphStyle('Normal'),
                          alignment=4))

Das Ergebnis ist das gleich und man würde den Import der TA_* Konstanten "sparen".

Samstag, 1. August 2015

WTForms: Daten für SelectField nachträglich einfügen

WTForms gehört in der Python-Welt zu den populäreren Modulen zum Generieren und Validieren von HTML-Formularen für Webanwendungen.

Hier im Blogeintrag geht es aber nicht um WTForms im gesamten - die offizielle Doku ist ja so wie so recht ausführlich - sondern "nur" um das nachträgliche Hinzufügen von Auswahlmöglichkeiten für ein SelectField (=Auswahlfeld). Wobei das im folgenden gezeigt auch für das RadioField gilt.

Vorab noch: Alle folgenden Beispiele sind mit WTForms 2.0.2 unter Python 3.4 getestet. Der Einfachheit wird alles im interaktiven Python-Interpreter auf der Kommandozeile durchgespielt. Das Vorgehen in realen Webanwendungen ist aber identisch.

Eine einfaches Formular mit einem SelectField sieht z.B. so aus:

>>> from wtforms import Form, StringField, SelectField
>>> class TestForm(Form):
...     name = StringField('name')
...     gender = SelectField('gender',

            choices=[('m', 'male'),
                     ('f', 'female')])
>>>

Zuerst werden die benötigten Klassen importiert, dann wird eine eigene Formularklasse Namens TestForm definiert, welche die zwei Felder name und gender hat. gender ist dabei ein  Auswahlfeld.

Die Auswahlmöglichkeiten werden über das choices-Attribute festgelegt. Diesem wird eine Liste von Tupeln zuwiesen, wobei das erste Element der value der Option ist und das zweite Element der angezeigte Text.

Erzeugt man nun eine neue Instanz der Klasse

>>> my_form = TestForm()

kann man sich das HTML-Markup, welche das Formular erzeugt, auch direkt auf der Kommandozeile anzeigen lassen, z.B. für gender:

>>> str(my_form.gender)
'<select id="gender" name="gender"><option value="m">male</option><option value="f">female</option></select>'

>>>

Jetzt gibt es aber natürlich auch genug Anwendungsfälle, wo man in einer Webanwendung ein Auswahlfeld dynamisch befüllen will und somit die Werte bei der Definition der Klasse noch nicht kennt.

Auch das ist natürlich mit WTForms problemlos möglich und zwar indem man das choices-Attribut erst später mit Daten füttert. Dies wird im folgenden Beispiel gezeigt:

>>> class TestForm2(Form):
...     name = StringField('name')
...     fav_color = SelectField('favourite color')
...
>>> form2 = TestForm2()


Wie zu sehen ist, wird für das SelectField kein Wert für choices übergeben. Folglich kann auch kein HTML generiert werden, stattdessen wird ein Fehler geworfen:

>>> str(form2.fav_color)
Traceback (most recent call last):
...
TypeError: 'NoneType' object is not iterable
>>>


Versorgt man das choices-Attribute mit Daten, dann funktioniert alles:

>>> my_choices = [('w', 'white'),
                  ('r', 'red'),
                  ('b', 'blue')]
>>> form2.fav_color.choices = my_choices
>>> str(form2.fav_color)
'<select id="fav_color" name="fav_color"><option value="w">white</option><option value="r">red</option><option value="b">blue</option></select>'

>>>

In einer realen Anwendung würde die Liste natürlich nicht hard-coded im Quelltext stehen, sondern z.B. aus einer Datenbankabfrage kommen.

Wie andere Field-Klassen auch kennt SelectField ebenfalls einen Default-Wert, was sich im HTML-Formular dann so darstellt, dass diese Option vorausgewählt ist.

Wollte man z.B. im ersten Beispiel female als Default-Wert haben, dann müsste die Zeile im Code so geändert werden:

gender = SelectField('gender',
            choices=[('m', 'male'),
                     ('f', 'female')],
            default='f')

Will man beim 2. Beispiel  die Farbe weiß als Default-Wert setzen, dann muss man das default-Attribute ebenfalls mit einem Wert versorgen:

>>> form2.fav_color.default='w'
>>> str(form2.fav_color)
'<select id="fav_color" name="fav_color"><option value="w">white</option><option value="r">red</option><option value="b">blue</option></select>'
>>> form2.fav_color.default
'w'
>>>


Das alleine reicht aber nicht, wie im obigen Code zu sehen ist, hat sich am generierten HTML aber nichts geändert, obwohl der Default-Wert korrekt gesetzt ist.

Damit das HTML wie gewünscht ist, muss ein weiterer Schritt durchgeführt werden, und zwar der Aufruf der process-Methode der Klasse: :

>>> form2.process()
>>> str(form2.fav_color)
'<select id="fav_color" name="fav_color"><option selected value="w">white</option><option value="r">red</option><option value="b">blue</option></select>'
>>>


So ist auch der Default-Wert korrekt gesetzt.