Generowanie plików PDF z szablonów – czyli RazorEngine, wkhtmltopdf, JavaScript FTW!

27.06.2013

Generowanie plików PDF z poziomu kodu programu to temat rzeka. Wystarczy przejrzeć staka. Temat przewija się w prawie każdej tworzonej dzisiaj aplikacji. Zawsze znajdzie się ktoś komu trzeba wysłać fakturę, raport czy kilka wykresów w PDFie. Problem w tym, że narzędzia do generowania ładnych dokumentów PDF kosztują i to nie mało. Dodatkowym minusem jest fakt, ze większość tych bibliotek za kilka tysięcy baksów skupia się na konwersji do PDF bardziej niż na generacji PDFa na podstawie szablonu.

Oprócz całej rzeszy komercyjnych bibliotek istnieją też rozwiązania open source, ale i tutaj nie jest łatwo coś wybrać. W większości przeznaczone są one do manipulacji/budowania PDF z kodu lub konwersja do tego formatu. Dodatkowo trzeba uważać na licencje pod jakimi rozpowszechniany jest dany komponent (patrz zmiany w iTextSharp), bo nie zawsze da się go spokojnie wykorzystać w naszym “komercyjnym” projekcie bez ponoszenia dodatkowych kosztów.

Drobnym wyjątkiem jest tutaj kontrolka ReportViewer dostarczana razem z Visual Studio i dająca możliwość (kontrolka!) generowania plików PDF z szablonów rdlc. Jeżeli w swoim programie masz do wygenerowania w miarę proste dokumenty, śmiało, skorzystaj z ReportViewer. Powinien wystarczyć. Jeżeli jednak musisz wygenerować dokument z piętrowymi tabelami, wewnątrz których czai się cała masa pogrupowanych wierszy… Strzeż się! To przeklęte przez Boga i ludzi narzędzie nie zostało stworzone z myślą o tym by ułatwić Ci pracę. Dość powiedzieć, że obsługuje się je z poziomu dizajnera, zaś programowanie sprowadza się do pisania paskudnych wyrażeń przypominających skrzyżowanie formuł Excela z Visual Basic. Istny koszmar.

Musze się przyznać, że do niedawna sam dość często korzystałem z ReportViewer. Po tym doświadczeniu został mi odruch bezwarunkowego rzucania przekleństw za każdym razem kiedy usłyszę lub zobaczę akronim RDLC.

W tym momencie na scenie pojawia się wkhtmltopdf. To sprytne narzędzie oparte o silnik WebKit pozwala wygenerować plik PDF ze URL’a lub pliku HTML. Niestety podobnie jak ma to miejsce w przeglądarkach, bezpośrednie “drukowanie” do pliku PDF powoduje ucięcie naszych pięknych tabelek w połowie wiersza, co nie wygląda zbyt ciekawie. Rozwiązaniem tego problemu jest JavaScript. Tak, JavaScript! wkhtmltopdf najzwyczajniej w świecie interpretuje i uruchamia skrypty na stronie, którą zamierza przekonwertować. Dzięki czemu niejaki Florian Stancu popełnił ten skrypt, który ładnie łamie tabele w naszym dokumencie.

Ostatnim elementem układanki jest biblioteka RazorEngine, dzięki której możliwe staje się wykorzystanie silnika z ASP.NET MVC w celu definiowania szablonów dokumentów. Żeby nie być gołosłownym, naklikałem małą bibliotekę DotReports łączącą te wszystkie komponenty w całość. Dzięki temu możliwe jest szybkie wygenerowanie pliku PDF na podstawie pliku/szablonu cshtml:

var model = new Model {Id = 1, text = "FTW!"};
var dr = new DotReport();
dr.GenerateFromFile("Example1.cshtml", @"C:\example1.pdf", model);

Garść porad.

  • Jeżeli masz ustawione inne niż standardowe Dpi w Windows, skorzystaj z parametru PdfSettings metody GenerateFromFile aby ustawić Dpi dokumentu. Określanie Dpi z poziomu JS nie działa (nie ważne co piszą w Internecie) i zwraca zawsze wartość 96 w Windows (WebKit pod Windows?). Wartość Dpi to tak na prawdę “jakaś stała”, która nie koniecznie odpowiada wartości Dpi systemu. U mnie na powiększeniu 125% wartość Dpi skryptu to 150. To prawdopodobnie błąd w skrypcie Floariana.
  • Jeżeli dołączysz plik chtml do projektu typu Class library nie będzie dostępne Intellisense i wszelkie dyrektywy Razora, w tym @model. Aby temu zaradzić należy dodać do projektu web.config jak poniżej, oraz dodać wszystkie wymienione w nim assembly jako referencje do projektu:
<?xml version="1.0"?>
<configuration>
    <system.web>
        <compilation debug="true" targetFramework="4.5" >
            <assemblies>
                <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
                <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
                <add assembly="System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
                <add assembly="System.Web.WebPages, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
            </assemblies>
        </compilation>
    </system.web>
</configuration>

Works on my machine.

Biblioteka dostępna pod adresem https://bitbucket.org/jdubrownik/dotreports.