Skip to main content

otf_pixels_codec_jxl/
decoder.rs

1//! The JPEG XL decoder: a bare codestream or the ISOBMFF container, decoded
2//! to its first displayed frame.
3
4#![allow(
5    clippy::indexing_slicing,
6    reason = "planes are the canvas size, so every pixel index is in range"
7)]
8
9use otf_pixels_core::{
10    Animation, Codec, Decoder, Format, ImageDescriptor, Limits, Orientation, PixelFormat,
11    PixelsError, Result, Source,
12};
13
14use crate::decode::{container, header::ExtraKind, render};
15
16/// The most compressed bytes read before a file is called hostile.
17const MAX_COMPRESSED: usize = 256 * 1024 * 1024;
18
19/// Decodes a JPEG XL stream.
20#[derive(Debug)]
21pub struct JxlDecoder {
22    descriptor: ImageDescriptor,
23    /// The codestream, until the first row is asked for.
24    codestream: Vec<u8>,
25    /// The decoded image, interleaved in the output format.
26    pixels: Option<Vec<u8>>,
27    row: u32,
28    orientation: Orientation,
29    icc: Option<Vec<u8>>,
30    exif: Option<Vec<u8>>,
31    animation: Option<Animation>,
32}
33
34impl JxlDecoder {
35    /// Read the file and parse its headers; the pixels decode when the first
36    /// row is read.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`PixelsError::Malformed`] for headers it rejects, or
41    /// [`PixelsError::LimitExceeded`] if the image exceeds `limits`.
42    pub fn new<S: Source>(mut source: S, limits: Limits) -> Result<Self> {
43        let mut bytes = Vec::new();
44        let mut chunk = [0_u8; 64 * 1024];
45        loop {
46            if bytes.len() > MAX_COMPRESSED {
47                return Err(PixelsError::malformed(
48                    "jxl",
49                    format!("stream exceeds {MAX_COMPRESSED} bytes"),
50                ));
51            }
52            match source.read(&mut chunk)? {
53                0 => break,
54                read => bytes.extend_from_slice(chunk.get(..read).unwrap_or(&[])),
55            }
56        }
57        let contents = container::parse(&bytes)?;
58        let exif = contents.exif;
59        let codestream = contents.codestream;
60        let (header, icc, durations) = render::probe(&codestream)?;
61        let (width, height) = (header.width, header.height);
62        let alpha = header.alpha().is_some();
63        let wide = header.bit_depth.bits > 8;
64        let pixel = match (header.grey(), alpha, wide, header.bit_depth.float) {
65            (_, false, _, true) => PixelFormat::RgbF32,
66            (_, true, _, true) => PixelFormat::RgbaF32,
67            (true, false, false, _) => PixelFormat::Gray8,
68            (true, false, true, _) => PixelFormat::Gray16,
69            (true, true, false, _) => PixelFormat::GrayA8,
70            (false, false, false, _) => PixelFormat::Rgb8,
71            (false, false, true, _) => PixelFormat::Rgb16,
72            (false, true, false, _) => PixelFormat::Rgba8,
73            (_, true, true, _) => PixelFormat::Rgba16,
74        };
75        let descriptor = ImageDescriptor::with_limits(width, height, pixel, &limits)?;
76        let animation = header.animation.and_then(|a| {
77            if durations.len() < 2 || a.tps_numerator == 0 {
78                return None;
79            }
80            let ms = durations
81                .iter()
82                .map(|&t| {
83                    (u64::from(t) * 1000 * u64::from(a.tps_denominator)
84                        / u64::from(a.tps_numerator)) as u32
85                })
86                .collect();
87            Animation::new(ms, a.num_loops)
88        });
89        // The codestream's own orientation; an EXIF block repeats it.
90        let orientation = Orientation::from_exif(header.orientation as u16).unwrap_or_default();
91        // XYB images are delivered as sRGB, so their profile does not apply.
92        let icc = if header.xyb { None } else { icc };
93        Ok(Self {
94            descriptor,
95            codestream,
96            pixels: None,
97            row: 0,
98            orientation,
99            icc,
100            exif,
101            animation,
102        })
103    }
104
105    fn decode_pixels(&self) -> Result<Vec<u8>> {
106        let image = render::decode(&self.codestream)?;
107        let canvas = &image.canvas;
108        let alpha = image
109            .header
110            .extra
111            .iter()
112            .position(|e| matches!(e.kind, ExtraKind::Alpha { .. }))
113            .and_then(|i| canvas.extra.get(i));
114        let n = canvas.width * canvas.height;
115        let pixel = self.descriptor.pixel;
116        let channels: Vec<&[f32]> = if pixel.channels() <= 2 {
117            vec![&canvas.colour[0][..n]]
118        } else {
119            canvas.colour.iter().map(|p| &p[..n]).collect()
120        };
121        let mut planes = channels;
122        if pixel.has_alpha() {
123            planes.push(alpha.map_or(&[][..], |a| &a[..n]));
124        }
125        let mut out = Vec::with_capacity(self.descriptor.row_bytes() * canvas.height);
126        for i in 0..n {
127            for p in &planes {
128                let v = p.get(i).copied().unwrap_or(1.0);
129                match pixel {
130                    PixelFormat::RgbF32 | PixelFormat::RgbaF32 => {
131                        out.extend_from_slice(&v.to_ne_bytes())
132                    }
133                    PixelFormat::Gray16 | PixelFormat::Rgb16 | PixelFormat::Rgba16 => {
134                        out.extend_from_slice(
135                            &((v.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16).to_ne_bytes(),
136                        );
137                    }
138                    _ => out.push((v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8),
139                }
140            }
141        }
142        Ok(out)
143    }
144}
145
146impl Decoder for JxlDecoder {
147    fn descriptor(&self) -> ImageDescriptor {
148        self.descriptor
149    }
150
151    fn orientation(&self) -> Orientation {
152        self.orientation
153    }
154
155    fn icc_profile(&self) -> Option<&[u8]> {
156        self.icc.as_deref()
157    }
158
159    fn exif(&self) -> Option<&[u8]> {
160        self.exif.as_deref()
161    }
162
163    fn animation(&self) -> Option<Animation> {
164        self.animation.clone()
165    }
166
167    fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
168        if self.row >= self.descriptor.height {
169            return Err(PixelsError::invalid_argument(
170                "out",
171                format!("all {} rows have already been read", self.descriptor.height),
172            ));
173        }
174        let row_bytes = self.descriptor.row_bytes();
175        if out.len() != row_bytes {
176            return Err(PixelsError::invalid_argument(
177                "out",
178                format!("row buffer is {} bytes, expected {row_bytes}", out.len()),
179            ));
180        }
181        if self.pixels.is_none() {
182            self.pixels = Some(self.decode_pixels()?);
183            self.codestream = Vec::new();
184        }
185        let start = self.row as usize * row_bytes;
186        let row = self
187            .pixels
188            .as_deref()
189            .and_then(|p| p.get(start..start + row_bytes))
190            .ok_or_else(|| PixelsError::malformed("jxl", "decoded image is short"))?;
191        out.copy_from_slice(row);
192        self.row += 1;
193        Ok(())
194    }
195}
196
197/// Whether `prefix` starts with a JPEG XL signature, bare or contained.
198#[must_use]
199pub fn probe(prefix: &[u8]) -> bool {
200    container::probe(prefix)
201}
202
203/// The JPEG XL entry in a sniffing registry.
204#[derive(Debug, Clone, Copy, Default)]
205pub struct JxlCodec;
206
207impl Codec for JxlCodec {
208    fn format(&self) -> Format {
209        Format::Jxl
210    }
211
212    fn magic_len(&self) -> usize {
213        12
214    }
215
216    fn probe(&self, prefix: &[u8]) -> bool {
217        probe(prefix)
218    }
219}