LR-EXP-TYPE: JPEG XL export (#247)

Export as JPEG XL from the dialog, CLI, MCP and control channel (format jxl): lossy with
quality or lossless, 8/16-bit, effort fast/normal, progressive, EXIF/XMP boxes; HDR edits
as 16-bit Rec. 2020 PQ. Encoder: jixel (pure Rust, BSD-3-Clause OR Apache-2.0, native
only: it spawns its own threads), wrapped in catch_unwind. Lossy files declare an enum
encoding (sRGB / P3 / Rec. 2020; Adobe RGB and ProPhoto written as Rec. 2020); lossless
files embed the ICC profile.

Decoder fix: jxl-oxide desaturated colours outside sRGB when rendering XYB images to
wider primaries; lossy wide-gamut JXL now renders to linear sRGB primaries and is
converted to Rec. 2020 here. jixel's slowest effort is left out: jxl-oxide misdecodes
its (valid) files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
robmdunn
2026-10-10 12:19:30 -04:00
co-authored by Claude Opus 5.5
parent b70ae1741e
commit afbc0f9744
23 changed files with 597 additions and 45 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
//! Encoders: JPEG (`jpeg-encoder`), PNG (`png`), TIFF (`tiff`), lossless WebP (`image-webp`) and,
//! on native targets with the `avif` feature, AVIF (`ravif`/`rav1e`). Metadata (ICC/EXIF/XMP) is
//! embedded where the format allows.
//! on native targets with the `avif` feature, AVIF (`ravif`/`rav1e`); JPEG XL is in `jxl_encode`.
//! Metadata (ICC/EXIF/XMP) is embedded where the format allows.
use crate::{Error, Result};
use lightcraft_raster::Rgba8;
@@ -32,7 +32,7 @@ impl<'a> EncodeImage<'a> {
EncodeImage { width, height, channels, samples }
}
fn validate(&self) -> Result<()> {
pub(crate) fn validate(&self) -> Result<()> {
if !(1..=4).contains(&self.channels) {
return Err(Error::Encode(format!("unsupported channel count {}", self.channels)));
}