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 describe(&self) -> String {
120 self.id().to_string()
121 }
122
123 fn encode(&self, src: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64>;
126
127 fn decode(&self, data: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64>;
130}
131
132#[derive(Debug, Clone)]
134pub struct Jxl {
135 pub effort: u8,
138}
139
140impl Default for Jxl {
141 fn default() -> Self {
142 Self { effort: 4 }
143 }
144}
145
146impl Jxl {
147 fn speed(&self) -> jpegxl_rs::encode::EncoderSpeed {
148 use jpegxl_rs::encode::EncoderSpeed::*;
149 match self.effort.clamp(1, 10) {
150 1 => Lightning,
151 2 => Thunder,
152 3 => Falcon,
153 4 => Cheetah,
154 5 => Hare,
155 6 => Wombat,
156 7 => Squirrel,
157 8 => Kitten,
158 9 => Tortoise,
159 _ => Glacier,
160 }
161 }
162}
163
164fn as_u16(src: &[u8]) -> std::borrow::Cow<'_, [u16]> {
170 match bytemuck::try_cast_slice::<u8, u16>(src) {
171 Ok(s) => std::borrow::Cow::Borrowed(s),
172 Err(_) => std::borrow::Cow::Owned(
173 src.chunks_exact(2)
174 .map(|c| u16::from_ne_bytes([c[0], c[1]]))
175 .collect(),
176 ),
177 }
178}
179
180fn endianness(little_endian: bool) -> jpegxl_rs::Endianness {
181 if little_endian {
182 jpegxl_rs::Endianness::Little
183 } else {
184 jpegxl_rs::Endianness::Big
185 }
186}
187
188pub fn libjxl_version() -> String {
190 let v = unsafe { jpegxl_sys::encoder::encode::JxlEncoderVersion() };
192 format!("{}.{}.{}", v / 1_000_000, (v / 1_000) % 1_000, v % 1_000)
193}
194
195impl Codec for Jxl {
196 fn id(&self) -> &'static str {
197 "jxl"
198 }
199
200 fn describe(&self) -> String {
201 format!(
202 "libjxl {} effort {}",
203 libjxl_version(),
204 self.effort.clamp(1, 10)
205 )
206 }
207
208 fn encode(&self, src: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64> {
209 let Frame {
210 width,
211 height,
212 channels: ch,
213 depth,
214 little_endian,
215 } = frame;
216 let expected = frame.byte_len();
217 if src.len() as u64 != expected {
218 return Err(Error::SizeMismatch {
219 expected,
220 actual: src.len() as u64,
221 });
222 }
223
224 let color = match ch {
229 Channels::Gray => jpegxl_rs::encode::ColorEncoding::SrgbLuma,
230 Channels::Rgb => jpegxl_rs::encode::ColorEncoding::Srgb,
231 };
232 let runner = jpegxl_rs::ThreadsRunner::default();
235 let mut enc = jpegxl_rs::encoder_builder()
236 .parallel_runner(&runner)
237 .lossless(true)
238 .uses_original_profile(true)
242 .speed(self.speed())
243 .color_encoding(color)
244 .has_alpha(false)
245 .build()
246 .map_err(|e| Error::Backend(e.to_string()))?;
247
248 let nch = ch.count() as u32;
249 let encoded: Vec<u8> = match depth {
250 Depth::Eight => {
251 let ef = jpegxl_rs::encode::EncoderFrame::new(src)
252 .num_channels(nch)
253 .endianness(endianness(little_endian));
254 enc.encode_frame::<u8, u8>(&ef, width, height)
255 .map_err(|e| Error::Backend(e.to_string()))?
256 .data
257 }
258 Depth::Sixteen => {
259 let samples = as_u16(src);
260 let ef = jpegxl_rs::encode::EncoderFrame::new(samples.as_ref())
261 .num_channels(nch)
262 .endianness(endianness(little_endian));
263 enc.encode_frame::<u16, u16>(&ef, width, height)
264 .map_err(|e| Error::Backend(e.to_string()))?
265 .data
266 }
267 };
268
269 out.write_all(&encoded)?;
270 Ok(encoded.len() as u64)
271 }
272
273 fn decode(&self, data: &[u8], frame: Frame, out: &mut dyn Write) -> Result<u64> {
274 let Frame {
275 depth,
276 little_endian,
277 ..
278 } = frame;
279 let runner = jpegxl_rs::ThreadsRunner::default();
280 let dec = jpegxl_rs::decoder_builder()
281 .parallel_runner(&runner)
282 .build()
283 .map_err(|e| Error::Backend(e.to_string()))?;
284 let expected = frame.byte_len();
285
286 let bytes: Vec<u8> = match depth {
287 Depth::Eight => {
288 dec.decode_with::<u8>(data)
289 .map_err(|e| Error::Backend(e.to_string()))?
290 .1
291 }
292 Depth::Sixteen => {
293 let (_, px) = dec
294 .decode_with::<u16>(data)
295 .map_err(|e| Error::Backend(e.to_string()))?;
296 let mut v = Vec::with_capacity(px.len() * 2);
298 if little_endian {
299 for s in &px {
300 v.extend_from_slice(&s.to_le_bytes());
301 }
302 } else {
303 for s in &px {
304 v.extend_from_slice(&s.to_be_bytes());
305 }
306 }
307 v
308 }
309 };
310
311 if bytes.len() as u64 != expected {
312 return Err(Error::SizeMismatch {
313 expected,
314 actual: bytes.len() as u64,
315 });
316 }
317 out.write_all(&bytes)?;
318 Ok(bytes.len() as u64)
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 fn noise(n: usize, seed: u64) -> Vec<u8> {
328 let mut s = seed | 1;
329 (0..n)
330 .map(|_| {
331 s ^= s << 13;
332 s ^= s >> 7;
333 s ^= s << 17;
334 (s >> 24) as u8
335 })
336 .collect()
337 }
338
339 fn gray16(w: u32, h: u32, le: bool) -> Frame {
340 Frame {
341 width: w,
342 height: h,
343 channels: Channels::Gray,
344 depth: Depth::Sixteen,
345 little_endian: le,
346 }
347 }
348
349 fn round_trip(w: u32, h: u32, ch: Channels, depth: Depth, le: bool) {
350 let c = Jxl { effort: 1 };
351 let f = Frame {
352 width: w,
353 height: h,
354 channels: ch,
355 depth,
356 little_endian: le,
357 };
358 let src = noise(f.byte_len() as usize, 99);
359 let mut enc = Vec::new();
360 c.encode(&src, f, &mut enc).unwrap();
361 let mut dec = Vec::new();
362 c.decode(&enc, f, &mut dec).unwrap();
363 assert_eq!(src, dec, "{w}x{h} {ch:?} {depth:?} le={le}");
364 }
365
366 #[test]
369 fn round_trips_losslessly() {
370 round_trip(64, 48, Channels::Gray, Depth::Sixteen, true);
371 round_trip(64, 48, Channels::Gray, Depth::Sixteen, false);
372 round_trip(40, 24, Channels::Rgb, Depth::Eight, true);
373 round_trip(33, 17, Channels::Rgb, Depth::Sixteen, true);
374 round_trip(33, 17, Channels::Rgb, Depth::Sixteen, false);
375 }
376
377 #[test]
380 fn handles_odd_dimensions() {
381 round_trip(1, 1, Channels::Gray, Depth::Sixteen, true);
382 round_trip(7, 3, Channels::Rgb, Depth::Eight, false);
383 }
384
385 #[test]
386 fn encode_rejects_wrong_byte_count() {
387 let c = Jxl::default();
388 assert!(matches!(
389 c.encode(&[0u8; 10], gray16(4, 4, false), &mut Vec::new()),
390 Err(Error::SizeMismatch { .. })
391 ));
392 }
393
394 #[test]
396 fn byte_order_is_preserved() {
397 let c = Jxl { effort: 1 };
398 let src = noise(16 * 16 * 2, 3);
399 for le in [true, false] {
400 let f = gray16(16, 16, le);
401 let mut enc = Vec::new();
402 c.encode(&src, f, &mut enc).unwrap();
403 let mut out = Vec::new();
404 c.decode(&enc, f, &mut out).unwrap();
405 assert_eq!(src, out, "le={le}");
406 }
407 }
408}
409
410#[cfg(test)]
411mod version_tests {
412 #[test]
415 fn reports_the_linked_libjxl_version() {
416 let v = super::libjxl_version();
417 assert!(
418 v.starts_with("0.") || v.starts_with("1."),
419 "odd version {v}"
420 );
421 assert_eq!(
422 v.matches('.').count(),
423 2,
424 "expected major.minor.patch, got {v}"
425 );
426 use super::Codec;
427 assert!(super::Jxl::default().describe().contains(&v));
428 }
429}