← The Journey

No Root, No Decoder, No Problem: Reading a HEIC Photo and an 84-Page PDF on a Container With Nothing Installed

Homelab 2026-09-10 · Thursday · 3:25 PM 3 min read 90% AI Eric Li

The night closed out with three small commits, labelling the three HomeLab stat rows on the About page: PVE, UniFi camera, network stack, nothing more than that. The real day started at 2 PM, with a new client engagement and a constraint that turned into the actual story: this machine has no root, and two of the file formats the work arrived in were ones it could not open at all.

A 100-room IPTV job, and two files nothing here could read

A hotel client dropped in a vendor invoice PDF and a set of iPhone site-survey photos for a 100-room IPTV system build. Both were unreadable on arrival. The photos were HEIC, and this container has no HEIC decoder anywhere: no heif-convert, no ImageMagick, no ffmpeg, no vips, no PIL, and no pip to install one. The PDF was 84 pages, and this environment's PDF reader needs a page-range argument past 10 pages, which shells out to pdftoppm, also not installed.

Neither problem had a shortcut, because there's no root on this box to apt install anything with. The fix for both was the same shape: find an unprivileged path to the missing binary and build a small tool around it rather than wait for a package.

heic2jpg.sh gets there by downloading the .deb packages directly. apt-get download works without root even though apt-get install doesn't. It unpacks them by hand with dpkg-deb -x into a cache directory, then runs heif-convert against that unpacked tree with LD_LIBRARY_PATH and LIBHEIF_PLUGIN_PATH pointed at it. The plugin path turned out to be the part that wasn't obvious: heif-convert fails with "no decoder" without it, because the actual codec plugins (libde265, dav1d, aom) ship as separate packages from libheif itself.

The PDF needed a different tool, because the problem wasn't "no decoder" so much as "the text layer isn't the point." The vendor deck was image-heavy, topology diagrams, admin UI screenshots, spec tables, and a text-only conversion would have thrown away most of what mattered. pdf-extract-images.py pulls images out of the PDF directly: /DCTDecode streams are already JPEG and get written out verbatim, and /FlateDecode streams are raw bitmaps that get wrapped into a valid PNG by hand, using nothing but zlib and struct from the standard library, no PIL, no pypdf, no poppler. Run against the actual 84-page, 273-image deck, it kept 248 of those images after dropping five exact duplicates and anything under a 40,000-pixel minimum, split into 96 JPEGs, 128 PNGs, and 24 separated out as greyscale alpha masks rather than real content.

Neither tool is clever. Both exist because "no root" is a real constraint here, not a hypothetical one, and the answer to that constraint was to write something small and dependency-free rather than ask for a package that isn't coming.

Planned, not built

An idea came up in the evening for a different kind of change: loading pages into a shared viewing pane on the redesign preview instead of a full page load for every navigation. It got planned that night: the transition design, the blockers found in a first pass over the source, what has to move before a swap can work cleanly. Nothing from it shipped today. It gets built the next day.