| 1 | //! JPEG XL through `jxl-oxide`, rendered to sRGB (moxcms handles ICC-encoded files), with |
| 2 | //! animation. |
| 3 | |
| 4 | use std::path::Path; |
| 5 | use std::time::Duration; |
| 6 | |
| 7 | use anyhow::anyhow; |
| 8 | use jxl_oxide::{EnumColourEncoding, JxlImage, Moxcms, RenderingIntent}; |
| 9 | |
| 10 | use super::color; |
| 11 | use super::{DecodedImage, Frame, MAX_FRAMES_BYTES, Options}; |
| 12 | |
| 13 | pub fn decode(path: &Path, opts: &Options) -> anyhow::Result<DecodedImage> { |
| 14 | let mut image = JxlImage::builder().open(path).map_err(|e| anyhow!("{e}"))?; |
| 15 | if opts.color { |
| 16 | image.set_cms(Moxcms); |
| 17 | image.request_color_encoding(EnumColourEncoding::srgb(RenderingIntent::Relative)); |
| 18 | } |
| 19 | // Seconds per tick of a frame's duration. |
| 20 | let tick = image |
| 21 | .image_header() |
| 22 | .metadata |
| 23 | .animation |
| 24 | .as_ref() |
| 25 | .filter(|a| a.tps_numerator > 0) |
| 26 | .map(|a| a.tps_denominator as f64 / a.tps_numerator as f64); |
| 27 | let count = image.num_loaded_keyframes(); |
| 28 | let mut frames = Vec::new(); |
| 29 | let mut size = [0, 0]; |
| 30 | let mut bytes = 0; |
| 31 | for i in 0..count { |
| 32 | let render = image.render_frame(i).map_err(|e| anyhow!("{e}"))?; |
| 33 | let mut stream = render.stream(); |
| 34 | let channels = stream.channels() as usize; |
| 35 | size = [stream.width(), stream.height()]; |
| 36 | let mut buf = vec![0u8; size[0] as usize * size[1] as usize * channels]; |
| 37 | stream.write_to_buffer(&mut buf); |
| 38 | // Extra channels (CMYK black, spot colours) beyond RGBA are dropped. |
| 39 | let pixels = if channels > 4 { |
| 40 | buf.chunks_exact(channels) |
| 41 | .flat_map(|p| [p[0], p[1], p[2], p[3]]) |
| 42 | .collect() |
| 43 | } else { |
| 44 | color::expand(&buf, channels) |
| 45 | }; |
| 46 | let delay = tick.map_or(Duration::ZERO, |t| { |
| 47 | Duration::from_secs_f64(render.duration() as f64 * t) |
| 48 | }); |
| 49 | bytes += pixels.len(); |
| 50 | frames.push(Frame { pixels, delay }); |
| 51 | if bytes > MAX_FRAMES_BYTES { |
| 52 | log::warn!("animation truncated at {} frames", frames.len()); |
| 53 | break; |
| 54 | } |
| 55 | } |
| 56 | if frames.is_empty() { |
| 57 | return Err(anyhow!("no frames")); |
| 58 | } |
| 59 | Ok(DecodedImage { |
| 60 | size, |
| 61 | animated: tick.is_some() && frames.len() > 1, |
| 62 | frames, |
| 63 | format: "JXL", |
| 64 | source_size: size, |
| 65 | orientation: image::metadata::Orientation::NoTransforms, |
| 66 | refine: None, |
| 67 | }) |
| 68 | } |