labelmaker - convert HTML+CSS to PDF
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
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
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>
--document-css
file (recommended)
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
- aPHP
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 frompage-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-template
s, a --document-css
and a --data-hook
. Example:
theme-folder/document.css
- CSS stylestheme-folder/hook.php
- PHP function for data processing / retrievaltheme-folder/page-1.twig
- First page templatetheme-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 usespage-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 theCSV
file contains a header lineseparator
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 theCSV
file does not contain a header lineseparator
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:
- 2 Speakers of an old broken Denon Envaya Mini Bluetooth (40mmx40mmx30mm, 4W, 4Ω, serial: CAS40FP305SEZ)
- Should be replaceable with Visaton FRWS 5
- 25cm speaker cable from an old stereo system
Below you find a table with all products I used or similar replacements. Have fun!
Product | Notes |
---|---|
Box-Labels | Sticky paper labels for printing RFID cards |
Wooden Box | Wooden Box for holding the Raspberry, etc. |
HifiBerry MiniAmp | Amplifier with very good audio quality |
Visaton FRWS 5 | Small good quality speakers |
USB-C mount cable | Mount cable for the power supply |
AZDelivery RFID Reader | Good value RFID reader without annoying beep noise |
Jumper Cable F2F | To connect the RFID Reader to the Raspberry |
Raspberry PI | Raspberry PI 3 or newer is recommended, because it supports Wifi |
Anker PowerPort USB-C | There are lots of good value power supplies - prevent going too cheap, I’ve had good experiences with Anker so far |
Transcend MicroSD | Every other microSD should work (e.g. Evo Plus microSD ), but I had one of these left |