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
82pub trait Codec: Sync {
87 fn id(&self) -> &'static str;
90
91 fn encode(
94 &self,
95 src: &[u8],
96 width: u32,
97 height: u32,
98 ch: Channels,
99 depth: Depth,
100 little_endian: bool,
101 out: &mut dyn Write,
102 ) -> Result<u64>;
103
104 fn decode(
107 &self,
108 data: &[u8],
109 width: u32,
110 height: u32,
111 ch: Channels,
112 depth: Depth,
113 little_endian: bool,
114 out: &mut dyn Write,
115 ) -> Result<u64>;
116}
117
118#[derive(Debug, Clone)]
120pub struct Jxl {
121 pub effort: u8,
124}
125
126impl Default for Jxl {
127 fn default() -> Self {
128 Self { effort: 4 }
129 }
130}
131
132impl Jxl {
133 fn speed(&self) -> jpegxl_rs::encode::EncoderSpeed {
134 use jpegxl_rs::encode::EncoderSpeed::*;
135 match self.effort.clamp(1, 10) {
136 1 => Lightning,
137 2 => Thunder,
138 3 => Falcon,
139 4 => Cheetah,
140 5 => Hare,
141 6 => Wombat,
142 7 => Squirrel,
143 8 => Kitten,
144 9 => Tortoise,
145 _ => Glacier,
146 }
147 }
148}
149
150fn as_u16(src: &[u8]) -> std::borrow::Cow<'_, [u16]> {
156 match bytemuck::try_cast_slice::<u8, u16>(src) {
157 Ok(s) => std::borrow::Cow::Borrowed(s),
158 Err(_) => std::borrow::Cow::Owned(
159 src.chunks_exact(2)
160 .map(|c| u16::from_ne_bytes([c[0], c[1]]))
161 .collect(),
162 ),
163 }
164}
165
166fn endianness(little_endian: bool) -> jpegxl_rs::Endianness {
167 if little_endian {
168 jpegxl_rs::Endianness::Little
169 } else {
170 jpegxl_rs::Endianness::Big
171 }
172}
173
174impl Codec for Jxl {
175 fn id(&self) -> &'static str {
176 "jxl"
177 }
178
179 fn encode(
180 &self,
181 src: &[u8],
182 width: u32,
183 height: u32,
184 ch: Channels,
185 depth: Depth,
186 little_endian: bool,
187 out: &mut dyn Write,
188 ) -> Result<u64> {
189 let expected = u64::from(width) * u64::from(height) * ch.count() * depth.bytes();
190 if src.len() as u64 != expected {
191 return Err(Error::SizeMismatch {
192 expected,
193 actual: src.len() as u64,
194 });
195 }
196
197 let color = match ch {
202 Channels::Gray => jpegxl_rs::encode::ColorEncoding::SrgbLuma,
203 Channels::Rgb => jpegxl_rs::encode::ColorEncoding::Srgb,
204 };
205 let runner = jpegxl_rs::ThreadsRunner::default();
208 let mut enc = jpegxl_rs::encoder_builder()
209 .parallel_runner(&runner)
210 .lossless(true)
211 .uses_original_profile(true)
215 .speed(self.speed())
216 .color_encoding(color)
217 .has_alpha(false)
218 .build()
219 .map_err(|e| Error::Backend(e.to_string()))?;
220
221 let nch = ch.count() as u32;
222 let encoded: Vec<u8> = match depth {
223 Depth::Eight => {
224 let frame = jpegxl_rs::encode::EncoderFrame::new(src)
225 .num_channels(nch)
226 .endianness(endianness(little_endian));
227 enc.encode_frame::<u8, u8>(&frame, width, height)
228 .map_err(|e| Error::Backend(e.to_string()))?
229 .data
230 }
231 Depth::Sixteen => {
232 let samples = as_u16(src);
233 let frame = jpegxl_rs::encode::EncoderFrame::new(samples.as_ref())
234 .num_channels(nch)
235 .endianness(endianness(little_endian));
236 enc.encode_frame::<u16, u16>(&frame, width, height)
237 .map_err(|e| Error::Backend(e.to_string()))?
238 .data
239 }
240 };
241
242 out.write_all(&encoded)?;
243 Ok(encoded.len() as u64)
244 }
245
246 fn decode(
247 &self,
248 data: &[u8],
249 width: u32,
250 height: u32,
251 ch: Channels,
252 depth: Depth,
253 little_endian: bool,
254 out: &mut dyn Write,
255 ) -> Result<u64> {
256 let runner = jpegxl_rs::ThreadsRunner::default();
257 let dec = jpegxl_rs::decoder_builder()
258 .parallel_runner(&runner)
259 .build()
260 .map_err(|e| Error::Backend(e.to_string()))?;
261 let expected = u64::from(width) * u64::from(height) * ch.count() * depth.bytes();
262
263 let bytes: Vec<u8> = match depth {
264 Depth::Eight => {
265 dec.decode_with::<u8>(data)
266 .map_err(|e| Error::Backend(e.to_string()))?
267 .1
268 }
269 Depth::Sixteen => {
270 let (_, px) = dec
271 .decode_with::<u16>(data)
272 .map_err(|e| Error::Backend(e.to_string()))?;
273 let mut v = Vec::with_capacity(px.len() * 2);
275 if little_endian {
276 for s in &px {
277 v.extend_from_slice(&s.to_le_bytes());
278 }
279 } else {
280 for s in &px {
281 v.extend_from_slice(&s.to_be_bytes());
282 }
283 }
284 v
285 }
286 };
287
288 if bytes.len() as u64 != expected {
289 return Err(Error::SizeMismatch {
290 expected,
291 actual: bytes.len() as u64,
292 });
293 }
294 out.write_all(&bytes)?;
295 Ok(bytes.len() as u64)
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn noise(n: usize, seed: u64) -> Vec<u8> {
305 let mut s = seed | 1;
306 (0..n)
307 .map(|_| {
308 s ^= s << 13;
309 s ^= s >> 7;
310 s ^= s << 17;
311 (s >> 24) as u8
312 })
313 .collect()
314 }
315
316 fn round_trip(w: u32, h: u32, ch: Channels, depth: Depth, le: bool) {
317 let c = Jxl { effort: 1 };
318 let n = (u64::from(w) * u64::from(h) * ch.count() * depth.bytes()) as usize;
319 let src = noise(n, 99);
320 let mut enc = Vec::new();
321 c.encode(&src, w, h, ch, depth, le, &mut enc).unwrap();
322 let mut dec = Vec::new();
323 c.decode(&enc, w, h, ch, depth, le, &mut dec).unwrap();
324 assert_eq!(src, dec, "{w}x{h} {ch:?} {depth:?} le={le}");
325 }
326
327 #[test]
330 fn round_trips_losslessly() {
331 round_trip(64, 48, Channels::Gray, Depth::Sixteen, true);
332 round_trip(64, 48, Channels::Gray, Depth::Sixteen, false);
333 round_trip(40, 24, Channels::Rgb, Depth::Eight, true);
334 round_trip(33, 17, Channels::Rgb, Depth::Sixteen, true);
335 round_trip(33, 17, Channels::Rgb, Depth::Sixteen, false);
336 }
337
338 #[test]
341 fn handles_odd_dimensions() {
342 round_trip(1, 1, Channels::Gray, Depth::Sixteen, true);
343 round_trip(7, 3, Channels::Rgb, Depth::Eight, false);
344 }
345
346 #[test]
347 fn encode_rejects_wrong_byte_count() {
348 let c = Jxl::default();
349 assert!(matches!(
350 c.encode(
351 &[0u8; 10],
352 4,
353 4,
354 Channels::Gray,
355 Depth::Sixteen,
356 false,
357 &mut Vec::new()
358 ),
359 Err(Error::SizeMismatch { .. })
360 ));
361 }
362
363 #[test]
365 fn byte_order_is_preserved() {
366 let c = Jxl { effort: 1 };
367 let src = noise(16 * 16 * 2, 3);
368 for le in [true, false] {
369 let mut enc = Vec::new();
370 c.encode(&src, 16, 16, Channels::Gray, Depth::Sixteen, le, &mut enc)
371 .unwrap();
372 let mut out = Vec::new();
373 c.decode(&enc, 16, 16, Channels::Gray, Depth::Sixteen, le, &mut out)
374 .unwrap();
375 assert_eq!(src, out, "le={le}");
376 }
377 }
378}