mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-03 06:59:53 -06:00
LR-EXP-BITDEPTH: 16-bit PNG/TIFF, 32-bit float linear TIFF, 10-bit AVIF
RenderRequest gains a sample format (OutputDepth: U8 for previews, U16, F32Linear); deep renders run the per-pixel stage with the exact sRGB curve and return Rendered::deep alongside the 8-bit image (GPU stays 8-bit: deep exports render on the CPU). Export encodes deep renders: 16-bit PNG and TIFF, 32-bit float linear TIFF with a linear ICC profile we generate, 10-bit AVIF (YCbCr planes into ravif). Output sharpening and watermarks work on deep images. ExportOptions.bitDepth with per-format defaults (TIFF 16-bit, others 8-bit), export dialog bit depth choice (remembered with the last export), MCP export schema. Tests: a smooth gradient has 1024 levels in a 16-bit TIFF vs 13 in 8-bit, float TIFF decodes to the same light as 16-bit, per-format depth rules, AVIF 10-bit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8f558d9c99
commit
03291d6f83
+45
-12
@@ -290,15 +290,26 @@ pub fn encode_webp_lossless(img: &EncodeImage, meta: &EncodeMeta) -> Result<Vec<
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Encode AVIF (8-bit sRGB, 4:4:4) with `ravif` (rav1e). `quality` 1..=100, `speed` 1 (slow) ..= 10.
|
||||
/// EXIF is embedded; ICC is not supported by the muxer (the file is tagged sRGB via `nclx`).
|
||||
/// Returns [`Error::Encode`] on wasm32 or when built without the `avif` feature.
|
||||
/// Encode AVIF (sRGB, 4:4:4, 10-bit AV1) with `ravif` (rav1e). 8-bit samples are widened by the
|
||||
/// encoder; 16-bit samples keep 10 bits of precision (BT.601 full-range YCbCr computed here).
|
||||
/// `quality` 1..=100, `speed` 1 (slow) ..= 10. EXIF is embedded; ICC is not supported by the muxer
|
||||
/// (the file is tagged sRGB via `nclx`). Returns [`Error::Encode`] for float samples, on wasm32 or
|
||||
/// when built without the `avif` feature.
|
||||
pub fn encode_avif(img: &EncodeImage, quality: u8, speed: u8, meta: &EncodeMeta) -> Result<Vec<u8>> {
|
||||
img.validate()?;
|
||||
#[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
|
||||
{
|
||||
let Samples::U8(s) = img.samples else {
|
||||
return Err(Error::Encode("AVIF encoder needs 8-bit samples".into()));
|
||||
let mut enc = ravif::Encoder::new()
|
||||
.with_quality(quality.clamp(1, 100) as f32)
|
||||
.with_alpha_quality(quality.clamp(1, 100) as f32)
|
||||
.with_speed(speed.clamp(1, 10));
|
||||
if let Some(exif) = meta.exif {
|
||||
enc = enc.with_exif(exif.to_vec());
|
||||
}
|
||||
let s = match img.samples {
|
||||
Samples::U8(s) => s,
|
||||
Samples::U16(s) => return avif_10bit(&enc, img, s),
|
||||
Samples::F32(_) => return Err(Error::Encode("AVIF encoder needs 8- or 16-bit samples".into())),
|
||||
};
|
||||
let n = img.width as usize * img.height as usize;
|
||||
let ch = img.channels as usize;
|
||||
@@ -312,13 +323,6 @@ pub fn encode_avif(img: &EncodeImage, quality: u8, speed: u8, meta: &EncodeMeta)
|
||||
_ => ravif::RGBA8::new(c[0], c[1], c[2], c[3]),
|
||||
})
|
||||
.collect();
|
||||
let mut enc = ravif::Encoder::new()
|
||||
.with_quality(quality.clamp(1, 100) as f32)
|
||||
.with_alpha_quality(quality.clamp(1, 100) as f32)
|
||||
.with_speed(speed.clamp(1, 10));
|
||||
if let Some(exif) = meta.exif {
|
||||
enc = enc.with_exif(exif.to_vec());
|
||||
}
|
||||
let r = enc.encode_rgba(ravif::Img::new(&px[..], img.width as usize, img.height as usize)).map_err(|e| Error::Encode(e.to_string()))?;
|
||||
let _ = img.has_alpha();
|
||||
Ok(r.avif_file)
|
||||
@@ -329,3 +333,32 @@ pub fn encode_avif(img: &EncodeImage, quality: u8, speed: u8, meta: &EncodeMeta)
|
||||
Err(Error::Encode("AVIF encoding is not available in this build".into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// 16-bit RGB(A) → 10-bit BT.601 full-range YCbCr planes (the matrix `ravif` uses for 8-bit input).
|
||||
#[cfg(all(feature = "avif", not(target_arch = "wasm32")))]
|
||||
fn avif_10bit(enc: &ravif::Encoder, img: &EncodeImage, s: &[u16]) -> Result<Vec<u8>> {
|
||||
const K: [f32; 3] = [0.299, 0.587, 0.114];
|
||||
let n = img.width as usize * img.height as usize;
|
||||
let ch = img.channels as usize;
|
||||
let rgb = |c: &[u16]| -> [f32; 3] {
|
||||
let v = |x: u16| x as f32 * (1023.0 / 65535.0);
|
||||
match ch {
|
||||
1 | 2 => [v(c[0]); 3],
|
||||
_ => [v(c[0]), v(c[1]), v(c[2])],
|
||||
}
|
||||
};
|
||||
let planes = s.chunks_exact(ch).take(n).map(|c| {
|
||||
let [r, g, b] = rgb(c);
|
||||
let y = K[0] * r + K[1] * g + K[2] * b;
|
||||
let cb = (b - y) * (0.5 / (1.0 - K[2])) + 512.0;
|
||||
let cr = (r - y) * (0.5 / (1.0 - K[0])) + 512.0;
|
||||
[y, cb, cr].map(|v| v.round().clamp(0.0, 1023.0) as u16)
|
||||
});
|
||||
let alpha_at = |c: &[u16]| if img.has_alpha() { c[ch - 1] } else { u16::MAX };
|
||||
let has_alpha = s.chunks_exact(ch).take(n).any(|c| alpha_at(c) != u16::MAX);
|
||||
let alpha = has_alpha.then(|| s.chunks_exact(ch).take(n).map(move |c| ((alpha_at(c) as u32 * 1023 + 32767) / 65535) as u16));
|
||||
let r = enc
|
||||
.encode_raw_planes_10_bit(img.width as usize, img.height as usize, planes, alpha, ravif::PixelRange::Full, ravif::MatrixCoefficients::BT601)
|
||||
.map_err(|e| Error::Encode(e.to_string()))?;
|
||||
Ok(r.avif_file)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user