1#![allow(clippy::multiple_crate_versions)] #[cfg(test)]
23use ::serde_json as _;
24
25use std::borrow::Cow;
26use std::fmt;
27
28use ndarray::{Array, Array1, ArrayBase, Axis, Data, Dimension, IxDyn, ShapeError};
29use num_traits::identities::Zero;
30use numcodecs::{
31 AnyArray, AnyArrayAssignError, AnyArrayDType, AnyArrayView, AnyArrayViewMut, AnyCowArray,
32 Codec, StaticCodec, StaticCodecConfig, StaticCodecVersion,
33};
34use schemars::JsonSchema;
35use serde::{Deserialize, Serialize};
36use thiserror::Error;
37
38mod ffi;
39
40type Jpeg2000CodecVersion = StaticCodecVersion<0, 1, 0>;
41
42#[derive(Clone, Serialize, Deserialize, JsonSchema)]
43#[schemars(deny_unknown_fields)]
45pub struct Jpeg2000Codec {
52 #[serde(flatten)]
54 pub mode: Jpeg2000CompressionMode,
55 #[serde(default, rename = "_version")]
57 pub version: Jpeg2000CodecVersion,
58}
59
60#[derive(Clone, Serialize, Deserialize, JsonSchema)]
61#[serde(tag = "mode")]
63pub enum Jpeg2000CompressionMode {
64 #[serde(rename = "psnr")]
66 PSNR {
67 psnr: f32,
69 },
70 #[serde(rename = "rate")]
72 Rate {
73 rate: f32,
75 },
76 #[serde(rename = "lossless")]
78 Lossless,
79}
80
81impl Codec for Jpeg2000Codec {
82 type Error = Jpeg2000CodecError;
83
84 fn encode(&self, data: AnyCowArray) -> Result<AnyArray, Self::Error> {
85 match data {
86 AnyCowArray::I8(data) => Ok(AnyArray::U8(
87 Array1::from(compress(data, &self.mode)?).into_dyn(),
88 )),
89 AnyCowArray::U8(data) => Ok(AnyArray::U8(
90 Array1::from(compress(data, &self.mode)?).into_dyn(),
91 )),
92 AnyCowArray::I16(data) => Ok(AnyArray::U8(
93 Array1::from(compress(data, &self.mode)?).into_dyn(),
94 )),
95 AnyCowArray::U16(data) => Ok(AnyArray::U8(
96 Array1::from(compress(data, &self.mode)?).into_dyn(),
97 )),
98 AnyCowArray::I32(data) => Ok(AnyArray::U8(
99 Array1::from(compress(data, &self.mode)?).into_dyn(),
100 )),
101 AnyCowArray::U32(data) => Ok(AnyArray::U8(
102 Array1::from(compress(data, &self.mode)?).into_dyn(),
103 )),
104 AnyCowArray::I64(data) => Ok(AnyArray::U8(
105 Array1::from(compress(data, &self.mode)?).into_dyn(),
106 )),
107 AnyCowArray::U64(data) => Ok(AnyArray::U8(
108 Array1::from(compress(data, &self.mode)?).into_dyn(),
109 )),
110 encoded => Err(Jpeg2000CodecError::UnsupportedDtype(encoded.dtype())),
111 }
112 }
113
114 fn decode(&self, encoded: AnyCowArray) -> Result<AnyArray, Self::Error> {
115 let AnyCowArray::U8(encoded) = encoded else {
116 return Err(Jpeg2000CodecError::EncodedDataNotBytes {
117 dtype: encoded.dtype(),
118 });
119 };
120
121 if !matches!(encoded.shape(), [_]) {
122 return Err(Jpeg2000CodecError::EncodedDataNotOneDimensional {
123 shape: encoded.shape().to_vec(),
124 });
125 }
126
127 decompress(&AnyCowArray::U8(encoded).as_bytes())
128 }
129
130 fn decode_into(
131 &self,
132 encoded: AnyArrayView,
133 mut decoded: AnyArrayViewMut,
134 ) -> Result<(), Self::Error> {
135 let decoded_in = self.decode(encoded.cow())?;
136
137 Ok(decoded.assign(&decoded_in)?)
138 }
139}
140
141impl StaticCodec for Jpeg2000Codec {
142 const CODEC_ID: &'static str = "jpeg2000.rs";
143
144 type Config<'de> = Self;
145
146 fn from_config(config: Self::Config<'_>) -> Self {
147 config
148 }
149
150 fn get_config(&self) -> StaticCodecConfig<Self> {
151 StaticCodecConfig::from(self)
152 }
153}
154
155#[derive(Debug, Error)]
156pub enum Jpeg2000CodecError {
158 #[error("Jpeg2000 does not support the dtype {0}")]
160 UnsupportedDtype(AnyArrayDType),
161 #[error("Jpeg2000 failed to encode the header")]
163 HeaderEncodeFailed {
164 source: Jpeg2000HeaderError,
166 },
167 #[error("Jpeg2000 failed to encode the data")]
169 Jpeg2000EncodeFailed {
170 source: Jpeg2000CodingError,
172 },
173 #[error("Jpeg2000 failed to encode a slice")]
175 SliceEncodeFailed {
176 source: Jpeg2000SliceError,
178 },
179 #[error(
182 "Jpeg2000 can only decode one-dimensional byte arrays but received an array of dtype {dtype}"
183 )]
184 EncodedDataNotBytes {
185 dtype: AnyArrayDType,
187 },
188 #[error("Jpeg2000 can only decode one-dimensional byte arrays but received a byte array of shape {shape:?}")]
191 EncodedDataNotOneDimensional {
192 shape: Vec<usize>,
194 },
195 #[error("Jpeg2000 failed to decode the header")]
197 HeaderDecodeFailed {
198 source: Jpeg2000HeaderError,
200 },
201 #[error("Jpeg2000 failed to decode a slice")]
203 SliceDecodeFailed {
204 source: Jpeg2000SliceError,
206 },
207 #[error("Jpeg2000 failed to decode from an excessive number of slices")]
209 DecodeTooManySlices,
210 #[error("Jpeg2000 failed to decode the data")]
212 Jpeg2000DecodeFailed {
213 source: Jpeg2000CodingError,
215 },
216 #[error("Jpeg2000 decoded into an invalid shape not matching the data size")]
218 DecodeInvalidShape {
219 source: ShapeError,
221 },
222 #[error("Jpeg2000Codec cannot decode into the provided array")]
224 MismatchedDecodeIntoArray {
225 #[from]
227 source: AnyArrayAssignError,
228 },
229}
230
231#[derive(Debug, Error)]
232#[error(transparent)]
233pub struct Jpeg2000HeaderError(postcard::Error);
235
236#[derive(Debug, Error)]
237#[error(transparent)]
238pub struct Jpeg2000SliceError(postcard::Error);
240
241#[derive(Debug, Error)]
242#[error(transparent)]
243pub struct Jpeg2000CodingError(ffi::Jpeg2000Error);
245
246pub fn compress<T: Jpeg2000Element, S: Data<Elem = T>, D: Dimension>(
256 data: ArrayBase<S, D>,
257 mode: &Jpeg2000CompressionMode,
258) -> Result<Vec<u8>, Jpeg2000CodecError> {
259 let mut encoded = postcard::to_extend(
260 &CompressionHeader {
261 dtype: T::DTYPE,
262 shape: Cow::Borrowed(data.shape()),
263 version: StaticCodecVersion,
264 },
265 Vec::new(),
266 )
267 .map_err(|err| Jpeg2000CodecError::HeaderEncodeFailed {
268 source: Jpeg2000HeaderError(err),
269 })?;
270
271 if data.is_empty() {
273 return Ok(encoded);
274 }
275
276 let mut encoded_slice = Vec::new();
277
278 let mut chunk_size = Vec::from(data.shape());
279 let (width, height) = match *chunk_size.as_mut_slice() {
280 [ref mut rest @ .., height, width] => {
281 for r in rest {
282 *r = 1;
283 }
284 (width, height)
285 }
286 [width] => (width, 1),
287 [] => (1, 1),
288 };
289
290 for slice in data.into_dyn().exact_chunks(chunk_size.as_slice()) {
291 encoded_slice.clear();
292
293 ffi::encode_into(
294 slice.iter().copied(),
295 width,
296 height,
297 match mode {
298 Jpeg2000CompressionMode::PSNR { psnr } => ffi::Jpeg2000CompressionMode::PSNR(*psnr),
299 Jpeg2000CompressionMode::Rate { rate } => ffi::Jpeg2000CompressionMode::Rate(*rate),
300 Jpeg2000CompressionMode::Lossless => ffi::Jpeg2000CompressionMode::Lossless,
301 },
302 &mut encoded_slice,
303 )
304 .map_err(|err| Jpeg2000CodecError::Jpeg2000EncodeFailed {
305 source: Jpeg2000CodingError(err),
306 })?;
307
308 encoded = postcard::to_extend(encoded_slice.as_slice(), encoded).map_err(|err| {
309 Jpeg2000CodecError::SliceEncodeFailed {
310 source: Jpeg2000SliceError(err),
311 }
312 })?;
313 }
314
315 Ok(encoded)
316}
317
318pub fn decompress(encoded: &[u8]) -> Result<AnyArray, Jpeg2000CodecError> {
332 fn decompress_typed<T: Jpeg2000Element>(
333 mut encoded: &[u8],
334 shape: &[usize],
335 ) -> Result<Array<T, IxDyn>, Jpeg2000CodecError> {
336 let mut decoded = Array::<T, _>::zeros(shape);
337
338 let mut chunk_size = Vec::from(shape);
339 let (width, height) = match *chunk_size.as_mut_slice() {
340 [ref mut rest @ .., height, width] => {
341 for r in rest {
342 *r = 1;
343 }
344 (width, height)
345 }
346 [width] => (width, 1),
347 [] => (1, 1),
348 };
349
350 for mut slice in decoded.exact_chunks_mut(chunk_size.as_slice()) {
351 let (encoded_slice, rest) =
352 postcard::take_from_bytes::<Cow<[u8]>>(encoded).map_err(|err| {
353 Jpeg2000CodecError::SliceDecodeFailed {
354 source: Jpeg2000SliceError(err),
355 }
356 })?;
357 encoded = rest;
358
359 let (decoded_slice, (_width, _height)) =
360 ffi::decode::<T>(&encoded_slice).map_err(|err| {
361 Jpeg2000CodecError::Jpeg2000DecodeFailed {
362 source: Jpeg2000CodingError(err),
363 }
364 })?;
365 let mut decoded_slice = Array::from_shape_vec((height, width), decoded_slice)
366 .map_err(|source| Jpeg2000CodecError::DecodeInvalidShape { source })?
367 .into_dyn();
368
369 while decoded_slice.ndim() > shape.len() {
370 decoded_slice = decoded_slice.remove_axis(Axis(0));
371 }
372
373 slice.assign(&decoded_slice);
374 }
375
376 if !encoded.is_empty() {
377 return Err(Jpeg2000CodecError::DecodeTooManySlices);
378 }
379
380 Ok(decoded)
381 }
382
383 let (header, encoded) =
384 postcard::take_from_bytes::<CompressionHeader>(encoded).map_err(|err| {
385 Jpeg2000CodecError::HeaderDecodeFailed {
386 source: Jpeg2000HeaderError(err),
387 }
388 })?;
389
390 if header.shape.iter().copied().product::<usize>() == 0 {
392 return match header.dtype {
393 Jpeg2000DType::I8 => Ok(AnyArray::I8(Array::zeros(&*header.shape))),
394 Jpeg2000DType::U8 => Ok(AnyArray::U8(Array::zeros(&*header.shape))),
395 Jpeg2000DType::I16 => Ok(AnyArray::I16(Array::zeros(&*header.shape))),
396 Jpeg2000DType::U16 => Ok(AnyArray::U16(Array::zeros(&*header.shape))),
397 Jpeg2000DType::I32 => Ok(AnyArray::I32(Array::zeros(&*header.shape))),
398 Jpeg2000DType::U32 => Ok(AnyArray::U32(Array::zeros(&*header.shape))),
399 Jpeg2000DType::I64 => Ok(AnyArray::I64(Array::zeros(&*header.shape))),
400 Jpeg2000DType::U64 => Ok(AnyArray::U64(Array::zeros(&*header.shape))),
401 };
402 }
403
404 match header.dtype {
405 Jpeg2000DType::I8 => Ok(AnyArray::I8(decompress_typed(encoded, &header.shape)?)),
406 Jpeg2000DType::U8 => Ok(AnyArray::U8(decompress_typed(encoded, &header.shape)?)),
407 Jpeg2000DType::I16 => Ok(AnyArray::I16(decompress_typed(encoded, &header.shape)?)),
408 Jpeg2000DType::U16 => Ok(AnyArray::U16(decompress_typed(encoded, &header.shape)?)),
409 Jpeg2000DType::I32 => Ok(AnyArray::I32(decompress_typed(encoded, &header.shape)?)),
410 Jpeg2000DType::U32 => Ok(AnyArray::U32(decompress_typed(encoded, &header.shape)?)),
411 Jpeg2000DType::I64 => Ok(AnyArray::I64(decompress_typed(encoded, &header.shape)?)),
412 Jpeg2000DType::U64 => Ok(AnyArray::U64(decompress_typed(encoded, &header.shape)?)),
413 }
414}
415
416pub trait Jpeg2000Element: ffi::Jpeg2000Element + Zero {
418 const DTYPE: Jpeg2000DType;
420}
421
422impl Jpeg2000Element for i8 {
423 const DTYPE: Jpeg2000DType = Jpeg2000DType::I8;
424}
425impl Jpeg2000Element for u8 {
426 const DTYPE: Jpeg2000DType = Jpeg2000DType::U8;
427}
428impl Jpeg2000Element for i16 {
429 const DTYPE: Jpeg2000DType = Jpeg2000DType::I16;
430}
431impl Jpeg2000Element for u16 {
432 const DTYPE: Jpeg2000DType = Jpeg2000DType::U16;
433}
434impl Jpeg2000Element for i32 {
435 const DTYPE: Jpeg2000DType = Jpeg2000DType::I32;
436}
437impl Jpeg2000Element for u32 {
438 const DTYPE: Jpeg2000DType = Jpeg2000DType::U32;
439}
440impl Jpeg2000Element for i64 {
441 const DTYPE: Jpeg2000DType = Jpeg2000DType::I64;
442}
443impl Jpeg2000Element for u64 {
444 const DTYPE: Jpeg2000DType = Jpeg2000DType::U64;
445}
446
447#[derive(Serialize, Deserialize)]
448struct CompressionHeader<'a> {
449 dtype: Jpeg2000DType,
450 #[serde(borrow)]
451 shape: Cow<'a, [usize]>,
452 version: Jpeg2000CodecVersion,
453}
454
455#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
457#[expect(missing_docs)]
458pub enum Jpeg2000DType {
459 #[serde(rename = "i8", alias = "int8")]
460 I8,
461 #[serde(rename = "u8", alias = "uint8")]
462 U8,
463 #[serde(rename = "i16", alias = "int16")]
464 I16,
465 #[serde(rename = "u16", alias = "uint16")]
466 U16,
467 #[serde(rename = "i32", alias = "int32")]
468 I32,
469 #[serde(rename = "u32", alias = "uint32")]
470 U32,
471 #[serde(rename = "i64", alias = "int64")]
472 I64,
473 #[serde(rename = "u64", alias = "uint64")]
474 U64,
475}
476
477impl fmt::Display for Jpeg2000DType {
478 fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
479 fmt.write_str(match self {
480 Self::I8 => "i8",
481 Self::U8 => "u8",
482 Self::I16 => "i16",
483 Self::U16 => "u16",
484 Self::I32 => "i32",
485 Self::U32 => "u32",
486 Self::I64 => "i64",
487 Self::U64 => "u64",
488 })
489 }
490}
491
492#[cfg(test)]
493#[allow(clippy::unwrap_used)]
494mod tests {
495 use ndarray::{Ix0, Ix1, Ix2, Ix3, Ix4};
496
497 use super::*;
498
499 #[test]
500 fn zero_length() {
501 std::mem::drop(simple_logger::init());
502
503 let encoded = compress(
504 Array::<i16, _>::from_shape_vec([3, 0], vec![]).unwrap(),
505 &Jpeg2000CompressionMode::PSNR { psnr: 42.0 },
506 )
507 .unwrap();
508 let decoded = decompress(&encoded).unwrap();
509
510 assert_eq!(decoded.dtype(), AnyArrayDType::I16);
511 assert!(decoded.is_empty());
512 assert_eq!(decoded.shape(), &[3, 0]);
513 }
514
515 #[test]
516 fn small_2d() {
517 std::mem::drop(simple_logger::init());
518
519 let encoded = compress(
520 Array::<i16, _>::from_shape_vec([1, 1], vec![42]).unwrap(),
521 &Jpeg2000CompressionMode::PSNR { psnr: 42.0 },
522 )
523 .unwrap();
524 let decoded = decompress(&encoded).unwrap();
525
526 assert_eq!(decoded.dtype(), AnyArrayDType::I16);
527 assert_eq!(decoded.len(), 1);
528 assert_eq!(decoded.shape(), &[1, 1]);
529 }
530
531 #[test]
532 fn small_lossless_types() {
533 macro_rules! check {
534 ($T:ident($t:ident)) => {
535 check! { $T($t,$t::MIN,$t::MAX) }
536 };
537 ($T:ident($t:ident,$min:expr,$max:expr)) => {
538 let data = Array::<$t, _>::from_shape_vec([4, 1], vec![$min, 0, 42, $max]).unwrap();
539
540 let encoded = compress(
541 data.view(),
542 &Jpeg2000CompressionMode::Lossless,
543 )
544 .unwrap();
545 let decoded = decompress(&encoded).unwrap();
546
547 assert_eq!(decoded.len(), 4);
548 assert_eq!(decoded.shape(), &[4, 1]);
549 assert_eq!(decoded, AnyArray::$T(data.into_dyn()));
550 };
551 ($($T:ident($($tt:tt),*)),*) => {
552 $(check! { $T($($tt),*) })*
553 };
554 }
555
556 check! {
557 I8(i8), U8(u8), I16(i16), U16(u16),
558 I32(i32,(i32::MIN/(1<<7)),(i32::MAX/(1<<7))),
559 U32(u32,(u32::MIN),(u32::MAX/(1<<7))),
560 I64(i64,(i64::MIN/(1<<(32+7))),(i64::MAX/(1<<(32+7)))),
561 U64(u64,(u64::MIN),(u64::MAX/(1<<(32+7))))
562 }
563 }
564
565 #[test]
566 fn out_of_range() {
567 macro_rules! check {
568 ($T:ident($t:ident,$($v:expr),*)) => {
569 $(
570 let data = Array::<$t, _>::from_shape_vec([1, 1], vec![$v]).unwrap();
571 compress(
572 data.view(),
573 &Jpeg2000CompressionMode::Lossless,
574 )
575 .unwrap_err();
576 )*
577 };
578 ($($T:ident($($tt:tt),*)),*) => {
579 $(check! { $T($($tt),*) })*
580 };
581 }
582
583 check! {
584 I32(i32,(i32::MIN),(i32::MAX)), U32(u32,(u32::MAX)),
585 I64(i64,(i64::MIN),(i64::MAX)), U64(u64,(u64::MAX))
586 }
587 }
588
589 #[test]
590 fn large_2d() {
591 std::mem::drop(simple_logger::init());
592
593 let encoded = compress(
594 Array::<i16, _>::zeros((64, 64)),
595 &Jpeg2000CompressionMode::PSNR { psnr: 42.0 },
596 )
597 .unwrap();
598 let decoded = decompress(&encoded).unwrap();
599
600 assert_eq!(decoded.dtype(), AnyArrayDType::I16);
601 assert_eq!(decoded.len(), 64 * 64);
602 assert_eq!(decoded.shape(), &[64, 64]);
603 }
604
605 #[test]
606 fn all_modes() {
607 std::mem::drop(simple_logger::init());
608
609 for mode in [
610 Jpeg2000CompressionMode::PSNR { psnr: 42.0 },
611 Jpeg2000CompressionMode::Rate { rate: 5.0 },
612 Jpeg2000CompressionMode::Lossless,
613 ] {
614 let encoded = compress(Array::<i16, _>::zeros((64, 64)), &mode).unwrap();
615 let decoded = decompress(&encoded).unwrap();
616
617 assert_eq!(decoded.dtype(), AnyArrayDType::I16);
618 assert_eq!(decoded.len(), 64 * 64);
619 assert_eq!(decoded.shape(), &[64, 64]);
620 }
621 }
622
623 #[test]
624 fn many_dimensions() {
625 std::mem::drop(simple_logger::init());
626
627 for data in [
628 Array::<i16, Ix0>::from_shape_vec([], vec![42])
629 .unwrap()
630 .into_dyn(),
631 Array::<i16, Ix1>::from_shape_vec([2], vec![1, 2])
632 .unwrap()
633 .into_dyn(),
634 Array::<i16, Ix2>::from_shape_vec([2, 2], vec![1, 2, 3, 4])
635 .unwrap()
636 .into_dyn(),
637 Array::<i16, Ix3>::from_shape_vec([2, 2, 2], vec![1, 2, 3, 4, 5, 6, 7, 8])
638 .unwrap()
639 .into_dyn(),
640 Array::<i16, Ix4>::from_shape_vec(
641 [2, 2, 2, 2],
642 vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
643 )
644 .unwrap()
645 .into_dyn(),
646 ] {
647 let encoded = compress(data.view(), &Jpeg2000CompressionMode::Lossless).unwrap();
648 let decoded = decompress(&encoded).unwrap();
649
650 assert_eq!(decoded, AnyArray::I16(data));
651 }
652 }
653}