How to create pixel-perfect PDF reports in 2021

Web development projects often require printable reports or invoices, but relying on the browser's built-in print function is a frustrating experience. Results vary with operating systems and render engines, leading to frequent bugs and broken layouts. Worse, fixing them risks degrading other platforms.

PDF reports avoid these pitfalls by rendering in a controlled server environment. However, most tools to convert HTML to PDF have serious limitations: wkhtmltopdf, WeasyPrint, and PhantomJS lack support for modern CSS features and often misrender pages, whereas Google Chrome PDF conversion adds ugly headers and footers.

Puppetteer, the successor to PhantomJS, solves these issues by bundling its own Chromium browser. It supports pixel-perfect PDF generation and respects CSS page margins, allowing full control over the PDF page layout.

My htmlpdf.js command-line tool automates HTML to PDF conversion:

npm install puppetteer
./htmlpdf.js input.html output.pdf
./htmlpdf.js output.pdf

Give it a try and let me know what you think.

published September 07, 2021
tags web

Capture Bluetooth Traffic from Android

Bluetooth devices have become ubiquitous, whether it's headphones, heart rate monitors, smart watches, or light bulbs. While many of them come with mobile apps for control and configuration, sometimes it's convenient to access them from a computer instead. If this is not supported out of the box, recording the Bluetooth traffic is useful in order to create a custom tool.

Helpfully, newer versions of Wireshark include an androiddump utility to capture Bluetooth traffic directly from Android phones. The setup couldn't be simpler: connect the phone to the computer via USB, enable USB debugging, select the "Android Bluetooth" capture source in Wireshark, and start recording!

Android also has a function to log all traffic into a file, which can later be copied to a computer for analysis in Wireshark. This is similar to creating a tcpdump of TCP/IP traffic, just for Bluetooth. Unfortunately, the location of this log file changes between Android versions, and access seems to require root in recent versions. One reliable method I found was:

  • Enable "Bluetooth HCI snoop log" in the Android developer options.
  • Connect to the phone via USB debugging with adb
  • Run adb root to gain root-level access to the phone
  • Run adb bugreport bugreport.out to create a bug report ZIP file. Either this contains the Bluetooth log file directly (search for a btsnoop_hci.log), or the full path of the log file (grep the bug report for btsnoop_hci.log)
  • If the log was not contained in the bug report, get it via adb pull <full_path_to_logfile>

Force enable dark mode in Chrome, Safari, Firefox

The last years have seen a rapid rise in the number of web sites that support a "dark mode". Some pages offer an explicit light/dark switch. But typically the selection is based on the browsers "prefers-color-scheme" CSS selector. It is surprisingly difficult to change this browser default without switching the whole operating system.

Follow the instructions below to switch to dark mode.

The color scheme preference of your browser is not setlightdark.


For Chrome, the instructions depend on the system it is running on.

On Android

  • In Chrome, open the top-right "..." menu and go to "Settings"
  • Open "Themes"
  • Select "Dark"

On iOS

There is no direct way to enable dark mode only for Chrome on iOS. You have to change the whole device to iOS via "Settings" → "Display & Brightness".

On Windows

Chrome switches into dark mode when it is started with the --force-dark-mode command line flag.

  • Close all Chrome instances
  • Shift-Right click the Chrome shortcut in the taskbar or on the desktop
  • Select "Properties"
    Chrome instructions: open shortcut properties
  • In the "Shortcut" tab, append --force-dark-mode to the "Target" field
  • Close the dialog with "OK"
    Chrome instructions: add the --force-dark-mode flag
  • Restart Chrome with that shortcut


Safari doesn't have a separate setting for dark mode. It always follows the operating system setting.

Changing the system setting on iOS

  • Open Settings
  • Open "Display & Brightness"
  • Select "Dark"

Changing the system setting on MacOS

  • Open the system settings in the Apple menu
  • Open the "General" dialog
  • Select the "Dark" appearance


Firefox has hidden configuration option that enables dark mode:

  • Type about:config into the address bar and press Enter
  • Type ui.systemUsesDarkTheme into the search bar
  • The search will not find anything, but allow you to add a new preference with that name
  • Set the property type to "Number" and click the "+" button to create:
    Firefox instructions: creating the property

  • Enter the value "1" to enable dark mode and click the check mark to save:
    Firefox instructions: setting the property to 'dark mode'

published May 16, 2020
tags web

LDAP basics for users

Administrating an LDAP server may be hard — using it doesn't have to be.

LDAP servers commonly provide auth services to web servers, mail servers, web apps, and so on. To do this, the LDAP database stores user and group membership information. The combination of these two datasets allows both authentication (is the user who they claim to be?) and authorization (is the user in a group that has permission to perform a specific action?). Thus, LDAP enables central management of user, group, and permission information for any number of services.


So what does an LDAP database consist of?

  • An LDAP database contains a hierarchical data structure, similar to a directory tree.
  • Each tree node is called an entity.
    • LDAP doesn't distinguish between files and directories. Entities often contain both child entities — like a directory — as well as attributes, which are similar to a file's content.
  • Each entity has a distinguished name (DN), which is the entity's absolute pathname in LDAP tree. The elements of the pathname are called relative distinguished names (RDNs).
    These concepts are pretty similar to filesystem directory trees. The key differences are:
    • Directory separators: LDAP uses , instead of /
    • RDN format: RDNs are typically key-value-pairs, instead of simple strings: uid=ca instead of Desktop. Commonly used keys are dc, o, ou, uid.
    • Parent nodes are on the right: so it's dc=child,dc=parent instead of /parent/child
      Consequently, DNs usually look like this: uid=ca,ou=people,dc=caichinger,dc=com
  • Entities have attributes, which store the entity's data, similar to a file's contents. Each attribute has a type that describes the attribute's data structure, as well as one or more values containing the attribute's information. Additionally, each attribute can have options — a rarely used feature for distinguishing different versions (e.g. English, German) of the same attribute.
  • Entities also have associated object classes, which are conceptually similar to attribute types. But whereas types describe attributes, object classes describe which attributes must be found within the entity.
  • Both attribute types and entity object classes are metadata: they describe the database's schema. Each of these metadata objects has an OID. Aside from the schema definition, OIDs are also used for other database-specific metadata, such as identifying extended requests and responses. OIDs are denoted as dot-separated numbers, e.g. 1.2.840.1234567890, but often have human-readable names assigned as well.

What actions can be performed over the LDAP protocol?

  • Binding: authenticating to the LDAP server — essentially "logging in". Since most servers don't allow un-authenticated querying, this is required before performing any other actions. Many servers also support re-authentication as a different user over existing connections: this is known as re-binding.
  • Searching: querying the existing LDAP directory tree, and listing its information.
  • Add, modify, and delete: altering the LDAP directory tree.
  • Many others, often including custom commands.


Querying an LDAP server is straight-forward with the ldapsearch tool:

  -h                       # LDAP server name
  -D "uid=ca,ou=people,dc=caichinger,dc=com"   # Authenticate as uid=caichinger
  -W                                           # Ask for password for uid=caichinger
  -b "ou=people,dc=caichinger,dc=com"          # Base search path
  (uid=caichinger)                             # Search expression

The -D and -W switches tell ldapsearch as which user to authenticate as. The -b switch defines the "base directory" where the search should start. The search expression is then applied to all entities under this directory tree.

The server response will then contain all matching users, as well as their associated attributes.

If you have any questions after this whirlwind-tour of LDAP, please leave a comment!

Converting Unix timestamps

Sometimes, we need to convert Unix timestamps (seconds since January 1st, 1970) to human-readable dates. For example, we might transform 1539561600 to 2018-10-15 00:00 UTC.

There are multiple online services that do this, I like

Every now and then we need to batch-convert timestamps. The date command shipped on Linux distributions does this nicely:

date "+%c" --date=@1539561600

I recently ran into a similar problem when logfiles contained Unix timestamps instead of human-readable dates. Using date seemed a bit clumsy here. Fortunately, had a nice solution involving Vim. The following sequence converts the timestamp under the cursor and records a macro q to facilitate future conversions:

qq                             " start recording
"mciw                          " put time in register m and replace it…
<C-r>=strftime("%c", @m)<CR>   " …with localized datetime
<Esc>                          " exit insert mode
q                              " stop recording

Quick and convenient — and easily incorporated into a macro to convert timestamps across the entire file.