Rendering PDF with Headless Chrome and Laravel

We already introduced two ways – DOMPdf and WKHTML – to render PDF with Laravel easily. However, there is a third option that worths to talk about. Let’s see how to render a PDF easily with headless chrome and Laravel.

First of all be sure, that Google Chrome is installed on your machine/server. There are ways to do it, depending on where do you need to use that.

We’ll stick here with the invoice concept to make it easily comparable with the other PDF rendering solutions we introduced.

The PDF Service

Like before, we also make a PDF service that handles the PDF generation for us. The difference here is that we don’t really need a service provider to bind the service to the container and resolve its dependencies. We don’t really have any configuration here – maybe the path of the binary – that we need to use for the service. Let’s see how does it look like then:

<?php

namespace App\Services;

use App\Invoice;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class Pdf
{
    /**
     * The command.
     * 
     * @var string
     */
    protected $command = '%s --headless --disable-gpu --print-to-pdf=%s %s 2>&1';

    /**
     * The binary.
     * 
     * @var string
     */
    protected $binary = '/Applications/Chromium.app/Contents/MacOS/Chromium';

    /**
     * Render the PDF.
     *
     * @param  \App\Invoice  $invoice
     * @return string
     */
    public function render(Invoice $invoice)
    {
        $view = View::make('invoice', compact('invoice'))->render();

        $process = new Process(sprintf(
            $this->command,
            escapeshellarg($this->binary),
            escapeshellarg($path = tempnam(sys_get_temp_dir(), Str::random())),
            escapeshellarg('data:text/html,'.rawurlencode($view))
        ));

        try {
            $process->mustRun();

            return File::get($path);
        } catch (ProcessFailedException $exception) {
            //
        }
    }
}

So, what’s going on here? We use Chrome’s PDF service. We need to provide a source URL and a destination to save the PDF. In our case, the URL is a data URI format, that contains our rendered HTML. We save the generated PDF in the temporary directory and return its content. That’s where the controller comes in the picture.

The PDF Controller

From this point, everything is the same as before. We create the controller and register the route for it.

Route::get('invoice/{invoice}', 'PdfController');

The controller is the same as it was in the WKHTML version:

<?php

namespace App\Http\Controllers;

use App\Invoice;
use App\Services\Pdf;
use Illuminate\Http\Request;

class PdfController extends Controller
{
    /**
     * The pdf instance.
     *
     * @var \App\Services\Pdf
     */
    protected $pdf;

    /**
     * Create a new controller instance.
     *
     * @param  \App\Services\Pdf  $pdf
     * @return void
     */
    public function __construct(Pdf $pdf)
    {
        $this->middleware('auth');

        $this->pdf = $pdf;
    }

    /**
     * Generate the PDF to inspect or download.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Invoice  $invoice
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request, Invoice $invoice)
    {
        return response($this->pdf->render($invoice), 200)->withHeaders([
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => ($request->has('download') ? 'attachment' : 'inline') . "; filename='invoice-{$invoice->id}.pdf'",
        ]);
    }
}

That’s it. It works the same as before. If we add the ?download key to the URL we download the PDF otherwise it will be an inline attachment.

Page Size and Orientation

Let’s quickly talk about configuring the blade templates. As we said, we don’t have any options that we can use and add to the command. It means, we need to find another way to control the page size and the orientation. We can do this with the @page CSS rule.

<!-- The blade template -->
...
<style>
    @page {
        size: A4 portrait;
        margin: 0;
    }
</style>
...

By defining the size, we can set the real size and orientation at the same time. During the PDF rendering, headless Chrome will read these properties and apply them.

The margin has a different purpose. If we set the margin to zero, we can remove the automatically appended header and footer from the document. Since using chrome from the command line, we don’t have any control over that, only by setting the margin of the whole page.

Summary

As we experienced it so far, this solution is quite fast and renders good-sized files, unline WKHML. Also, we can be very flexible here, because we can use any CSS thing here – like flexbox – to create any layout, which is impossible with DOMPdf and WKHTML.

Need a web developer? Maybe we can help, get in touch!