In deze blogpost maken we de vergelijking tussen enkele PDF oplossingen die wij gebruiken in onze applicaties.
Aanleiding tot deze post was een project dat een grondige update onderging en daarbij hoorde ook een update van onze PDF library. We besloten over te schakelen van wkhtmltopdf naar AthenaPdf. Dit om verschillende redenen waaronder inconsistente rendering PDF files op verschillende servers, maar daarover later meer.

wkhtmltopdf

wkhtmltopdf is een command line tool waarmee je makkelijk html kan converteren naar een PDF file.

De eerste versie verscheen meer dan 10 jaar geleden en nadien groeide wkhtmltopdf uit tot een veel gebruikte oplossing om html te converteren naar PDF’s.

 

Pro's

  • Accepteert rechtstreeks html strings, html bestanden of url’s
  • Optie om header en footer mee te geven
  • Enkele basic opties als het genereren van een “table of contents” gebaseerd op h-tags.

Con's

  • Vereist installatie van de library op de server
  • Outdated: niet alle nieuwe HTML5 en css features worden netjes ondersteund
  • Inconsistente rendering: sommige files werden anders gerenderd lokaal vs op staging vs op productie
  • Veel gebruikers ervaren verschillende issues met deze library, wat onder andere te merken is aan de vele openstaande issues in hun git repo (+1000) (https://github.com/wkhtmltopdf/wkhtmltopdf/issues?page=1&q=is%3Aissue+is%3Aopen)

Setup

Om gebruik te kunnen maken, dient de binary geïnstalleerd te worden op je lokale systeem en de servers waarop je gebruik wilt maken van wkhtmltopdf. (https://wkhtmltopdf.org/downloads.html)

Daarna kan je via een PHP-wrapper de binary aanspreken in code:

composer require mikehaertl/phpwkhtmltopdf

Hoe gebruiken

In zijn simpelste vorm kunnen we op onderstaande manier een html file naar een PDF converteren:

use mikehaertl\wkhtmlto\Pdf;

$pdf = new Pdf('/path/to/page.html');
$pdf->saveAs($('/path/to/page.pdf);

 

We kunnen echter ook extra opties meegeven, waaronder de locatie van de binary, print in landscape, gebruik utf-8 encoding, ... En daarnaast de html pagina’s apart toevoegen

 

    use mikehaertl\wkhtmlto\Pdf;


    $pdf = new Pdf([
      'ignoreWarnings' => TRUE,
      'binary' => '/usr/local/bin/wkhtmltopdf',
    ]);
    $pdf->getCommand()->useExec = TRUE;
    $pdf->addPage(
      '<html><head lang="en">
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>My wkhtmltopdf test</title>
    </head><body>' . $myHtml . '</body></html>'
    );
    $pdf->getCommand()->setArgs('-O landscape --encoding utf-8');
    $filename = strtotime('now') . '.pdf';
    $path = 'pdf/' . $filename;
    $pdf->saveAs($path);

AthenaPDF

Athena is een open source dockerized PDF oplossing die vrij simpel op te zetten is in projecten die reeds met docker werken. Je kan html zowel via command line als via url omzetten naar PDF.

Pro’s

  • Docker powered
  • Ondersteunt alle nodige html elementen
  • Snel in gebruik te nemen

Con’s

  • Niet erg veel vrijheid (je kan geen custom headers etc gaan definieren zoals dat bij TCPDF wel kan)

Setup

Om te starten, voeg je de docker container toe aan je project. In dit geval hebben we gekozen voor de service die je vanuit PHP kan aanspreken. Kies een WEAVER_AUTH_KEY. Let op: deze moet ook gekend zijn in je php container.

#docker-compose.yml
athenapdf:
  image: arachnysdocker/athenapdf-service # belangrijk is om de microservice in te stellen en niet de CLI
  environment:
      - WEAVER_AUTH_KEY=${WEAVER_AUTH_KEY}
  ports:
    - '8080:8080'
  labels:
    - "traefik.enable=false"
  networks:
    - default
php:
  image: <your-php-image>
  environment:
      - WEAVER_AUTH_KEY=${WEAVER_AUTH_KEY}

Hoe gebruiken

Gebruik is erg simpel, je roept de pdf service op met dezelfde naam als je container:

 http://athenapdf:8080/convert?auth={$weaverKey}&url=http://www.google.com

Belangrijk is om je weaver key mee te geven alsook de url waarvan je de html wil converteren.  Dat kan een externe of interne url zijn. Als je volledig met docker werkt, kan je een interne route aanspreken via de www container:

http://www/your_route

Indien je html niet rechtstreeks beschikbaar is op een URL, kan je een specifieke "serve-html" endpoint te voorzien die de juiste html gaat samenstellen op basis van de doorgegeven parameters.  Een voorbeeld hiervan is dat je een overzicht wil exporteren naar PDF zonder de rest van de pagina (header, footer, ...) mee te exporteren.
Een andere optie is om de html als post data door te sturen en "&ext=html" toe te voegen aan de athena url.

Eens je de request hebt samengesteld, kan je de response (de gegenereerde PDF file) met het copy command makkelijk op je server bewaren. Bijvoorbeeld:

copy('http://athenapdf:8080/convert?auth={$weaverKey}&url=http://www.google.com', $locationWhereYouWantToSaveThePDF);

Daarna kan je deze file vanaf die locatie oproepen en downloaden.

 

Things to keep in mind

  • AthenaPDF is slim wat betreft tables, maar in sommige gevallen kan dit net tot ongewenste layout leiden.
    Athena herkent table headers (th elementen) en wanneer je tabel over meerdere pagina's heen loopt, zal Athena de table headers automatisch herhalen bovenaan elke pagina.  Dit kan handig zijn, maar is zeker niet altijd gewenst. Om dergelijk gedrag te voorkomen, dien je de "th's" om te zetten naar "td's".
  • Er is geen default option om mee te geven dat je de PDF in landscape format wil printen, maar dit is makkelijk op te vangen via css:
    @media print{
      @page {
        size: landscape;
      }
    }

TCPDF

Heb je nood aan een uitgebreide PHP library, dan is TCPDF de tool voor jou. Bij uitbreiding kan je deze combineren met FPDI om ook merging van PDF files mogelijk te maken.

Waar voorgaande tools je de gegenereerde PDF terugsturen, heb je hier alles zelf in de hand: custom header of footer, extra velden, images, ... Wat je maar wilt, (bijna) alles is mogelijk.

Voor de meeste frameworks zijn er reeds packages gebouwd rond TCPDF, denk maar aan QipsiusTCPDFBundle voor Symfony of elibyy/tcpdf-laravel voor Laravel.  TCPDF is echter ook perfect te gebruiken zonder deze wrappers.

Het volstaat om simpelweg “composer require tecnickcom/tcpdf” uit te voeren. Hierna kan je de basic file makkelijk extenden en overschrijven met de gewenste zaken, zoals bvb een custom header en footer.

Pro’s

Con’s

  • Vereist wat kennis om PDF’s correct te genereren
  • Huidige versie is niet langer maintained zoals ze ook vermelden op hun git pagina. Er wordt echter wel gewerkt aan een nieuwe versie (https://github.com/tecnickcom/tc-lib-pdf)

Setup

Zoals eerder gezegd volstaat het om een simpele composer require uit te voeren:

composer require tecnickcom/tcpdf

Hoe gebruiken

Hoe je TCPDF zal gebruiken is sterk afhankelijk van het doel en de complexiteit van je PDF generatie.
Een simpel voorbeeld van hoe je deze library kan gebruiken, is als volgt:

// Create
$myFile = new \TCPDF();
$myFile->SetAuthor('John Doe');
// Set content
$myFile->AddPage();
$content = '<div>This is a test</div>';
$myFile->writeHTML($content);
// Save
$myFile->Output('my_first_pdf_file.pdf', 'F');

Maar zoals eerder aangehaald, gebruiken we deze library voor complexere zaken, want bovenstaand voorbeeld kan even makkelijk met wkhtmltopdf of AthenaPDF gerendered worden.

Omdat de mogelijkheden van TCPDF zeer uitgebreid zijn, vermelden we slechts enkele voorbeelden die belangrijk waren voor onze toepassing.

Om te beginnen hebben we de TCPDF class extended en aangepast aan onze noden. Zo konden we onder andere gebruik maken van:

  1. de FpdiTrait
  2. custom footer en headers toevoegen
  3. custom veldjes toevoegen

Over deze zaken lees je hieronder meer.


Extenden van de class:

final class ContractPdf extends \TCPDF

1) FpdiTrait om files te mergen

Een functionaliteit die vereist was in de applicatie die we bouwden, was het mergen van een zelf samengestelde PDF met opgeladen bijlages.
Er zijn een paar mogelijke manieren om dit aan te pakken, maar wij hanteerden onderstaande methode.

Eerst includen we de FpdiTrait in onze custom TCPDF class om gebruik te kunnen maken van de nodige merge functionaliteiten.

use FpdiTrait;

Vervolgens werken we de effectieve merge functionaliteit uit.

We intialiseren een TCPDF object en voegen het basiscontract eraan toe.
Daarna voegen we alle gewenste bijlages toe.
Uiteindelijk kan de samengevoegde PDF bewaard worden op de server.

// Initialize TCPDF (which uses the FpdiTrait)
$this->file = new ContractPdf();
// Add our custom created contract PDF file
$this->addFile('contract.pdf');
// Add all attachments
$attachments = ['attachment1.pdf', 'attachment2.pdf'];
foreach ($attachments as $attachment) {
    $this->addFile($attachment);
}
// Save our merged PDF file
$this->file->Output('mergedPDF.pdf', 'F');

De addFile functie die hierboven aangesproken wordt, is als volgt opgebouwd:

public function addFile($filename)
{
    $pageCount = $this->file->setSourceFile($filename);
    $i = 1;
    // Loop over all the pages in de given PDF
    while ($i <= $pageCount) {
        $pageId = $this->file->importPage($i, PageBoundaries::MEDIA_BOX);
        $this->file->AddPage();
        // Import the page in our final file
        $this->file->useImportedPage($pageId);
        $i++;
    }
}

Belangrijke opmerking:

De standaard FPDI parser kan slechts PDF’s verwerken tot en met versie 1.4. Dit omdat de compressie features in versie 1.5 en hoger anders zijn. Meer daarover kan je hier lezen: https://www.setasign.com/products/fpdi-pdf-parser/details/

Setasign biedt een commerciële add-on aan om PDF’s met hogere versies te kunnen mergen.
Het is echter ook mogelijk om de PDF’s naar versie 1.4 te converteren alvorens te mergen. Deze conversie kan je bekomen met bijvoorbeeld Ghostscript.


2) Custom headers en footers

De header en footer functies kan je overschrijven in je custom extended class zodat je de header kan renderen op de manier die jij nodig hebt.  Voor ons was het belangrijk om via opties te kunnen doorgeven of de header op elke pagina geprint moest worden of enkel op de eerste pagina. Daarnaast moest de alignment van het logo (left/right) dynamisch bepaald kunnen worden.

public function Header() {
    // Only print header if it may be printed on each page, or if we are on the first page
    // If on each page, only on contract pages itself, not on attachment pages
    $printHeader = ($this->headerData['header_on_each'] && $this->page <= $this->headerData['contract_num_pages']) || $this->page === 1;
    if($printHeader) {
        $logoWidth = 50;
        $logoHeight = 50;
        // Check which type of alignment is chosen and place header data accordingly
        switch ($this->headerData['align']) {
            case 'textleft':
                $this->writeHTMLCell(100,50, 10,10, $this->headerData['text']);
                $this->Image($this->headerData['image'], 150, 10, $logoWidth, $logoHeight, $this->headerData['image_extension'], ...);
                break;
            case 'textright':
                $this->writeHTMLCell(100,50, 95,10, $this->headerData['text']);
                $this->Image($this->headerData['image'], 10, 10, $logoWidth, $logoHeight, $this->headerData['image_extension'], ...);
                break;
        }
    }
}

3) Custom Velden toevoegen

Daarnaast zijn er zoveel meer mogelijkheden met TCPDF, waaronder "table of content" pagina's automatisch toevoegen, background images voorzien voor pages, protection inbouwen, digital signature certification en zoveel meer.

Conclusie

Afhankelijk van de manier waarop je PDF's wil genereren, leent de ene oplossing zich beter dan de andere. 

Ondanks dat wkhtmltopdf één van de meer populaire converters was, zou ik deze niet aanraden vanwege de outdated code en de inconsistentie in rendering. Er zijn ondertussen verschillende alternatieven beschikbaar zoals onder andere AthenaPDF, maar tijdens research voor dit blogartikel kwam ik ook zaken als Puppeteer etc tegen.

Wil je echter meer complexe PDF's opbouwen en heb je nood aan veel customization, dan is een oplossing als TCPDF ideaal.

Auteur: Sarah Jehin
PHP developer
Sarah Jehin

More insights

Cross-platform applicaties met React Native

Nog nooit was het ontwikkelen van native mobiele applicaties zo toegankelijk als vandaag. Bij Codana doen we dit door gebruik te maken het React Native, een open-source framework dat werd ontwikkeld door Meta.

Auteur: Jinse Camps
Architect | Analyst
Jinse Camps
dev

Laracon EU 2024

Een fantastisch leerrijke ervaring om met een hoop Laravel gepassioneerde mensen te inspireren! Iets wat niet gemist kan worden en heel veel voeling geeft met de community. Wat een top evenement! Wie zien we volgende edities? 😮

Auteur: Noah Gillard
PHP / Laravel Developer
Noah Gillard AI generated Face
laracon codana persoon

Een efficiënt datamanagementsysteem voor toerisme

Een TDMS of Tourist Data Management System, is simpelweg een platform dat data uit verschillende bronnen ophaalt, intern al dan niet automatisch verwerkt en deze gegevens terug aanbiedt aan externe platformen.

Auteur: Tom Van den Eynden
Web Architect | Coordinator
Tom Van den Eynden
laptop

Systemen voor gegevensbeheer in toerisme

In dit artikel verkennen we wat een TDMS is, waarom het essentieel is voor de toerisme-industrie, en hoe technologieën zoals Laravel en ElasticSearch het verschil kunnen maken. 

Auteur: Tom Van den Eynden
Web Architect | Coordinator
Tom Van den Eynden
tdms

Beveiliging van Laravel 101

In deze blogpost gaan we dieper in op een aantal veelvoorkomende Laravel beveiligingsfouten.

Auteur: Robbe Reygel
PHP developer
laravel

Test Driven Development - toepassing op een project

TDD, of voluit Test Driven Development, is een aanpak van ontwikkeling waarbij we vertrekken van het schrijven van tests. 

Auteur: Sarah Jehin
PHP developer
Sarah Jehin
development