Django + PDF - przegląd metod i narzędzi
Dzisiaj chciałbym pokazać Wam dostępne narzędzia i metody tworzenia plików PDF z poziomu frameworka Django. Dokumentacja samego frameworka w tym temacie jest nad wyraz uboga i ukazuje bardzo pobieżnie jedną tylko metodę z wykorzystaniem narzędzia reportlab.
Zastanówmy się co właściwie jest nam potrzebne aby z poziomu Django móc tworzyć dokumenty PDF?
- po pierwsze - odpowiednie narzędzie, które umożliwi nam z poziomu języka Python tworzyć dokumenty PDF. Może to być zewnętrzny program, lub moduł/biblioteka języka Python.
- po drugie - element (dodatek), który zrealizuje połączenie pomiędzy kodem naszego projektu Django, a tym narzędziem.
Narzędzia do tworzenia PDFów
Poniżej przedstawiam listę najpopularniejszych narzędzi związanych z zagadnieniem tworzenia dokumentów PDF z poziomu języka Python i Django. Wszystkie wymienione tutaj narzędzia i biblioteki możemy wykorzystywać z Pythonem w wersji 3.x i Django w wersji 2.x.
ReportLab
ReportLab jest darmowym, otwartoźródłowym silnikiem do tworzenia dynamicznych dokumentów PDF i grafik wektorowych napisanym w Pythonie. Na stronie producenta dostępna jest również bardziej rozbudowana, płatna wersja PLUS. Wersja PLUS posiada wiele dodatkowych funkcjonalności m.in. zaimplementowano w niej obsługę własnego języka szablonów RML (Report Markup Language).
Silnik ReportLab składa się z trzech komponentów (warstw):
- z niskopoziomowego API, które umożliwia “graficzne rysowanie” na płótnie reprezentującym stronę PDF
- z biblioteki widżetów i wykresów do tworzenia grafik
- z wysokopoziomowej biblioteki PLATYPUS (Page Layout And TYPography Using Scripts), która pozwala programowo budować dokumenty PDF z elementów takich jak, nagłówki, paragrafy, czcionki, tabele i grafiki wektorowe.
Instalacja silnika ReportLab sprowadza się do wydania polecenia:
xhtml2pdf
Xhtml2pdf jest biblioteką języka Python, która umożliwia tworzenie dokumentów PDF na podstawie zawartości HTML. Mamy tutaj możliwość zarządzania przepływem treści - automatycznym lub ręcznym dzieleniem na strony. Biblioteka ta występuje jako moduł języka Python, który może być importowany w programach (np. Django). Dodatkowo dostępny jest niezależny program, który może być wywoływany z linii poleceń.
Bibliotekę instalujemy w następujący sposób:
wkhtmltopdf
Kolejnym narzędziem, który możemy wykorzystać do tworzenia dokumentów PDF jest zewnętrzny program wkhtmltopdf. Podobnie jak xhtml2pdf pozwala na tworzenie dokumentów PDF na postawie zawartości HTML. Aby z niego skorzystać musimy posiadać egzemplarz programu w postaci binarnej, działający w systemie operacyjnym, w którym działa nasz projekt - ogranicza to mocno jego stosowanie np. w produkcyjnych środowiskach chmurowych (np. Heroku).
Instalacja sprowadza się do pobrania skompilowanej wersji binarnej programu lub zbudowaniu go samodzielnie ze źródeł.
W przypadku niektórych dystrybucji linuksa (Debian/Ubuntu) pakiet wkthtmltopdf dostępny jest w repozytoriach dystrybucji. Jednak po zainstalowaniu może okazać się, że jest pozbawiony niektórych funkcjonalności albo nawet nie działa w trybie headless (bez serwera X). Należy wówczas pobrać (zainstalować) odpowiednią wersję spoza repozytoriów dystrybucji.
WeasyPrint
Ostatnim narzędziem, jaki chciałbym tutaj przedstawić jest WeasyPrint. Jest to darmowy, otwartoźródłowy silnik renderujący dla zawartości HTML i CSS, który pozwala na eksport to dokumentów PDF i w założeniu ma spełniać standardy internetowe w zakresie drukowania.
Podobnie jak xhtml2pdf występuje zarównko jako moduł języka Python i jako niezależny program wykonywalny.
WeasyPrint instalujemy w następujący sposób:
Może się okazać, że do instalacji potrzebujemy zestawu dodatkowych pakietów (w przypadku dystrybucji Linux, macOs) lub komponentów samego Pythona (Windows). W razie wystąpienia problemów z instalacją możemy wesprzeć się dokumentacją biblioteki.
Korzystamy z narzędzi PDF w Django
Znamy już dostępne narzędzia, teraz przyszła więc pora na poznanie sposobów (rozszerzeń Django) na łatwe ich użycie w kodzie naszych projektów. Każde wyżej wymienione narzędzie (poza ReportLabe) posiada dedykowane rozszerzenie, które pośredniczy pomiędzy Django a danym narzędziem.
narzędzie | rozszerzenie |
---|---|
ReportLab | - |
xhtml2pdf | django-xhtml2pdf |
wkhtmltopdf | django-wkhtmltopdf |
WeasyPrint | django-weasyprint |
Oczywiście korzystanie z tych rozszerzeń nie jest obligatoryjne ale bardzo często jest wygodne i umożliwia znaczne uproszczenie kodu - co pokażę w dalszej części artykułu.
Instalacja każdego z tych rozszerzeń to tradycyjnie:
gdzie xxxxx to nazwa rozszerzenia.
- dopisujemy 'wkhtmltopdf' do _INSTALLED_APPS_
- w ustawieniach (settings) definiujemy ścieżkę do narzędzia: WKHTMLTOPDF_CMD = '/path/to/my/wkhtmltopdf'
- weryfikujemy czy w ustawieniach mamy zdefiniowaną zmienną określającą ścieżkę dla plików statycznych STATIC_ROOT
Metody tworzenia PDFów
Renderowanie PDF w widoku
Pierwszą metodą tworzenia PDFów w Django, jaką chciałbym tutaj pokazać jest metoda polegająca na tworzeniu dokument PDF w widoku. W momencie wywołania funkcji widoku, tworzony jest dynamicznie dokument PDF i przekazywany jest on do obiektu response (HttpResponse) celem zwrócenia do przeglądarki.
ReportLab
Dla narzędzia ReportLab przykładowy kod widoku wygląda następująco (niskopoziomowe API):
a tworzenie PDFów z wykorzystaniem PLATYPUS:
xhtml2pdf
Przygotujemy najpierw szablon HTML django, który będziemy wykorzystywać kolejnych przykładowych widokach:
Użyjemy najpierw “czystej” biblioteki xhtml2pdf, tak aby moć później porównać kody widoków:
następnie zobaczmy przykładowy kod widoku z wykorzystaniem dodatku django-xhtml2pdf:
W dodatku django-xhtml2pdf mamy już domyślnie zaimplementowaną funkcję zwrotną (callback) i nie musimy martwić się o obsługę i pobieranie linków (URL), które będą występować w szablonie HTML (lokalne i zdalne). W przypadku, gdy korzystamy z “czystej” biblioteki xhtml2pdf powinniśmy napisać własną funkcję i przekazać ją do metody CreatePDF.
dla formalności jeszcze zobaczmy jak wygląda kod widoku klasowego:
Dodatek django-xhtml2pdf daje nam możliwość skorzystania z dekoratora pdf_decorator, dzięki któremu możemy łatwo przekształcić istniejący widok w widoku zwracający dokument PDF.
wkhtmltopdf
Tak jak wspominałem wcześniej dodatek django-wkhtmltopdf wykorzystuje zewnętrzny program wkhtmltopdf, który musi być dostępny do uruchomienia z poziomu projektu django. W pierwszej kolejności, na podstawie szablonu, tworzony jest tymczasowy plik HTML, następnie, poprzez Popen, wywoływany jest program wkhtmltopdf, którego standardowe wyjście jest przechwytywane i zwracane do widoku.
WeasyPrint
django_weasyprint dostarcza nam klasę widoku WeasyTempalteView, która może nam dostarczać dokumenty PDF. Do dyspozycji mamy też klasę pomocniczą (Mixin) WeasyTemplateResponseMixin, którą pozwala nam na prostą rozbudowę istniejących już widoków celem wygenerowania dokumentów PDF. Ten drugi sposób jako mniej trywialny przedstawiam poniżej:
Dysponujemy wcześniej napisanym widokiem który zwraca nam kod HTML MyView. Dopisujemy następnie widok WeasyPrintView, który rozszerzy istniejący widok i doda odpowiedni Mixin, tak aby uzyskać jako odpowiedź dokument PDF.
Zapis PDF do pliku
W niektórych projektach możemy spotkać się z sytuacją, gdy potrzeba będzie zapisać gdzieś na serwerze, albo odległym zasobie (np. w AWS S3) wygenerowany dokument PDF celem późniejszego wykorzystania. Do tej pory tworzenie dokumentów PDF odbywało się niejako “w locie”. Na podstawie żądania, wywoływana była funkcja widoku, która inicjowała utworzenie dokument PDF i niezwłocznie zwracała taki utworzony dokument jako odpowiedź na żądanie użytkownika. Wszystko odbywało się “w pamięci” bez użycia przestrzeni dyskowej (wyjątkiem są: wkhtmltopdf - na potrzeby tego programu przygotowany zostaje tymczasowy plik HTML, i ReportLab w trybie PLATYPUS - tutaj tworzymy na dysku tymczasowy plik PDF dla SimpleDocTemplate).
Weźmy dla przykładu konkretny przypadek użycia. Potrzebujemy funkcjonalności generowania faktur elektronicznych dla naszych klientów. Posiadamy odpowiednie modele reprezentujące klientów, usługi, ceny itp. Chcemy raz w miesiącu generować faktury PDF i automatycznie wysyłać je mailem do klientów. Dodatkowo każdy klient ma mieć dostęp do swoich faktur w formacie PDF z poziomu swojego panelu klienta.
Istotną kwestią jaka pojawia się w takim przypadku jest kwestia lokalizacji wygenerowanych plików PDF. Gdyby chodziło o samą wysyłkę mailem jako załącznika, to moglibyśmy nawet wogóle nie tworzyć pliku PDF w przestrzeni dyskowej, tylko przekazywać treść dokumentu do metody wysyłającej maile, podobnie jak do obiektu response. Moglibyśmy też skorzystać z mechanizmu plików tymczasowych Pythona. Wygenerowane pliki PDF muszą być fizycznie gdzieś przechowywane w tej sytuacji. Powstaje pytanie czy lokalizacja MEDIA_ROOT w django jest odpowiednia? W tym wypadku zdecydowanie NIE! Każdy, kto znałby lokalizację i nazwę pliku PDF byłby wstanie pobrać go z naszego serwera i to niezależnie od tego jaki schemat nazywania i rozmieszczania tych plików byśmy przyjęli.
Jedną z możliwości z jakich możemy w tym miejscu skorzystać to stworzenie lokalizacji poza MEDIA_ROOT i STATIC_ROOT (o ile zdecydujemy się na przechowywanie plików w przestrzeni dyskowej serwera) i w niej umieszczać wygenerowane pliki PDF. Dodatkowo powinniśmy stworzyć mechanizm do “serwowania” tych plików użytkownikom wyposażony w mechanizmy autoryzacyjne. Pomocna tutaj mogą okazać sie klasa FileSystemStorage oraz pola typu db.models.FileField.
Zobaczmy jak wygląda fragment kodu, umożliwiający zapis dokumentu PDF do pliku z wykorzystaniem WeasyPrint:
i analogicznie pobieranie takiego pliku:
Wydajność
Generowanie dokumentów PDF bezpośrednio w widoku może się wiązać z pewnym narzutem czasowym, który z punktu widzenia użytkownika przeglądarki może być czasami nieakceptowalny. Należy wówczas wykorzystać metody asynchronicznego generowania dokumentów (np. przy pomocy Celery).
Średnie czasy odpowiedzi na żądanie w przypadku wyżej wymienionych metod i przedstawionego prostego szablonu HTML, oscylują w granicach 1000-1700 ms (czyli od 1 do ok. 2 sekund). Bardziej złożone dokumenty mogą generować się nieco dłużej. Przykładowo w podobnych warunkach kilkunastostronicowy dokument PDF z grafikami i uzupełnianiem danych z modeli generuje się ok 5 sekund.
Podsumowanie
Podczas pracy z tworzeniem dokumentów PDF możemy spotkać się kilkoma problemami, które na zakończenie tylko zasygnalizuję:
- osadzanie czcionek i problemy z polskimi (międzynarodowymi) znakami diakrytycznymi
- osadzanie obrazów będących zdalną zawartością (http vs https)
- skalowanie obrazów przy umieszczaniu w PDF
- problemy z automatycznym podziałem tekstu między stronami w przypadku tabel czy nawet paragrafów.
- brak obsługi wielu właściwości CSS w różnych narzędziach.
Jeśli macie własne doświadczenia i przemyślenia w temacie tworzenia dokumentów PDF online to zapraszam do dzielenia się w komentarzach.
Zostaw komentarz