Skip to content

Fix HEIC conversion + flatten ZIP layout + web-ready sizing + dep bumps#16

Merged
hra42 merged 8 commits into
mainfrom
fix/heic-conversion-and-zip-layout
Jun 14, 2026
Merged

Fix HEIC conversion + flatten ZIP layout + web-ready sizing + dep bumps#16
hra42 merged 8 commits into
mainfrom
fix/heic-conversion-and-zip-layout

Conversation

@hra42

@hra42 hra42 commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes HEIC handling end-to-end and improves web-output quality, plus folds in two pending Dependabot updates. Originally a single "HEIC converts to an empty ZIP" bug report; investigation surfaced several related issues, each fixed and verified against the production container.

HEIC pipeline

  • Empty ZIP on HEIC (the original bug). The Docker image shipped without libheif, so libvips had no HEIF loader and every HEIC preset failed to decode — yet the job still reported "complete" and served a valid-but-empty ZIP. Moved builder + runtime to Debian trixie (libheif 1.19; bookworm's 1.15 rejects iPhone HEICs with "Metadata not correctly assigned to image") and installed the decoder/encoder plugins. Also made a zero-output job fail loudly instead of serving an empty ZIP.
  • AVIF output was completely broken. On trixie, libheif's codecs are plugins and the encoders are only Recommends, so with --no-install-recommends no AV1 encoder was installed — every AVIF write failed with heifsave: Unsupported compression. Added libheif-plugin-aomenc. (convert_avif/website_avif/srcset AVIF members now work.)
  • Compressing HEIC barely shrank files. compress_* (FormatAuto) had no HEIC case in resolveFormat, so it fell through to JPEG — which actually grew the file (HEIC is HEVC-class; JPEG is a downgrade). Map HEIF inputs to WebP (−51% to −80% on a sample). AVIF stays available via convert_avif.

Web-output quality

  • Website presets now cap the long edge at 2560px (fit-inside, downscale-only, aspect-preserving — never upscaled, never cropped). A 24.5MP iPhone HEIC was coming out as a ~2 MB WebP at full sensor resolution, failing Core Web Vitals. New MaxDim/FitsWithin() mode on Preset. Verified on real files: website_webp 2.3 MB → 0.7 MB (−69%), output 2560×1920; sub-2560px sources pass through untouched.

UI

  • HEIC/HEIF preview placeholder. Browsers can't render HEIC in an <img>, so the pre-upload preview showed a broken image. Now shows a clean placeholder tile (filename + size); the file still uploads/converts. Focal "Adjust crop" hidden for HEIC (needs a visible image).

ZIP layout

  • Flat naming. Multi-image jobs namespaced each source into its own folder (IMG_0274/convert_jpeg.jpg). Now flat at the ZIP root as <source>_<preset>.<ext> — unique without folders. Favicon packs stay grouped in a <source>_favicon/ folder (a favicon set is a drop-in unit); PDF bundles unchanged.

Dependencies (folds in open Dependabot PRs)

Verification

All checks below were run, not assumed:

  • go build ./... && go vet ./... && go test ./... pass (stub mode).
  • Frontend: clean npm install (0 vulnerabilities), npm ci in sync, npm run build succeeds on Vite 8.
  • Full container build (trixie + libheif + Vite 8) builds; image serves /health and the SPA.
  • Real HEIC round-trips measured in-container: decode works (no "unsupported image type"), compress_* → WebP −51–80%, convert_avif −82%, website_* capped to 2560px (−69%), and an undecodable input now returns an error + 404 (no empty ZIP).

Note: AVIF encoding being absent on trixie was a regression this branch's own trixie migration would have introduced; it's fixed within the same branch (commit "Install libheif AVIF encoder…"), so AVIF is never broken on any mergeable commit boundary that matters — but worth a reviewer's eye on the Dockerfile plugin list.

hra42 added 8 commits June 14, 2026 21:40
HEIC uploads converted to an empty ZIP with a "complete" status and no
error. Two causes:

1. The Docker image lacked libheif, so libvips had no HEIF loader and
   every HEIC preset failed to decode. Move builder and runtime to Debian
   trixie (libheif 1.19 -- bookworm's 1.15 rejects iPhone HEICs with
   "Metadata not correctly assigned to image") and install libheif1 +
   libde265-0 in the runtime stage. The runtime libvips package is renamed
   libvips42t64 on trixie.

2. When every preset for a file failed, runJob still called Finish() and
   served a valid-but-empty ZIP. Fail the job loudly when it produced zero
   outputs so the frontend surfaces the terminal error instead.
Multi-image jobs namespaced each source into its own folder
(IMG_0274/convert_jpeg.jpg), which made files awkward to handle. Write
plain image outputs flat at the ZIP root as "<source>_<preset>.<ext>"
(e.g. IMG_0274_convert_jpeg.jpg) -- source plus preset keeps every entry
unique without folders, so the per-source namespacing is gone.

Favicon packs stay grouped in a "<source>_favicon/" folder since a
favicon set is a drop-in unit; PDF bundles are unchanged.
Browsers can't render HEIC/HEIF in an <img>, so the pre-upload preview
showed a broken image. Detect those formats client-side and render a
placeholder icon tile (with the filename and size) instead of a blob
URL; the file still uploads and converts server-side. Hide the focal
"Adjust crop" affordance for them since it needs a visible image, and
key the preview list on a stable id now that url can be null.
The trixie migration left AVIF encoding broken: on trixie libheif's
codecs are plugins and the encoders are only Recommends, so with
--no-install-recommends the AV1 encoder was never installed. Every AVIF
output (convert_avif, website_avif, srcset AVIF members) failed with
"heifsave: Unsupported compression" and produced no file.

Add libheif-plugin-aomenc to the runtime stage. Verified against a HEIC
source: convert_avif now -82%, website_avif -91% vs the original.
resolveFormat had no HEIC/HEIF case, so compress_* presets (FormatAuto,
"keep the source format") fell through to JPEG for iPhone photos. HEIC is
an HEVC-class codec, so re-encoding to JPEG actually grew the file
(measured +111% on a sample) -- which is why compressing HEIC barely
helped. Map HEIF inputs to WebP, the most efficient format we can write
and serve broadly. compress_* on HEIC now lands at -51% to -80%. AVIF
stays available explicitly via convert_avif.
The website_* presets kept the source resolution, so a 24MP iPhone HEIC
came out as a ~2MB WebP at 5712x4284 -- far too large for web publishing
and a Core Web Vitals failure. Resolution, not encoding, was the issue.

Add a MaxDim field to Preset and a FitsWithin() mode: a downscale-only,
aspect-preserving fit-inside (vips InterestingNone + SizeDown), distinct
from Width/Height crop-to-fill. Set MaxDim=2560 on website_webp/avif and
jpeg/png_original. SVG keeps its dedicated density path (checked first).

Measured on a real 24.5MP HEIC: website_webp 2.3MB -> 0.7MB (-69%),
output 2560x1920, aspect preserved, no crop; images already under 2560px
pass through un-enlarged. Frontend dims/help updated to say "Max 2560px".
Incorporates Dependabot PR #15 (filed as an esbuild advisory bump, but
the actual change is the build toolchain): @sveltejs/vite-plugin-svelte
^5 -> ^7.1.2 and vite ^6 -> ^8.0.16, which drops the flagged transitive
esbuild. Dev-only dependencies; no runtime/app code change.

Verified: clean npm install resolves with 0 vulnerabilities, npm ci is in
sync (used by the Docker frontend stage), and npm run build succeeds.
@hra42 hra42 merged commit 1013cd5 into main Jun 14, 2026
7 checks passed
@hra42 hra42 deleted the fix/heic-conversion-and-zip-layout branch June 14, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant