1use std::io::Write;
30
31#[derive(Debug, thiserror::Error)]
32pub enum Error {
33 #[error("io: {0}")]
34 Io(#[from] std::io::Error),
35 #[error("libjxl: {0}")]
36 Backend(String),
37 #[error("byte count mismatch: got {actual}, expected {expected}")]
38 SizeMismatch { expected: u64, actual: u64 },
39}
40
41pub type Result<T> = std::result::Result<T, Error>;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Channels {
45 Gray,
46 Rgb,
47}
48
49impl Channels {
50 pub fn count(self) -> u64 {
51 match self {
52 Channels::Gray => 1,
53 Channels::Rgb => 3,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Depth {
62 Eight,
63 Sixteen,
64}
65
66impl Depth {
67 pub fn from_bits(bits: u16) -> Option<Self> {
68 match bits {
69 8 => Some(Depth::Eight),
70 16 => Some(Depth::Sixteen),
71 _ => None,
72 }
73 }
74 pub fn bytes(self) -> u64 {
75 match self {
76 Depth::Eight => 1,
77 Depth::Sixteen => 2,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct Frame {
89 pub width: u32,
90 pub height: u32,
91 pub channels: Channels,
92 pub depth: Depth,
93 pub little_endian: bool,
95}
96
97impl Frame {
98 pub fn byte_len(&self) -> u64 {
100 u64::from(self.width) * u64::from(self.height) * self.channels.count() * self.depth.bytes()
101 }
102}
103
104pub trait Codec: Sync {
109 fn id(&self) -> &'static str;
112
113 fn encode(&self, src: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64>;
116
117 fn decode(&self, data: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64>;
120}
121
122#[derive(Debug, Clone)]
124pub struct Jxl {
125 pub effort: u8,
128}
129
130impl Default for Jxl {
131 fn default() -> Self {
132 Self { effort: 4 }
133 }
134}
135
136impl Jxl {
137 fn speed(&self) -> jpegxl_rs::encode::EncoderSpeed {
138 use jpegxl_rs::encode::EncoderSpeed::*;
139 match self.effort.clamp(1, 10) {
140 1 => Lightning,
141 2 => Thunder,
142 3 => Falcon,
143 4 => Cheetah,
144 5 => Hare,
145 6 => Wombat,
146 7 => Squirrel,
147 8 => Kitten,
148 9 => Tortoise,
149 _ => Glacier,
150 }
151 }
152}
153
154fn as_u16(src: &[u8]) -> std::borrow::Cow<'_, [u16]> {
160 match bytemuck::try_cast_slice::<u8, u16>(src) {
161 Ok(s) => std::borrow::Cow::Borrowed(s),
162 Err(_) => std::borrow::Cow::Owned(
163 src.chunks_exact(2)
164 .map(|c| u16::from_ne_bytes([c[0], c[1]]))
165 .collect(),
166 ),
167 }
168}
169
170fn endianness(little_endian: bool) -> jpegxl_rs::Endianness {
171 if little_endian {
172 jpegxl_rs::Endianness::Little
173 } else {
174 jpegxl_rs::Endianness::Big
175 }
176}
177
178impl Codec for Jxl {
179 fn id(&self) -> &'static str {
180 "jxl"
181 }
182
183 fn encode(&self, src: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64> {
184 let Frame {
185 width,
186 height,
187 channels: ch,
188 depth,
189 little_endian,
190 } = frame;
191 let expected = frame.byte_len();
192 if src.len() as u64 != expected {
193 return Err(Error::SizeMismatch {
194 expected,
195 actual: src.len() as u64,
196 });
197 }
198
199 let color = match ch {
204 Channels::Gray => jpegxl_rs::encode::ColorEncoding::SrgbLuma,
205 Channels::Rgb => jpegxl_rs::encode::ColorEncoding::Srgb,
206 };
207 let runner = jpegxl_rs::ThreadsRunner::default();
210 let mut enc = jpegxl_rs::encoder_builder()
211 .parallel_runner(&runner)
212 .lossless(true)
213 .uses_original_profile(true)
217 .speed(self.speed())
218 .color_encoding(color)
219 .has_alpha(false)
220 .build()
221 .map_err(|e| Error::Backend(e.to_string()))?;
222
223 let nch = ch.count() as u32;
224 let encoded: Vec<u8> = match depth {
225 Depth::Eight => {
226 let ef = jpegxl_rs::encode::EncoderFrame::new(src)
227 .num_channels(nch)
228 .endianness(endianness(little_endian));
229 enc.encode_frame::<u8, u8>(&ef, width, height)
230 .map_err(|e| Error::Backend(e.to_string()))?
231 .data
232 }
233 Depth::Sixteen => {
234 let samples = as_u16(src);
235 let ef = jpegxl_rs::encode::EncoderFrame::new(samples.as_ref())
236 .num_channels(nch)
237 .endianness(endianness(little_endian));
238 enc.encode_frame::<u16, u16>(&ef, width, height)
239 .map_err(|e| Error::Backend(e.to_string()))?
240 .data
241 }
242 };
243
244 out.write_all(&encoded)?;
245 Ok(encoded.len() as u64)
246 }
247
248 fn decode(&self, data: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64> {
249 let Frame {
250 depth,
251 little_endian,
252 ..
253 } = frame;
254 let runner = jpegxl_rs::ThreadsRunner::default();
255 let dec = jpegxl_rs::decoder_builder()
256 .parallel_runner(&runner)
257 .build()
258 .map_err(|e| Error::Backend(e.to_string()))?;
259 let expected = frame.byte_len();
260
261 let bytes: Vec<u8> = match depth {
262 Depth::Eight => {
263 dec.decode_with::<u8>(data)
264 .map_err(|e| Error::Backend(e.to_string()))?
265 .1
266 }
267 Depth::Sixteen => {
268 let (_, px) = dec
269 .decode_with::<u16>(data)
270 .map_err(|e| Error::Backend(e.to_string()))?;
271 let mut v = Vec::with_capacity(px.len() * 2);
273 if little_endian {
274 for s in &px {
275 v.extend_from_slice(&s.to_le_bytes());
276 }
277 } else {
278 for s in &px {
279 v.extend_from_slice(&s.to_be_bytes());
280 }
281 }
282 v
283 }
284 };
285
286 if bytes.len() as u64 != expected {
287 return Err(Error::SizeMismatch {
288 expected,
289 actual: bytes.len() as u64,
290 });
291 }
292 out.write_all(&bytes)?;
293 Ok(bytes.len() as u64)
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn noise(n: usize, seed: u64) -> Vec<u8> {
303 let mut s = seed | 1;
304 (0..n)
305 .map(|_| {
306 s ^= s << 13;
307 s ^= s >> 7;
308 s ^= s << 17;
309 (s >> 24) as u8
310 })
311 .collect()
312 }
313
314 fn gray16(w: u32, h: u32, le: bool) -> Frame {
315 Frame {
316 width: w,
317 height: h,
318 channels: Channels::Gray,
319 depth: Depth::Sixteen,
320 little_endian: le,
321 }
322 }
323
324 fn round_trip(w: u32, h: u32, ch: Channels, depth: Depth, le: bool) {
325 let c = Jxl { effort: 1 };
326 let f = Frame {
327 width: w,
328 height: h,
329 channels: ch,
330 depth,
331 little_endian: le,
332 };
333 let src = noise(f.byte_len() as usize, 99);
334 let mut enc = Vec::new();
335 c.encode(&src, f, &mut enc).unwrap();
336 let mut dec = Vec::new();
337 c.decode(&enc, f, &mut dec).unwrap();
338 assert_eq!(src, dec, "{w}x{h} {ch:?} {depth:?} le={le}");
339 }
340
341 #[test]
344 fn round_trips_losslessly() {
345 round_trip(64, 48, Channels::Gray, Depth::Sixteen, true);
346 round_trip(64, 48, Channels::Gray, Depth::Sixteen, false);
347 round_trip(40, 24, Channels::Rgb, Depth::Eight, true);
348 round_trip(33, 17, Channels::Rgb, Depth::Sixteen, true);
349 round_trip(33, 17, Channels::Rgb, Depth::Sixteen, false);
350 }
351
352 #[test]
355 fn handles_odd_dimensions() {
356 round_trip(1, 1, Channels::Gray, Depth::Sixteen, true);
357 round_trip(7, 3, Channels::Rgb, Depth::Eight, false);
358 }
359
360 #[test]
361 fn encode_rejects_wrong_byte_count() {
362 let c = Jxl::default();
363 assert!(matches!(
364 c.encode(&[0u8; 10], gray16(4, 4, false), &mut Vec::new()),
365 Err(Error::SizeMismatch { .. })
366 ));
367 }
368
369 #[test]
371 fn byte_order_is_preserved() {
372 let c = Jxl { effort: 1 };
373 let src = noise(16 * 16 * 2, 3);
374 for le in [true, false] {
375 let f = gray16(16, 16, le);
376 let mut enc = Vec::new();
377 c.encode(&src, f, &mut enc).unwrap();
378 let mut out = Vec::new();
379 c.decode(&enc, f, &mut out).unwrap();
380 assert_eq!(src, out, "le={le}");
381 }
382 }
383}