Deși online-ul (cum îi zic ăia care se dau șmecheri în el) se întâmplă în mod normal în fereastra unu browser web, se întâmplă atunci când construiești aplicații web să ai nevoie să generezi materiale care pot fi printate. Și aici ai, în realitate, doar două opțiuni: imagini și PDF, pentru că restul formatelor în general depind de sisteme de operare, software instalat, fonturi instalate, e o nebunie care nu se mai termină.
Am făcut aplicații și pentru una și pentru alta. Generarea de imagini pentru printare este mai ales interesantă când ai o cantitate neînsemnată de text, când documentul generat nu trebuie să poată fi selectat și când nu te interesează flow-ul elementelor (în generarea de imagini în general se lucrează cu poziționare absolută). Spre exemplu, când generăm ecusoanele pentru UP generăm imagini de rezoluție decentă care se pot printa (și când foloseam PHP foloseam the allmighty GD2 library, iar acum folosesc PIL).
Totuși, atunci când generezi documente în care se lucrează cu mult text, și cu text a cărui dimensiune este greu de estimat, atunci începe să fie nevoie de o soluție care să gestioneze flow-ul. Enter ReportLab, o bibliotecă OSS de python pentru realizat documente PDF. ReportLab are de toate, de la acces lowlevel pe un Canvas pe care poți desena orice, la implementări complexe de diferite template-uri de documente, și de pagini individuale. Una
De curând am avut o sarcină care implica generarea unui chestionar în format PDF, după niște reguli bine definite, într-un format dat, și în 12 ore am învățat mai multe mici chestii despre ReportLab.
În primul rând, documentația online este complicat de folosit și nu am reușit să mă conving că este la zi. În schimb, biblioteca fiind până la urmă un generator de PDF-uri, documentația pentru download (evident, format PDF) este completă și oferă câteva exemple utile. Totuși, începutul pare îngrozitor de complicat. Pentru început mi-a fost de mare ajutor post-ul asta.
Font-uri
O primă problemă pe care o ai când faci un document care folosește caractere Unicode (cum ar fi diacriticele românești, indiferent dacă sunt cele corecte sau cele cu ședilă) e că multe font-uri nu au toate glyph-urile (toate semnele) și te trezești cu un document cu multe dreptunghiuri negre în el. În Linux (sistem de operare care, chiar dacă nu rulează pe calculatorul vostru, rulează aproape cu sigranță pe server-ul pe care urmează să existe aplicația voastră), un font recomandat pentru suportul de caractere Unicode este DejaVu.
Sunt câțiva pași aici pentru a avea acces apoi la font-urile astea în cadrul aplicației.
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.pdfmetrics import registerFontFamily, registerFont
registerFont(TTFont('DejaVu', '/usr/share/fonts/truetype/ttf-dejavu/DejaVuSerif.ttf'))
registerFont(TTFont('DejaVuBold', '/usr/share/fonts/truetype/ttf-dejavu/DejaVuSerif-Bold.ttf'))
registerFont(TTFont('DejaVuItalic', '/usr/share/fonts/truetype/ttf-dejavu/DejaVuSerif-Italic.ttf'))
registerFont(TTFont('DejaVuBoldItalic', '/usr/share/fonts/truetype/ttf-dejavu/DejaVuSerif-BoldItalic.ttf'))
registerFontFamily('DejaVu',normal='DejaVu',bold='DejaVuBold',italic='DejaVuItalic',boldItalic='DejaVuBoldItalic')
Documente
Apoi, avem mai multe nivele de acces la construcția paginii. Există obiectul de bază, Canvas, în care se poate desena, pe coordonate, practic oriunde (utile, spre exemplu, dacă scoatem o factură, o chitanță, sau alt fel de tipizate). Totuși, de multe ori, avem nevoie să creem documente în care nu știm cât text vom gestiona (spre exemplu, cărți, documente oficiale, procese verbale).
Pentru asta, ReportLab ne pune la dispoziție o clasă numită SimpleDocTemplate, care are niște sensible defaults pentru pagini. O chestie de reținut este că formatul implicit al paginii este A4.
La creearea documentului, se pot seta și marginile și alți parametrii. Și aici ajungem la ceva foarte important (și interesant) în ReportLab, și anume cum se măsoară distanțele. Cred că exemplul de mai jos este grăitor pentru asta.
from reportlab.platypus import SimpleDocTemplate
from reportlab.lib.units import cm
doc = SimpleDocTemplate(buff, rightMargin = 1 * cm, leftMargin = 1 * cm, topMargin = 1.5 * cm,bottomMargin = 1.5 * cm)
Flowables și paragrafe
Acuma, ReportLab pune la dispoziție ceea ce ei numesc Flowables, adică elemente care se așează unele după altele (ca și cum ar curge) în document. Cel mai util exemplu este clasa Paragraph.
Pentru creare, clasa Paragraph are nevoie de textul efectiv și de stilul cu care trebuie să apară în cadrul documentului (despre stiluri, mai jos). Textul poate fi ca atare, dar ReportLab suportă un limbaj HTML basic, în care putem să introducem dimensiuni, familii și culori de font-uri (font), tag-uri pentru bold (b) și italic (i), linii noi (br) și alte asemenea – se găsesc în ghid toate.
from reportlab.platypus import Paragraph
Paragraph(u"Lorem ipsum Integer in tristique massa. Nunc accumsan.", styles["Normal"])
Stiluri
Pentru a aranja cum arată paragrafele, se pot folosi diferite stiluri. SimpleDocTemplate are o serie de stiluri, cum ar fi Normal din exemplul de deasupra, care pot fi modificate, sau la care se pot adăuga altele, într-un workflow asemănător celui din LibreOffice / Microsoft Office și stilurile de acolo.
Paragrafele au o serie de atribute, cum ar fi font-ul, dimensiunea default, distanța între rânduri, distanța față de alte paragrafe, distanță față de marginea din stânga și din dreapta, alinierea, existența și culoarea unui border și așa mai departe.
O chestiune care mi-a lipsit a fost posibilitatea de a pune border numai pentru o anumită latură.
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_JUSTIFY, TA_CENTER, TA_RIGHT
from reportlab.lib.units import cm
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name='Justify', alignment=TA_JUSTIFY))
styles.add(ParagraphStyle(name='Inalt', fontName = "DejaVu", leading = 0.6 * cm, fontSize = 10))
styles.add(ParagraphStyle(name='Largplus', fontName = "DejaVu", leading = 0.8 * cm, fontSize = 10))
styles.add(ParagraphStyle(name='Right', fontName = 'DejaVu', fontSize = 12, alignment = TA_RIGHT, rightIndent = 1 * cm, leading = 0.6 * cm))
Alte scheme cu flowables
Două elemente bune care intră tot la Flowables sunt Spacer și PageBreak. Spacer-ul are doi parametrii, distanța pe orizontală și distanța pe verticală (deși în documentație spune că distanța orizontală este ignorată).
PageBreak nu are niciun parametru și face exact ce pare a face, încheie pagina și trece la o pagină nouă.
from reportlab.platypus.flowables import PageBreak
from reportlab.platypus import Spacer
Spacer(1, 0.1 * cm)
PageBreak()
Imagini
Evident, se pot introduce și imagini. Pentru partea de imagini cred (nu am testat fără) că este nevoie de PIL, și imaginea este și ea un Flowable
from reportlab.platypus import Image
Image("%s/logo.jpg" % "/var/www/yeti.albascout.ro/images")
Tabele
Tabelele sunt o nebunie întreagă. Principiul este simplu, e o listă de liste python pe baza căreia se construiește tabelul. Ce n-am înțeles, deși am încercat un pic, este cum se stabilesc stilurile peste anumite celule. Un tutorial bun se poate găsi aici.
Conținutul tabelului poate fi un text simplu, sau poate fi un Flowable sau o lista de flowables. Un alt lucru care nu l-am găsit din prima, este că se pot pasa ca parametru cu nume și lățimea coloanelor cu parametru colWidths.
from reportlab.platypus import Paragraph, Image
from reportlab.platypus.tables import Table, TableStyle
data = [["lorem", Paragraph(u"<font><b>Ipsum</b></font>"],
["sit", Image("%s/logo.jpg" % "/var/www/yeti.albascout.ro/images")]]
table = Table(data, colWidths = [1.5 * cm, 15 * cm])
table.setStyle(TableStyle([('INNERGRID', (0,0), (-1, -1), 0.25, colors.black),
('BOX', (0,0), (-1,-1), 0.25, colors.black),]))
Nebunia deosebită stă în cazul în care spre exemplu vrei să scoți pentru o anumită celulă niște border-uri, pe motiv că, spre exemplu, nu poți pune border doar pe-o anumită latură unui paragraf.
Construirea unui document
Ok, în concluzie, există o serie de elemente. Dar cum se pun ele laolaltă. În principiu este foarte simplu: se face un array cu ele, și apoi se pasează metodei build a documentului.
from reportlab.platypus.flowables import PageBreak
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image
nume_fisier = "/home/yeti/Desktop/output.pdf"
doc = SimpleDocTemplate(nume_fisier, rightMargin = 1 * cm, leftMargin = 1 * cm, topMargin = 1.5 * cm,bottomMargin = 1.5 * cm)
Story = []
image = Image("%s/logo.jpg" % "/var/www/yeti.albascout.ro/images")
paragraf = Paragraph("<font size=9>Lorem</font>", styles["Normal"])
Story.append(image)
Story.append(Spacer(1, 2 * cm))
Story.append(paragraf)
Story.append(PageBreak())
doc.build(Story)
Footere și headere
Se întâmpla de multe ori, mai ales când trebuie să respecți un anumit template tehnic, să ai nevoie să pui un header sau un footer. Cum header-ul și footer-ul sunt niște elemente care nu intră la categoria Flowables, se impune intervenția direct pe Canvas-ul documentului. Totuși, ar fi de preferat ca lucrul ăsta să nu ne afecteze abilitatea de a folosi SimpleDocTemplate. De aceea, metoda build a SimpleDocTemplate suportă mai mulți parametrii, cei mai interesanți fiind onFirstPage și onLaterPages , unde se pot specifica numele a două metode care să fie apelate pe prima, și respectiv pe celălalte pagini.
Funcțiile definite trebuie să accepte doi parametrii care vor fi trimiși de build, unul va fi Canvas-ul paginii, iar celălalt va fi documentul, în cazul meu aici un SimpleDocTemplate.
def laterPages(canvas, doc):
'''
Metoda scrie numărul paginii în colțul din dreapta jos pe o pagină A4
'''
canvas.saveState()
canvas.setFont('DejaVu', 8)
canvas.drawString(18 * cm, 1 * cm, "Pagina %d" % (doc.page))
canvas.restoreState()
doc = SimpleDocTemplate("/home/yeti/Desktop/output.pdf")
Story = []
# aici se adauga elemente la doc
doc.build(Story, onLaterPages = laterPages)
Integrare cu Django
Se întâmplă să vrem să generăm nu un fișier, ci un răspuns către browser-ul utilizatorului. Un motiv, spre exemplu, ar fi că nu avem drepturi să scriem local pe server. Altul ar fi timpul de răspuns – dacă ne permite memoria de pe server (altfel spus, dacă documentele sunt suficient de mici).
Soluția este să folosim StringIO, care ne permite să creem obiecte file-like, dar direct în memorie. So, o schiță de soluție ar putea suna așa:
from StringIO import StringIO
from django.http import HttpResponse
from reportlab.platypus import SimpleDocTemplate
def trimite_pdf_ca_raspuns(request):
buff = StringIO()
nume_fisier = "output.pdf"
Story = []
doc = SimpleDocTemplate(buff)
# aici se adauga elemente in Story
doc.build(Story)
response = HttpResponse(mimetype='application/pdf')
response["Content-Disposition"] = 'attachment; filename=%s.pdf' % nume_fisier
response.write(buff.getvalue())
buff.close()
return response