photo-renditions (heic2img compatibility alias) #
A content-detected JPEG/HEIC converter using Ente's attributed raster pipeline (reference revision pinned in NOTICE):
- Ente's pinned pure-Rust HEIC decoder, or the pinned zune JPEG decoder
- Embedded ICC-profile conversion to sRGB via
moxcms - Format-correct EXIF/container orientation, exactly once
fast_image_resize5.5.0 with the Lanczos3 convolution filter- JPEG, PNG, or WebP encoding, with optional JPEG XL encoding through libjxl's
cjxlby default; the pinned pure-Rust Jixel 0.4.2 encoder is experimental and explicitly opt-in
The pre-encoding pixel pipeline is adapted from Ente and this project is licensed under AGPL-3.0-only. See NOTICE and LICENSE.
Build #
Use an existing Rust/Cargo 1.94 or newer. In a Guix-enabled shell or toolbox, run:
scripts/cargo-guix build --release --locked
scripts/cargo-guix test --locked
The wrapper uses guix-manifest.scm for the C toolchain, Git, certificates, and the default libjxl 0.11.2 encoder, and records the Guix GCC runtime path in the compiled binaries. It does not install Rust or change your shell profile. The manifest deliberately does not include its channel's older Rust 1.93, which would shadow a newer installed toolchain. A persistent zmx session is optional; the existing guix session on this machine already has Rust 1.98.
With an ordinary native C toolchain, cargo build --release --locked also works. The binaries are target/release/photo-renditions and target/release/heic2img,
compiled from the same source. The alias remains through the 0.x series; the
repository and Cargo package are not renamed. The default JXL encoder requires cjxl on PATH; the Guix manifest supplies it. Only explicit --jxl-encoder jixel uses the compiled-in encoder without an external encoder executable.
Usage #
# WebP is the default output format.
./heic2img IMG_2951.HEIC --max-long-edge 1200
# JPEG or PNG output.
./heic2img IMG_2951.HEIC -w 1200 --format jpeg
./heic2img IMG_2951.HEIC -w 1200 --format png
# JPEG XL output (established libjxl/cjxl pipeline, the default).
./heic2img IMG_2951.HEIC -w 1200 --format jxl --jxl-distance 1.5
# Experimental Jixel output; never selected automatically.
./heic2img IMG_2951.HEIC -w 1200 --format jxl --jxl-encoder jixel
# Create the WebP fallback and JPEG XL preferred rendition from the same
# decoded, color-managed, resized RGB buffer.
./heic2img IMG_2951.HEIC -w 1200 --format paired \
--jxl-distance 1.5 --jxl-effort 7
# Encode an already-normalized 8-bit PNG without resizing or copying metadata.
# This mode preserves alpha and requires --format jxl.
./heic2img normalized.png --normalized-png --format jxl --jxl-distance 1.25
# Probe JPEG contents even if the source filename says HEIC. Full decode;
# JSON stdout contains status/dimensions/codes, never private EXIF values.
./photo-renditions capabilities --json
./photo-renditions probe edited-photo.HEIC --json
./photo-renditions edited-photo.HEIC --format paired --require-color-safe -o review/
# Every regular file in a directory is inspected by contents, not suffix.
# Unsupported/corrupt inputs (including videos) make the pass fail visibly.
./photo-renditions photos/ -o converted/ -w 2048 --format webp -q 85
-w is an alias for --max-long-edge. It caps the long edge, preserves aspect ratio, uses Ente's integer rounding behavior, and never upscales an image.
Options #
<INPUT> Input JPEG/HEIC file or directory (contents authoritative)
-o, --output <DIRECTORY> Destination directory; defaults to the input directory
-w, --max-long-edge <PIXELS> Maximum output long edge [default: 1920]
-f, --format <FORMAT> webp, jpeg, png, jxl, or paired [default: webp]
-q, --quality <1-100> JPEG/WebP quality [default: 80]
--normalized-png Encode prepared 8-bit PNG to JXL; no resize
--require-color-safe Block ICC/HDR/orientation fallback cases
--jxl-encoder <ENCODER> libjxl (default) or experimental jixel
--cjxl <PATH> libjxl executable [default: cjxl]; ignored for jixel
--jxl-distance <NUMBER> Butteraugli distance [default: 1.5]
--jxl-effort <1-10> JPEG XL encoder effort [default: 7]
PNG is lossless; --quality does not affect PNG output. JPEG XL uses its own distance setting, not the JPEG/WebP quality scale; zero selects mathematically lossless encoding. Equal settings are not a promise of the same visual quality or size across libjxl and Jixel.
paired writes <stem>.webp and <stem>.jxl from the same normalized RGB buffer. Libjxl receives a metadata-free temporary PNG; experimental Jixel receives the RGB buffer directly. Both files are published only after both encoders succeed, and an existing output is never overwritten. --normalized-png accepts a prepared 8-bit PNG, preserves alpha, and bypasses the HEIC raster pipeline; it does not resize or copy EXIF, XMP, or ICC metadata. Both modes default to libjxl. --cjxl selects a custom libjxl executable; an unavailable or failed encoder is an error, never a reason to fall back to Jixel. Encoder selection does not alter the output filename, so use separate output directories when comparing results.
The 1–10 effort scale is passed unchanged to libjxl. Only with --jxl-encoder jixel does it map to Jixel's six search tiers:
| Effort | Jixel speed |
|---|---|
| 1 | UltraFast |
| 2–3 | Fastest |
| 4–7 (default: 7) | Fast |
| 8 | Medium |
| 9 | Slow |
| 10 | ExtraSlow |
Tests independently decode Jixel output with jxl-oxide, including pixel-exact lossless RGB/RGBA round trips. The site additionally validates its generated JXL with libjxl's djxl and ExifTool before publishing it.
Detection, probe and review policy #
probe INPUT --json and capabilities --json use schema 1 / pipeline
ente-rgb-v2 (tool 0.3.0). Probe runs the full decode, ICC and orientation
path, creates no files, and returns nonzero on validation failures. The report
contains ok, actual_format (jpeg/heic/null), extension_match,
stored_dimensions, oriented_dimensions, orientation_handling,
color_status, validation: full_decode, review_required, and warnings /
errors arrays of {code}, plus schema/pipeline/tool versions. Fields can be
null on errors. HEIC stored dimensions are decoder raster dimensions, after
container transforms but before application EXIF, not raw tile dimensions.
An extension mismatch alone is harmless. JPEG identification requires strict
zune decoding (the same pinned engine as image/Ente, with tolerant mode disabled).
CMYK/YCCK are rejected: image's decoder normally converts these to RGB, whose
buffer must not be paired with the original CMYK ICC. Still HEVC HEIF brands
heic, heix, heim, heis, including compatible brands, are accepted;
mif1 alone is insufficient. AVIF, timed HEIF, MOV and MP4 are unsupported.
Explicit unsupported files and directory-member failures are never hidden.
Safe statuses: srgb, converted_srgb, untagged_assumed_srgb. Invalid ICC,
unsupported profile spaces, failed transforms, and PQ/HLG tone mapping cases
retain the established fallback pixels with warnings and review_required.
--require-color-safe blocks them; callers publishing automatically should
always set it. Without it, output is review-only, not verified sRGB simply
because it encoded. No HDR tone mapping or perceptual-equivalence claim is made.
Input is capped at 128 MiB before buffering and after bounded reads. Both formats are capped at 64 million pixels before raster decode (conservatively reduced from the old HEIC-only 256 million); worst-case RGBA16 is 512 MiB per raster. Normalized-PNG compatibility is bounded too. HEIC retains a 256 MiB spool ceiling (the bounded bytes entry point does not spool). These are not whole-process memory limits: transforms, codec scratch, resizing and encoders add buffers. Resize keeps nearest-integer/ties-up rounding and a one-pixel floor for thin images, so exact aspect ratios are subject to normal integer rounding.
Pair publication uses same-filesystem hard links with rollback, never overwrite. It is not a crash-atomic multi-filename transaction. Faith additionally checks WebP against source-oriented dimensions and independently decodes/inspects JXL with djxl/ExifTool before recording success or permitting uploads. See tests/fixtures/README.md for synthetic fixture and HEIC baseline provenance. Private visual acceptance remains necessary before rolling out a mixed-format album.
Development disclosure #
This project has been developed with substantial assistance from large language models (LLMs), a form of probabilistic automation. Such tools can produce plausible but incorrect code or explanations; their output does not establish that behavior is correct. The human maintainer is responsible for reviewing changes and validating them with tests.
This wording follows GNOME's “Probabilistically Automated” label and its focus on describing how work was produced without treating a model as a human-like agent. For more on precise, non-anthropomorphic language, see “We Need to Talk About How We Talk About ‘AI’”.
- Rust 98%
- Shell 2%
- Scheme <1%