Recently I had to create PDF files in a Symfony project using Twig, however this is not as easy as it seems, for example you need a special CSS file adapted to print.

Another obstacle is images, I didn’t have any problem including images from an external source, the problem came when I had to use images from the same project. In this post I explain why using images is a tricky task and how I solved this problem.

This is the stack I used to generate PDF files:

I will not explain how to generate PDF files, I assume you already know how to use Twig and KnpSnappyBundle.

Using assets to include an image (didn’t work)

The naive solution is treat a PDF file as any other Twig view, simply use assets function to include an image.

<img src="{{ asset('avatar.png') }}"/>

This didn’t work because I was in a Dockerized environment, outside the container the project’s url was something like http://web.project.localhost, but within the Docker container this URL does not exist and means nothing, therefore the image was never found.

Using special asset configuration (it didn’t work either)

As told before, the url http://web.project.localhost is not resolved when requested inside the container, so what kind of URL is always valid inside a container ? The answer is a loopback address, that’s how I had the idea to use 127.0.0.1 to load my images, so I had to configure a special asset package in framework.yaml file.

#config/packages/framework.yaml
assets:
  packages:
    attachments-pdf:
      base_urls: 'http://127.0.0.1/attachments'

To generate an absolute url using attachments-pdf package:

<img src="{{ asset('avatar.png', 'attachments-pdf') }}"/>

This would have worked except that, in order to view an image, I needed to be logged in to the website, image was never displayed because instead of an image I was receiving the login page.

Embedding images in Twig

So finally I came up to the idea to embed images directly inside PDF file, I wasn’t so keen about this solution because it’s not a drop-in solution, it requires some configuration to work properly.

To embed an image within any HTML file you can use DATA URIs instead of an image url, this is the syntax that DATA URI follows:

data:<mimetype>;<encoding>,<data>

Here an example taken from Wikipedia:

<img src="data:image/png;base64,iVBORw0KGgoAAA
ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU
5ErkJggg==" alt="Red dot"/>

Luckily for me, somebody already created a the data_uri filter to create DATA URIs, this filter is not installed by default, so the following packages are required twig/html-extra and twig/extra-bundle.

composer require twig/html-extra twig/extra-bundle

To avoid any problems with paths I decided to use absolute paths, in consequence I configured this in framework.yaml, I used base_path instead of base_urls.

#config/packages/framework.yaml
assets:
  packages:
    attachments-pdf:
-      base_urls: 'http://127.0.0.1/attachments'
+      base_path: '/app/assets/attachments'

Next, I needed a way to read the image content, this can be easily done in PHP with file_get_contents function, because there is no similar functionality in Twig I had to write my own file_get_contents Twig filter. You can use make bundle to create a Twig extension, our custom filter will be located inside.

bin/console make:twig-extension FileExtension

Then we create file_get_contents Twig filter, as you can see our Twig function will call PHP’s file_get_contents function.

<?php // src/Twig/FileExtension.php

namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use function file_get_contents;

class FileExtension extends AbstractExtension
{
  public function getFilters(): array
  {
    return [
      new TwigFunction('file_get_contents', file_get_contents(...)),
    ];
  }
}

Finally, putting everything together:

<img src="{{ asset('avatar.png', 'attachments-pdf') | file_get_contents | data_uri }}"/>

How it works ?

  1. First asset('avatar.png', 'attachments-pdf') will generate an absolute path, in our example this will be /app/assets/attachments/avatar.png. Remember we configured this in framework.yaml.
  2. Then file_get_contents filter will receive the file path and return the content of avatar.png.
  3. Finally, we pass the image content to data_uri filter, this filter is very handy because it will do all the hard work for us: generate a valid DATA URI syntax, detect the mime type and convert the image to base 64.

Conclusion

I explained my journey trying to display images in a PDF file, I tested many solutions and I decided to use DATA URIs.

In order to use DATA URIs I had to:

  1. Install data_uri Twig filter.
  2. Configure assets packages in framework.yaml.
  3. Create file_get_contents Twig filter.

I think using DATA URIs is the best solution when you are creating PDF files, this will allow you to add images in any environment, dockerized or not.