labelmaker - convert HTML+CSS to PDF

labelmaker - convert HTML+CSS to PDF
Page content

labelmaker is a tool to convert themeable HTML and CSS templates to PDF files. It is written in PHP and supports dynamic scripts including data retrieval and conversion as well as powerful TWIG templates.

Introduction

Phoniebox PDF Preview

I started labelmaker to generate Box-Labels for my daughters Phoniebox . The source code is available on GitHub and still in a pretty early state of development, but already pretty powerful. If you would like build a Phoniebox yourself, here is the hardware I used

Prerequisites

To use labelmaker, you only need:

  • PHP >= 7.4

Note: PHP has some extensions, that are required, but they should be enabled by default. If you get an error, try to look up the php.ini / extension=... parts and enable the required extensions.

  • json, zip, phar, mbstring

Setup

labelmaker is packaged in a single PHAR file. It is huge (> 80MB, PHP Archive), mainly because of mpdf fonts required for PDF generation, but at least you only need to

Download

one single file. Recommended is the latest release, just move it in your $PATH and run it like this:

labelmaker.phar --version

Or on Windows maybe

php labelmaker.phar --version

That’s it…

Usage basics - page-templates

For creating PDFs with labelmaker, you should start with some basic examples. You should also know that it is possible to use the --theme parameter, which simplifies the document building process by providing shorthand for all other options. Read the Themes chapter for more information, but I would recommend to start here.

Static single page PDF - Hello world

To generate a simple PDF without dynamic parts, you can simply create a page template with the following contents, while {# hello-world.twig #} is only a comment, that tells you the name of the file:

{# hello-world.twig #}
<style>
    .hello  {
        color:red;
    }
</style>

<p class="hello">hello world</p>
labelmaker.phar --page-template="hello-world.twig" --output-file="hello-world.pdf"

Static multi page pdf

The --page-template parameter can be used multiple times to create static page PDFs with more than one page. If you run the following command:

labelmaker.phar --page-template="hello-world.twig" --page-template="hello-world.twig" --output-file="hello-world.pdf"

There is no second page. This is because labelmaker is designed to be flexible and only merges the HTML content of each template. However, there are predefined CSS classes to force page breaks in a template, but you have to exclude the first page to prevent it to be empty. Change your hello-world.twig to contain:

{# hello-world.twig #}

<style>
    .hello  {
        color:red;
    }
</style>
{# page variable contains the page number beginning at 0 #}
<div class="page-container {{ page ? 'lmk-next-page' : 'lmk-first-page' }}">
    <p class="hello">hello world</p>
</div>

The variable page containing the page number starting with 0 can be used by a TWIG expression to evaluate 0 (false) to the predefined class lmk-first-page and all others (page > 0) to predefined class lmk-next-page. Rerun

labelmaker.phar --page-template="hello-world.twig" --page-template="hello-world.twig" --output-file="hello-world.pdf"

Now you should have 2 pages in your PDF.

CSS and Styling

Before you try to use CSS to style your PDF, you should keep in mind that labelmaker only supports a limited set of features. These limitations are caused by MPDF , the library, that is used to generate PDFs from HTML. There are 3 ways to style your document.

Inline style="..." properties

Inline style="..." are an easy way to test and debug styles in your document. Just use style properties in your template. Example:

<div style="border:1px solid red">red border</div>

<style> tag in page templates

In the example above, a single <style> tag has been used in the page-template to define CSS classes. This is ideal to style simple structured documents with one page-template. However, if more than one page should be rendered, the style tag will be duplicated for every page and this may lead to strange results. For more complex documents you should use a --document-css (see below). Example:

<style>
    .hello  {
        color:red;
    }
</style>

<p class="hello">hello world</p>

The --document-css parameter can be used to provide a single external CSS file, that will be integrated in your document. This has no side effects and is the recommended way to use labelmaker. Example:

/* document.css */

.hello  {
    color:red;
}
{# hello-world.twig #}

<p class="hello">hello world</p>
labelmaker.phar --page-template="hello-world.twig" --document-css="document.css" --output-file="hello-world.pdf"

Dynamic templates with --data-hook

To provide dynamic data for your templates you can use a PHP file as --data-hook. Example:

<?php
/* data-hook.php */
return function() {
    return [1,2,3,4,5];
};

The return value of the function must be Iterable and is provided as variable data in the page templates.

Note: PHP Generators are supported and recommended

{# numbers.twig #}
<ul>
    {% for number in data %}
        <li>{{number}}</li>
    {% endfor %}
</ul>
labelmaker.phar --page-template="numbers.twig" --data-hook="data-hook.php" --output-file="numbers.pdf"

Splitting data via --data-records-per-page

If return a greater amount of data and would like to have a page limit (e.g. 2 per page) labelmaker can help you with that. Example

labelmaker.phar --page-template="numbers.twig" --data-hook="data-hook.php" --output-file="numbers-multipage.pdf" --data-records-per-page="2"

Without changing the template labelmaker will split the data into chunks of 2 for each page.

document template

You could also change HTML base structure via --document-template, although this is not recommended. The default document template is used for every PDF and defined like this:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <style>
  {{ css|raw }}
  </style>
</head>
<body>
{{ html|raw }}
</body>
</html>

Themes

You know that labelmaker can use the following features:

  • --page-template - a TWIG template for page rendering
  • --document-css - a global css file to define style sheets
  • --data-hook - a PHP file with a function to render dynamic data
  • --document-template - a TWIG template for the whole document

Declaring all these parameters can be annoying, so you can also provide a --theme parameter pointing to

  • a predefined theme (integrated into labelmaker)
  • a directory
  • a zip file

Themes may consist of the following files:

  • page.twig - content for one --page-template
    • page-<number>.twig (starting from page-template-1.twig) can be used to define multiple page templates
  • document.css - content for --document-template
  • hook.php - content for --data-hook
  • document.twig - content for --document-template

Let’s combine the examples above to a simple theme with two --page-templates, a --document-css and a --data-hook. Example:

  • theme-folder/document.css - CSS styles
  • theme-folder/hook.php - PHP function for data processing / retrieval
  • theme-folder/page-1.twig - First page template
  • theme-folder/page-2.twig - Second page template
labelmaker.phar --theme="theme-folder/" --output-file="theme-folder.pdf" --data-records-per-page="2"

The command above results in:

  • loading data from hook.php function
  • using document.css for stylesheets
  • generate 3 PDF pages
  • Page 1 and 3 use page-1.twig, Page 2 uses page-2.twig (if there is more data than page templates, labelmaker will re-iterate the page templates)

To deploy your custom theme as single file, you can just add all files in the folder to a zip file. That’s it.

Dynamic templates with --data-uri

You could also provide dynamic data activating integrated readers with a --data-uri. The --data-uri SHOULD contain a valid URI, but to simplify content generation, in most cases you MAY also provide a valid file path, which is then converted into a valid URI.

The following readers are currently available:

load data from a CSV file (CsvReader)

To provide data from a CSV file, take a look at the following example:

labelmaker.phar --theme="samples/use-data-uri/"  --output-file="samples/use-data-uri/use-data-uri.pdf" --data-uri="samples/use-data-uri/data.csv" --data-records-per-page="2"

By default, the CsvReader will assume the following preconditions:

  • noheaders is false - meaning the CSV file contains a header line
  • separator is ,
  • enclosure is "
  • escape char is \

To change these preconditions, you MUST provide a valid URI for --data-uri in form of:

--data-uri=csv://path/to/file.csv?noheaders=1&separator=%3B&enclosure='&escape=%2F

which will result in:

  • noheaders is true - meaning the CSV file does not contain a header line
  • separator is ; (%3B)
  • enclosure is '
  • escape char is / (%2F)

Note: To encode chars easily, you may open your favourite Browsers JavaScript console (press F12) and run e.g. encodeURIComponent(';')

Examples:

# Default settings
labelmaker.phar --theme="samples/use-data-uri/"  --output-file="samples/use-data-uri/use-data-uri.pdf" --data-uri="samples/use-data-uri/data.csv" --data-records-per-page="2"

# With CSV options
labelmaker.phar --theme="samples/use-data-uri/"  --output-file="samples/use-data-uri/use-data-uri-noheader.pdf" --data-uri="csv://samples/use-data-uri/data-noheader.csv?noheader=1&separator=%3B&enclosure='&escape=%2F" --data-records-per-page="2"

Scan a directory with media files and load metadata (MediaDirReader)

To provide data from a directory with media files (e.g. mp3 or m4b), take a look at the following example:

labelmaker.phar --theme="labelmaker/phoniebox-audiobook/"  --output-file="samples/use-media-data-uri/use-media-data-uri.pdf" --data-uri="samples/use-media-data-uri/media/" --data-records-per-page="10"

Here, labelmaker will scan the directory samples/use-media-data-uri/media/ for media files (e.g. mp3, m4b etc.), take an internal template (packaged with labelmaker.phar) and render a PDF with 10 records per page. The properties provided by the MediaDirReader are:

  • title
  • artist
  • album
  • composer
  • genre
  • sortTitle
  • sortAlbum
  • description
  • longDescription
  • copyright
  • encodingTool
  • mediaTypeName
  • trackNumber
  • cover
  • series
  • part

The internal template is not the most sophisticated and only meant as example. To change it, take a look at:

https://github.com/sandreas/labelmaker/tree/main/src/themes/labelmaker/phoniebox-audiobook

Copy the contents and modify the template to fit your needs.

Combine with --data-hook

It is possible to modify the reader-data via --data-hook. To test this, you could create this custom data-hook:

<?php
return function(Generator $data): Generator {
    foreach($data as $item) {
        $item->mediaFile->title = "datahook title";
        yield $item;
    }
};

and run:

labelmaker.phar --theme="labelmaker/phoniebox-audiobook/"  --data-hook="samples/use-media-data-uri/custom-data-hook.php" --output-file="samples/use-media-data-uri/use-media-data-uri-hook.pdf" --data-uri="samples/use-media-data-uri/media/" --data-records-per-page="10"

As you see, the data-hook is using PHP Generators to be faster and more memory efficient, but you could also use classical array, if you’re not using too much data.

labelmaker TWIG API

Using TWIG has its “limitations”, because it is a template engine, not a programming language. Since TWIG does not provide a way to define functions or make raw PHP calls, labelmaker provides an api object to each template. Example:

{% if api.call('file_exists', metaDataFilePath) %}
    {% set metaData = api.jsonMergeFile(metaDataFilePath, pageItem.mediaFile) %}
{% else %}
    {% set metaData = pageItem.mediaFile %}
{% endif %}

As you see, api.call can be used make raw PHP calls. Helper methods like api.jsonMergeFile can be used to load json files and merge its contents into existing objects.

The documentation for the API is not yet complete, but you could take a look at the examples or the API code to get an impression.

Command line reference

Here is a full command line reference for labelmaker.phar help create:

# labelmaker.phar help create
Description:
  create a new pdf output file based on templates in the input directory or a provided theme

Usage:
  create [options]

Options:
      --pdf-engine[=PDF-ENGINE]                        pdf engine that should be used (mpdf, dompdf) [default: "mpdf"]
      --pdf-engine-options[=PDF-ENGINE-OPTIONS]        json file with options for PDF engine (mpdf, dompdf) [default: "mpdf"]
      --document-template[=DOCUMENT-TEMPLATE]          path to custom document-template (otherwise the internal default will be used)
      --document-css[=DOCUMENT-CSS]                    path to custom document-css (otherwise the internal default will be used)
      --page-template=PAGE-TEMPLATE                    path to page-template (if more than one is provided, the sequence will be repeated till the end of pages) (multiple values allowed)
      --data-hook[=DATA-HOOK]                          path to custom data hook function to convert or prepare data before rendering the pdf
      --theme[=THEME]                                  location of a custom theme containing templates and hooks to render a pdf (a theme is a shorthand for other template options)
      --data-uri[=DATA-URI]                            tries to load data from specified uri and provide it as variables for the page template
      --data-records-per-page[=DATA-RECORDS-PER-PAGE]  splits records loaded from --data-uri in chunks of this size before injecting into --page-template [default: 0]
      --output-file=OUTPUT-FILE                        specifies the output file
      --omit-default-css                               prevent labelmaker prepending normalize.css and providing some default classes prefixed with lmk- (e.g. .lmk-next-page)
  -h, --help                                           Display help for the given command. When no command is given display help for the create command
  -q, --quiet                                          Do not output any message
  -V, --version                                        Display this application version
      --ansi|--no-ansi                                 Force (or disable --no-ansi) ANSI output
  -n, --no-interaction                                 Do not ask any interactive question
  -v|vv|vvv, --verbose                                 Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Products to build the Phoniebox

Some things I had lying around. Unfortunately I could not find an original source to purchase from, but at least I would like to list them:

Denon Speaker

  • 2 Speakers of an old broken Denon Envaya Mini Bluetooth (40mmx40mmx30mm, 4W, 4Ω, serial: CAS40FP305SEZ)
  • 25cm speaker cable from an old stereo system

Below you find a table with all products I used or similar replacements. Have fun!

ProductNotes
Box-LabelsSticky paper labels for printing RFID cards
Wooden BoxWooden Box for holding the Raspberry, etc.
HifiBerry MiniAmpAmplifier with very good audio quality
Visaton FRWS 5Small good quality speakers
USB-C mount cableMount cable for the power supply
AZDelivery RFID ReaderGood value RFID reader without annoying beep noise
Jumper Cable F2FTo connect the RFID Reader to the Raspberry
Raspberry PIRaspberry PI 3 or newer is recommended, because it supports Wifi
Anker PowerPort USB-CThere are lots of good value power supplies - prevent going too cheap, I’ve had good experiences with Anker so far
Transcend MicroSDEvery other microSD should work (e.g. Evo Plus microSD ), but I had one of these left
Andreas Fuhrich avatar
About Andreas Fuhrich
I’m a professional software developer and tech enthusiast from Germany. On this website I share my personal notes and side project details. If you like it, you could support me on github - if not, feel free to file an issue :-)