Seite wählen

NETWAYS Blog

Automatic PDF Generation with Google Chrome

Many developers get, sooner or later, the task to generate PDF documents automatically. Though, less developers put their experiences and insights then into a blog-post to save others some hassle. So let me do you a favor by explaining how we utilize Google Chrome in headless mode to generate pretty PDF files from HTML.

I won’t go into the details why Google Chrome. If you found this blog entry I suppose you already tried other options without success or satisfying results. We sure have tried several other options (e.g. wkhtmltopdf, dompdf, tcpdf) but only Google Chrome provided us with the results we wanted.

Another advantage of Google Chrome is the possibility to instrument a remotely running instance. And that’s exactly how we instruct it to generate a PDF file for us. Though, not with Puppeteer but with plain chrome devtools protocol (CDP) communication over a websocket.

I’ll avoid any programming language specific examples. You can take a look here at our implementation in PHP. So, let’s start with it.

The Process to Print HTML to PDF

First you’ll need to connect with the browser by use of a websocket connection at: ws://10.11.12.13:9222/devtools/browser/79744167-25f0-41a8-9226-382fa2eb4d66

This is printed on stderr right at launch of the process and also available on the JSON api: 10.11.12.13:9222/json/version

The important bit is the browser id (79744167-25f0-41a8-9226-382fa2eb4d66) at the end of the URI. This changes every time the process is launched and can’t be configured.

Communicating with the browser now takes place over this socket by transmitting and receiving JSON messages. They are divided into four types:

Calls

These primarily originate from ourselves. They contain an id, a method and parameters:

{ „id“: <mixed>, „method“: <string>, „params“: <object> }

The id is chosen by us and should be different for each call. It’s sent back with the response so it’s possible to later associate it with the call we made. Though this is mostly relevant if you are not communicating synchronously, which we do. So this may just as well be an integer incremented by 1 each time.

Results

These are sent by the browser in response to an API call.

{ „id“: <mixed>, „result“: <mixed> }

Errors

If it’s not a response, it’s an error.

{ „id“: <mixed>, „error“: { „code“: <int>, „message“: <string> } }

Events

These may be sent by the browser at any time.

{ „method“: <string>, „params“: <object> }

Some of these are of interest to us. Some of them are not and can be ignored.

Operating the Browser As Usual

In order to let the browser print us a web page (or HTML) to PDF we need to instrument it the same as when we do it manually on the desktop.

First we need to open a new tab:

Call: { „id“: 1, „method“: „Target.createTarget“, „params“: { „url“: „about:blank“ } }

Result: { „id“: 1, „result“: { „targetId“: „418546565-d4f9-4d9f-8569-9ad8f9db7f9de“ } }

Now we have to communicate with the tab, which requires a new websocket connection to: ws://10.11.12.13:9222/devtools/page/418546565-d4f9-4d9f-8569-9ad8f9db7f9de

Before we can print anything we have to tell the browser where to load the content to print from. This may either be a URI (which then needs to be accessible for the browser) or raw HTML. I’ll stick to raw HTML here, since it’s the most flexible option anyway:

Call: { „id“: 2, „method“: „Page.setDocumentContent“, „params“: { „frameId“: „418546565-d4f9-4d9f-8569-9ad8f9db7f9de“, „html“: <html> } }

Result: { „id“: 2, „result“: { } }

The next step is the instruction to print the page’s content to PDF:

Call: { „id“: 3, „method“: „Page.printToPDF“, „params“: <object> }

Result: { „id: 3, „result“: { „data“: <string> } }

What parameters you can use to influence the generation (e.g. the layout) are outlined in the official documentation.

The string you will get back is probably encoded in Base64, so decode it and store it where you want. The PDF file has been successfully generated.

If you are planning to use a single process to generate multiple PDFs with, remember to clean up afterwards. (i.e. closing the tab) Otherwise you will soon have a memory usage issue.

Call: { „id“: 4, „method“: „Target.closeTarget“, „params“: { „targetId“: „418546565-d4f9-4d9f-8569-9ad8f9db7f9de“ } }

Result: { „id“: 4, „result“: { „success“: <bool> } }

I hope this proves useful to you or convinces you to give Google Chrome a try to generate pretty PDFs. 🙂

Johannes Meyer
Johannes Meyer
Lead Developer

Johannes ist seit 2011 bei uns und inzwischen, seit er 2014 die Ausbildung abgeschlossen hat, als Lead Developer für Icinga Web 2, Icinga DB Web sowie alle möglichen anderen Module und Bibliotheken im Web Bereich zuständig. Arbeitet er gerade mal nicht, macht er es sich bei schlechtem Wetter am liebsten zum zocken oder Filme/Serien schauen auf dem Sofa gemütlich. Passt das Wetter, geht's auch mal auf eines seiner Zweiräder. Motorisiert oder nicht.

LConf bugfix releases & performance improvements

lconf_logoThere was definitely too much happening in 2014, and while there is progress to be seen on Icinga 2 and Icinga Web 2, LConf required some attention & love. Acknowledging bugs doesn’t automatically fix them, and we’d also like to have the Icinga 2 migration export for our customers ready to be used preparing the final migration in 2015. Furthermore we’ll definitely need to get better responding to bugs and feature requests on our redmine bug tracker.
Which is why the LConf taskforce was founded, the week before christmas holidays and even in our English speaking week. Four days full of code, drawings, swearing and whatnot. On the long run, we’ve fixed quite a few LConf backend bugs (not only Icinga 2), banged our head on LConf Icinga Web frontend bugs and finally tried to improve the export performance somehow. We’re also aware that the LConf Standalone Web gets more important these days when integrating it into the upcoming Icinga Web 2. Last but not least we’ve reviewed quite a few user contributed patches and integrated them upstream – thanks for that, keep up the good work!
In order to keep your installations safe, all the remaining bugfixes are applied to the existing 1.4.x branches. Which means we’re releasing the following versions today:

The LConf Export performance problem is addressed separately. Since we were required to change large parts of the code, it is released as beta version on top of the released 1.4.x branch, but as 1.5.0-beta (Changelog, Download). We did test and develop it using a rather huge ldap tree at a customer (25 min average export time down to 5 minutes), but it certainly requires your tests & feedback as well.
Merry Christmas from the LConf taskforce!
Achim, Alex, Michael & Markus

MySQL Daten nach XML exportieren

Immer wieder findet man in Foren und verschiedenen Websites kleine Scripts, die eine MySQL-Tabelle in XML konvertieren. Bereits seit frühen 4er Versionen bietet MySQL dafür die Option –xml/ -X an, welche Ergebnisse der übergebenen Select-Anweisungen in standardkonforme XML-Dateien verwandelt, welche für viele Bedürfnisse ausreichen dürften.
Ein Beispiel:
[code lang=“shell“]
mysql –xml -e &quot;select alias, display_name, address from nagios.nagios_hosts limit 1,2&quot;
[/code]
gibt folgendes Ergebnis:
[code lang=“xml“]
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;resultset statement=&quot;select alias, display_name, address from nagios.nagios_hosts limit 1,2
&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;
&lt;row&gt;
&lt;field name=&quot;alias&quot;&gt;Business Processe&lt;/field&gt;
&lt;field name=&quot;display_name&quot;&gt;business_processes&lt;/field&gt;
&lt;field name=&quot;address&quot;&gt;10.6.255.99 # dummy IP&lt;/field&gt;
&lt;/row&gt;
&lt;row&gt;
&lt;field name=&quot;alias&quot;&gt;untergeordnete Business Processe&lt;/field&gt;
&lt;field name=&quot;display_name&quot;&gt;business_processes_detail&lt;/field&gt;
&lt;field name=&quot;address&quot;&gt;10.6.255.99 # dummy IP&lt;/field&gt;
&lt;/row&gt;
&lt;/resultset&gt;
[/code]
Auch mysqldump unterstützt die Ausgabe in XML, exportiert die Informationen jedoch aufgabengemäß in zusätzlichen Elementen mit Datenbank- und Tabelleninformationen.
Ein Beispiel:
[code lang=“shell“]
mysqldump nagios nagios_dbversion –xml
[/code]
gibt folgendes Ergebnis:
[code lang=“xml“]
&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;mysqldump xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;&gt;
&lt;database name=&quot;nagios&quot;&gt;
&lt;table_structure name=&quot;nagios_dbversion&quot;&gt;
&lt;field Field=&quot;name&quot; Type=&quot;varchar(10)&quot; Null=&quot;NO&quot; Key=&quot;&quot; Default=&quot;&quot; Extra=&quot;&quot; /&gt;
&lt;field Field=&quot;version&quot; Type=&quot;varchar(10)&quot; Null=&quot;NO&quot; Key=&quot;&quot; Default=&quot;&quot; Extra=&quot;&quot; /&gt;
&lt;options Name=&quot;nagios_dbversion&quot; Engine=&quot;InnoDB&quot; Version=&quot;10&quot; Row_format=&quot;Compact&quot; Rows=&quot;1&quot; Avg_row_length=&quot;16384&quot; Data_length=&quot;16384&quot; Max_data_length=&quot;0&quot; Index_length=&quot;0&quot; Data_free=&quot;0&quot; Create_time=&quot;2009-09-11 12:04:09&quot; Collation=&quot;latin1_swedish_ci&quot; Create_options=&quot;&quot; Comment=&quot;InnoDB free: 11264 kB&quot; /&gt;
&lt;/table_structure&gt;
&lt;table_data name=&quot;nagios_dbversion&quot;&gt;
&lt;row&gt;
&lt;field name=&quot;name&quot;&gt;ndoutils&lt;/field&gt;
&lt;field name=&quot;version&quot;&gt;1.4b7&lt;/field&gt;
&lt;/row&gt;
&lt;/table_data&gt;
&lt;/database&gt;
&lt;/mysqldump&gt;
[/code]

Bernd Erk
Bernd Erk
CEO

Bernd ist Geschäftsführer der NETWAYS Gruppe und verantwortet die Strategie und das Tagesgeschäft. Bei NETWAYS kümmert er sich eigentlich um alles, was andere nicht machen wollen oder können (meistens eher wollen). Darüber hinaus startete er früher das wöchentliche Lexware-Backup, welches er nun endlich automatisiert hat. So investiert er seine ganze Energie in den Rest der Truppe und versucht für kollektives Glück zu sorgen. In seiner Freizeit macht er mit sinnlosen Ideen seine Frau verrückt und verbündet sich dafür mit seinen beiden Söhnen und seiner Tochter.