1use alloc::vec::Vec;
4
5use signinum_core::{BackendKind, Unsupported};
6use signinum_j2k_native::{DecodeSettings, EncodeOptions, EncodeProgressionOrder, Image};
7
8use crate::{
9 adapter::encode_stage::{
10 J2kEncodeDispatchReport, J2kEncodeStageAccelerator, NativeEncodeStageAdapter,
11 },
12 J2kError,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub enum EncodeBackendPreference {
18 #[default]
20 Auto,
21 CpuOnly,
23 RequireDevice,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
29pub enum J2kProgressionOrder {
30 #[default]
32 Lrcp,
33 Rlcp,
35 Rpcl,
37 Pcrl,
39 Cprl,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub enum J2kBlockCodingMode {
46 #[default]
48 Classic,
49 HighThroughput,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
55pub enum ReversibleTransform {
56 #[default]
58 Rct53,
59 None53,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
65pub enum J2kEncodeValidation {
66 #[default]
69 CpuRoundTrip,
70 External,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub struct J2kLosslessEncodeOptions {
79 pub backend: EncodeBackendPreference,
81 pub block_coding_mode: J2kBlockCodingMode,
83 pub progression: J2kProgressionOrder,
85 pub max_decomposition_levels: Option<u8>,
89 pub reversible_transform: ReversibleTransform,
91 pub validation: J2kEncodeValidation,
93}
94
95impl Default for J2kLosslessEncodeOptions {
96 fn default() -> Self {
97 Self {
98 backend: EncodeBackendPreference::Auto,
99 block_coding_mode: J2kBlockCodingMode::Classic,
100 progression: J2kProgressionOrder::Lrcp,
101 max_decomposition_levels: None,
102 reversible_transform: ReversibleTransform::Rct53,
103 validation: J2kEncodeValidation::CpuRoundTrip,
104 }
105 }
106}
107
108impl J2kLosslessEncodeOptions {
109 pub const fn new(
111 backend: EncodeBackendPreference,
112 block_coding_mode: J2kBlockCodingMode,
113 progression: J2kProgressionOrder,
114 max_decomposition_levels: Option<u8>,
115 reversible_transform: ReversibleTransform,
116 validation: J2kEncodeValidation,
117 ) -> Self {
118 Self {
119 backend,
120 block_coding_mode,
121 progression,
122 max_decomposition_levels,
123 reversible_transform,
124 validation,
125 }
126 }
127
128 #[must_use]
130 pub const fn with_backend(mut self, backend: EncodeBackendPreference) -> Self {
131 self.backend = backend;
132 self
133 }
134
135 #[must_use]
137 pub const fn with_accelerated_backend(self) -> Self {
138 self.with_backend(EncodeBackendPreference::Auto)
139 }
140
141 #[must_use]
143 pub const fn with_cpu_only_backend(self) -> Self {
144 self.with_backend(EncodeBackendPreference::CpuOnly)
145 }
146
147 #[must_use]
149 pub const fn with_strict_device_backend(self) -> Self {
150 self.with_backend(EncodeBackendPreference::RequireDevice)
151 }
152
153 #[must_use]
155 pub const fn with_block_coding_mode(mut self, block_coding_mode: J2kBlockCodingMode) -> Self {
156 self.block_coding_mode = block_coding_mode;
157 self
158 }
159
160 #[must_use]
162 pub const fn with_progression(mut self, progression: J2kProgressionOrder) -> Self {
163 self.progression = progression;
164 self
165 }
166
167 #[must_use]
169 pub const fn with_max_decomposition_levels(
170 mut self,
171 max_decomposition_levels: Option<u8>,
172 ) -> Self {
173 self.max_decomposition_levels = max_decomposition_levels;
174 self
175 }
176
177 #[must_use]
179 pub const fn with_reversible_transform(
180 mut self,
181 reversible_transform: ReversibleTransform,
182 ) -> Self {
183 self.reversible_transform = reversible_transform;
184 self
185 }
186
187 #[must_use]
189 pub const fn with_validation(mut self, validation: J2kEncodeValidation) -> Self {
190 self.validation = validation;
191 self
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq)]
197pub enum J2kRateTarget {
198 BitsPerPixel(f64),
200 Bytes(u64),
202 PsnrDb(f64),
204}
205
206#[derive(Debug, Clone, Copy, PartialEq)]
208pub struct J2kQualityLayer {
209 pub target: J2kRateTarget,
211}
212
213impl J2kQualityLayer {
214 #[must_use]
216 pub const fn new(target: J2kRateTarget) -> Self {
217 Self { target }
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
223pub enum J2kMarkerSegment {
224 Sop,
226 Eph,
228 Tlm,
230 Plt,
232 Plm,
234}
235
236#[derive(Debug, Clone, PartialEq)]
238#[non_exhaustive]
239pub struct J2kLossyEncodeOptions {
240 pub backend: EncodeBackendPreference,
242 pub block_coding_mode: J2kBlockCodingMode,
244 pub progression: J2kProgressionOrder,
246 pub max_decomposition_levels: Option<u8>,
248 pub rate_target: Option<J2kRateTarget>,
250 pub quality_layers: Vec<J2kQualityLayer>,
252 pub tile_size: Option<(u32, u32)>,
254 pub precinct_exponents: Vec<(u8, u8)>,
256 pub marker_segments: Vec<J2kMarkerSegment>,
258 pub psnr_tolerance_db: f64,
260 pub psnr_iteration_budget: u8,
262 pub validation: J2kEncodeValidation,
264}
265
266impl Default for J2kLossyEncodeOptions {
267 fn default() -> Self {
268 Self {
269 backend: EncodeBackendPreference::Auto,
270 block_coding_mode: J2kBlockCodingMode::Classic,
271 progression: J2kProgressionOrder::Lrcp,
272 max_decomposition_levels: None,
273 rate_target: None,
274 quality_layers: Vec::new(),
275 tile_size: None,
276 precinct_exponents: Vec::new(),
277 marker_segments: Vec::new(),
278 psnr_tolerance_db: 0.25,
279 psnr_iteration_budget: 8,
280 validation: J2kEncodeValidation::CpuRoundTrip,
281 }
282 }
283}
284
285impl J2kLossyEncodeOptions {
286 #[must_use]
288 pub fn with_backend(mut self, backend: EncodeBackendPreference) -> Self {
289 self.backend = backend;
290 self
291 }
292
293 #[must_use]
295 pub fn with_accelerated_backend(self) -> Self {
296 self.with_backend(EncodeBackendPreference::Auto)
297 }
298
299 #[must_use]
301 pub fn with_cpu_only_backend(self) -> Self {
302 self.with_backend(EncodeBackendPreference::CpuOnly)
303 }
304
305 #[must_use]
307 pub fn with_strict_device_backend(self) -> Self {
308 self.with_backend(EncodeBackendPreference::RequireDevice)
309 }
310
311 #[must_use]
313 pub fn with_block_coding_mode(mut self, block_coding_mode: J2kBlockCodingMode) -> Self {
314 self.block_coding_mode = block_coding_mode;
315 self
316 }
317
318 #[must_use]
320 pub fn with_progression(mut self, progression: J2kProgressionOrder) -> Self {
321 self.progression = progression;
322 self
323 }
324
325 #[must_use]
327 pub fn with_max_decomposition_levels(mut self, max_decomposition_levels: Option<u8>) -> Self {
328 self.max_decomposition_levels = max_decomposition_levels;
329 self
330 }
331
332 #[must_use]
334 pub fn with_rate_target(mut self, rate_target: Option<J2kRateTarget>) -> Self {
335 self.rate_target = rate_target;
336 self
337 }
338
339 #[must_use]
341 pub fn with_quality_layers(mut self, quality_layers: Vec<J2kQualityLayer>) -> Self {
342 self.quality_layers = quality_layers;
343 self
344 }
345
346 #[must_use]
348 pub fn with_marker_segments(mut self, marker_segments: Vec<J2kMarkerSegment>) -> Self {
349 self.marker_segments = marker_segments;
350 self
351 }
352
353 #[must_use]
355 pub fn with_validation(mut self, validation: J2kEncodeValidation) -> Self {
356 self.validation = validation;
357 self
358 }
359}
360
361#[derive(Debug, Clone, Copy)]
363pub struct J2kLosslessSamples<'a> {
364 pub data: &'a [u8],
366 pub width: u32,
368 pub height: u32,
370 pub components: u8,
373 pub bit_depth: u8,
375 pub signed: bool,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380struct SampleGeometry {
381 expected_bytes: usize,
382}
383
384fn validate_sample_geometry(
385 data: &[u8],
386 width: u32,
387 height: u32,
388 components: u8,
389 bit_depth: u8,
390 component_what: &'static str,
391 bit_depth_what: &'static str,
392) -> Result<SampleGeometry, J2kError> {
393 if width == 0 || height == 0 {
394 return Err(J2kError::InvalidSamples {
395 what: "dimensions must be non-zero".to_string(),
396 });
397 }
398 if !(1..=4).contains(&components) {
399 return Err(J2kError::Unsupported(Unsupported {
400 what: component_what,
401 }));
402 }
403 if bit_depth == 0 || bit_depth > 16 {
404 return Err(J2kError::Unsupported(Unsupported {
405 what: bit_depth_what,
406 }));
407 }
408 let bytes_per_sample = if bit_depth <= 8 { 1usize } else { 2usize };
409 let expected_bytes = (width as usize)
410 .checked_mul(height as usize)
411 .and_then(|px| px.checked_mul(components as usize))
412 .and_then(|samples| samples.checked_mul(bytes_per_sample))
413 .ok_or(J2kError::DimensionOverflow { width, height })?;
414 if data.len() != expected_bytes {
415 let what = if data.len() < expected_bytes {
416 format!(
417 "pixel data too short: expected {expected_bytes} bytes, got {}",
418 data.len()
419 )
420 } else {
421 format!(
422 "pixel data has trailing bytes: expected {expected_bytes} bytes, got {}",
423 data.len()
424 )
425 };
426 return Err(J2kError::InvalidSamples { what });
427 }
428 Ok(SampleGeometry { expected_bytes })
429}
430
431impl<'a> J2kLosslessSamples<'a> {
432 pub fn new(
434 data: &'a [u8],
435 width: u32,
436 height: u32,
437 components: u8,
438 bit_depth: u8,
439 signed: bool,
440 ) -> Result<Self, J2kError> {
441 let geometry = validate_sample_geometry(
442 data,
443 width,
444 height,
445 components,
446 bit_depth,
447 "JPEG 2000 lossless encode supports 1-4 component samples",
448 "JPEG 2000 lossless encode supports 1-16 bits per sample",
449 )?;
450 debug_assert_eq!(geometry.expected_bytes, data.len());
451 Ok(Self {
452 data,
453 width,
454 height,
455 components,
456 bit_depth,
457 signed,
458 })
459 }
460}
461
462#[derive(Debug, Clone, Copy)]
464pub struct J2kLossySamples<'a> {
465 pub data: &'a [u8],
467 pub width: u32,
469 pub height: u32,
471 pub components: u8,
473 pub bit_depth: u8,
475 pub signed: bool,
477}
478
479impl<'a> J2kLossySamples<'a> {
480 pub fn new(
482 data: &'a [u8],
483 width: u32,
484 height: u32,
485 components: u8,
486 bit_depth: u8,
487 signed: bool,
488 ) -> Result<Self, J2kError> {
489 let geometry = validate_sample_geometry(
490 data,
491 width,
492 height,
493 components,
494 bit_depth,
495 "JPEG 2000 lossy encode supports 1-4 component samples",
496 "JPEG 2000 lossy encode supports 1-16 bits per sample; 17-38 bit encode is not supported",
497 )?;
498 debug_assert_eq!(geometry.expected_bytes, data.len());
499 Ok(Self {
500 data,
501 width,
502 height,
503 components,
504 bit_depth,
505 signed,
506 })
507 }
508}
509
510#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct EncodedJ2k {
513 pub codestream: Vec<u8>,
515 pub backend: BackendKind,
517 pub dispatch_report: J2kEncodeDispatchReport,
523 pub width: u32,
525 pub height: u32,
527 pub components: u8,
529 pub bit_depth: u8,
531 pub signed: bool,
533}
534
535#[derive(Debug, Clone, PartialEq)]
537pub struct J2kLossyEncodeReport {
538 pub target: Option<J2kRateTarget>,
540 pub quality_layers: u16,
542 pub quantization_scale: f32,
544 pub actual_bytes: u64,
546 pub actual_bits_per_pixel: f64,
548 pub psnr_db: Option<f64>,
550 pub ht_rate_granularity_bytes: Option<u64>,
552}
553
554#[derive(Debug, Clone, PartialEq)]
556pub struct EncodedLossyJ2k {
557 pub codestream: Vec<u8>,
559 pub backend: BackendKind,
561 pub dispatch_report: J2kEncodeDispatchReport,
567 pub width: u32,
569 pub height: u32,
571 pub components: u8,
573 pub bit_depth: u8,
575 pub signed: bool,
577 pub report: J2kLossyEncodeReport,
579}
580
581pub fn encode_j2k_lossless(
583 samples: J2kLosslessSamples<'_>,
584 options: &J2kLosslessEncodeOptions,
585) -> Result<EncodedJ2k, J2kError> {
586 let backend = resolve_encode_backend(options.backend)?;
587 let codestream = encode_cpu(samples, *options)?;
588 validate_lossless_roundtrip(samples, &codestream, options.validation)?;
589 Ok(EncodedJ2k {
590 codestream,
591 backend,
592 dispatch_report: J2kEncodeDispatchReport::default(),
593 width: samples.width,
594 height: samples.height,
595 components: samples.components,
596 bit_depth: samples.bit_depth,
597 signed: samples.signed,
598 })
599}
600
601pub fn encode_j2k_lossless_with_accelerator(
607 samples: J2kLosslessSamples<'_>,
608 options: &J2kLosslessEncodeOptions,
609 accelerated_backend: BackendKind,
610 accelerator: &mut impl J2kEncodeStageAccelerator,
611) -> Result<EncodedJ2k, J2kError> {
612 if options.backend == EncodeBackendPreference::CpuOnly {
613 return encode_j2k_lossless(samples, options);
614 }
615
616 let before = accelerator.dispatch_report();
617 let required_stages = required_encode_stages(samples, *options, accelerated_backend);
618 let codestream = encode_with_native_accelerator(samples, *options, accelerator)?;
619 let dispatch = accelerator.dispatch_report().saturating_delta(before);
620 validate_lossless_roundtrip(samples, &codestream, options.validation)?;
621
622 let backend = resolve_accelerated_encode_backend(
623 options.backend,
624 accelerated_backend,
625 dispatch,
626 required_stages,
627 )?;
628 Ok(EncodedJ2k {
629 codestream,
630 backend,
631 dispatch_report: dispatch,
632 width: samples.width,
633 height: samples.height,
634 components: samples.components,
635 bit_depth: samples.bit_depth,
636 signed: samples.signed,
637 })
638}
639
640pub fn encode_j2k_lossy(
642 samples: J2kLossySamples<'_>,
643 options: &J2kLossyEncodeOptions,
644) -> Result<EncodedLossyJ2k, J2kError> {
645 validate_lossy_options(options)?;
646 let target = effective_lossy_target(options)?;
647 let attempt = encode_lossy_targeted(samples, options, target, |scale| {
648 encode_cpu_lossy(samples, options, scale)
649 })?;
650 let report = lossy_report(samples, options, target, &attempt)?;
651 Ok(EncodedLossyJ2k {
652 codestream: attempt.codestream,
653 backend: resolve_encode_backend(options.backend)?,
654 dispatch_report: J2kEncodeDispatchReport::default(),
655 width: samples.width,
656 height: samples.height,
657 components: samples.components,
658 bit_depth: samples.bit_depth,
659 signed: samples.signed,
660 report,
661 })
662}
663
664pub fn encode_j2k_lossy_with_accelerator(
666 samples: J2kLossySamples<'_>,
667 options: &J2kLossyEncodeOptions,
668 accelerated_backend: BackendKind,
669 accelerator: &mut impl J2kEncodeStageAccelerator,
670) -> Result<EncodedLossyJ2k, J2kError> {
671 if options.backend == EncodeBackendPreference::CpuOnly {
672 return encode_j2k_lossy(samples, options);
673 }
674
675 validate_lossy_options(options)?;
676 let target = effective_lossy_target(options)?;
677 let before = accelerator.dispatch_report();
678 let required_stages = required_lossy_encode_stages(samples, options, accelerated_backend);
679 let attempt = encode_lossy_targeted(samples, options, target, |scale| {
680 encode_lossy_with_native_accelerator(samples, options, scale, accelerator)
681 })?;
682 let dispatch = accelerator.dispatch_report().saturating_delta(before);
683 let backend = resolve_accelerated_encode_backend(
684 options.backend,
685 accelerated_backend,
686 dispatch,
687 required_stages,
688 )?;
689 let report = lossy_report(samples, options, target, &attempt)?;
690 Ok(EncodedLossyJ2k {
691 codestream: attempt.codestream,
692 backend,
693 dispatch_report: dispatch,
694 width: samples.width,
695 height: samples.height,
696 components: samples.components,
697 bit_depth: samples.bit_depth,
698 signed: samples.signed,
699 report,
700 })
701}
702
703fn resolve_encode_backend(preference: EncodeBackendPreference) -> Result<BackendKind, J2kError> {
704 match preference {
705 EncodeBackendPreference::Auto | EncodeBackendPreference::CpuOnly => Ok(BackendKind::Cpu),
706 EncodeBackendPreference::RequireDevice => Err(J2kError::Unsupported(Unsupported {
707 what: "device JPEG 2000 lossless encode backend is unavailable",
708 })),
709 }
710}
711
712fn resolve_accelerated_encode_backend(
713 preference: EncodeBackendPreference,
714 accelerated_backend: BackendKind,
715 dispatch: J2kEncodeDispatchReport,
716 required_stages: RequiredEncodeStages,
717) -> Result<BackendKind, J2kError> {
718 if required_stages.satisfied_by(dispatch) {
719 return Ok(accelerated_backend);
720 }
721 match preference {
722 EncodeBackendPreference::RequireDevice => Err(J2kError::Unsupported(Unsupported {
723 what: required_stages.missing_message(dispatch),
724 })),
725 EncodeBackendPreference::Auto | EncodeBackendPreference::CpuOnly => Ok(BackendKind::Cpu),
726 }
727}
728
729fn encode_cpu(
730 samples: J2kLosslessSamples<'_>,
731 options: J2kLosslessEncodeOptions,
732) -> Result<Vec<u8>, J2kError> {
733 let options = native_lossless_options(samples, options);
734 signinum_j2k_native::encode(
735 samples.data,
736 samples.width,
737 samples.height,
738 samples.components,
739 samples.bit_depth,
740 samples.signed,
741 &options,
742 )
743 .map_err(|err| J2kError::Backend(format!("JPEG 2000 lossless encode failed: {err}")))
744}
745
746fn encode_with_native_accelerator(
747 samples: J2kLosslessSamples<'_>,
748 options: J2kLosslessEncodeOptions,
749 accelerator: &mut impl J2kEncodeStageAccelerator,
750) -> Result<Vec<u8>, J2kError> {
751 let options = native_lossless_options(samples, options);
752 let mut native_accelerator = NativeEncodeStageAdapter::new(accelerator);
753 signinum_j2k_native::encode_with_accelerator(
754 samples.data,
755 samples.width,
756 samples.height,
757 samples.components,
758 samples.bit_depth,
759 samples.signed,
760 &options,
761 &mut native_accelerator,
762 )
763 .map_err(|err| J2kError::Backend(format!("JPEG 2000 lossless encode failed: {err}")))
764}
765
766struct LossyAttempt {
767 codestream: Vec<u8>,
768 quantization_scale: f32,
769}
770
771fn encode_cpu_lossy(
772 samples: J2kLossySamples<'_>,
773 options: &J2kLossyEncodeOptions,
774 quantization_scale: f32,
775) -> Result<Vec<u8>, J2kError> {
776 let options = native_lossy_options(samples, options, quantization_scale)?;
777 signinum_j2k_native::encode(
778 samples.data,
779 samples.width,
780 samples.height,
781 samples.components,
782 samples.bit_depth,
783 samples.signed,
784 &options,
785 )
786 .map_err(|err| J2kError::Backend(format!("JPEG 2000 lossy encode failed: {err}")))
787}
788
789fn encode_lossy_with_native_accelerator(
790 samples: J2kLossySamples<'_>,
791 options: &J2kLossyEncodeOptions,
792 quantization_scale: f32,
793 accelerator: &mut impl J2kEncodeStageAccelerator,
794) -> Result<Vec<u8>, J2kError> {
795 let options = native_lossy_options(samples, options, quantization_scale)?;
796 let mut native_accelerator = NativeEncodeStageAdapter::new(accelerator);
797 signinum_j2k_native::encode_with_accelerator(
798 samples.data,
799 samples.width,
800 samples.height,
801 samples.components,
802 samples.bit_depth,
803 samples.signed,
804 &options,
805 &mut native_accelerator,
806 )
807 .map_err(|err| J2kError::Backend(format!("JPEG 2000 lossy encode failed: {err}")))
808}
809
810fn encode_lossy_targeted(
811 samples: J2kLossySamples<'_>,
812 options: &J2kLossyEncodeOptions,
813 target: Option<J2kRateTarget>,
814 mut encode_at_scale: impl FnMut(f32) -> Result<Vec<u8>, J2kError>,
815) -> Result<LossyAttempt, J2kError> {
816 match target {
817 None => {
818 let codestream = encode_at_scale(1.0)?;
819 Ok(LossyAttempt {
820 codestream,
821 quantization_scale: 1.0,
822 })
823 }
824 Some(J2kRateTarget::Bytes(bytes)) => {
825 encode_lossy_to_byte_target(samples, options, bytes, encode_at_scale)
826 }
827 Some(J2kRateTarget::BitsPerPixel(bits_per_pixel)) => {
828 let target_bytes = target_bytes_for_bpp(samples, bits_per_pixel)?;
829 encode_lossy_to_byte_target(samples, options, target_bytes, encode_at_scale)
830 }
831 Some(J2kRateTarget::PsnrDb(psnr_db)) => {
832 encode_lossy_to_psnr_target(samples, options, psnr_db, encode_at_scale)
833 }
834 }
835}
836
837fn encode_lossy_to_byte_target(
838 _samples: J2kLossySamples<'_>,
839 options: &J2kLossyEncodeOptions,
840 target_bytes: u64,
841 mut encode_at_scale: impl FnMut(f32) -> Result<Vec<u8>, J2kError>,
842) -> Result<LossyAttempt, J2kError> {
843 let tolerance = byte_target_tolerance(target_bytes);
844 let mut low = 1.0f32;
845 let mut high = 1.0f32;
846 let mut best = LossyAttempt {
847 codestream: encode_at_scale(high)?,
848 quantization_scale: high,
849 };
850 let mut best_diff = byte_target_diff(best.codestream.len() as u64, target_bytes);
851
852 while best.codestream.len() as u64 > target_bytes.saturating_add(tolerance)
853 && high < 1_048_576.0
854 {
855 low = high;
856 high *= 2.0;
857 let codestream = encode_at_scale(high)?;
858 let diff = byte_target_diff(codestream.len() as u64, target_bytes);
859 if diff < best_diff {
860 best = LossyAttempt {
861 codestream,
862 quantization_scale: high,
863 };
864 best_diff = diff;
865 }
866 }
867
868 if best.codestream.len() as u64 > target_bytes.saturating_add(tolerance) {
869 return Err(J2kError::RateTargetUnreachable {
870 target: format!("{target_bytes} bytes"),
871 best: format!("{} bytes", best.codestream.len()),
872 });
873 }
874
875 for _ in 0..options.psnr_iteration_budget.max(1) {
876 let mid = (low + high) * 0.5;
877 let codestream = encode_at_scale(mid)?;
878 let len = codestream.len() as u64;
879 let diff = byte_target_diff(len, target_bytes);
880 if diff < best_diff {
881 best = LossyAttempt {
882 codestream,
883 quantization_scale: mid,
884 };
885 best_diff = diff;
886 }
887 if len > target_bytes {
888 low = mid;
889 } else {
890 high = mid;
891 }
892 }
893
894 Ok(best)
895}
896
897fn encode_lossy_to_psnr_target(
898 samples: J2kLossySamples<'_>,
899 options: &J2kLossyEncodeOptions,
900 target_psnr_db: f64,
901 mut encode_at_scale: impl FnMut(f32) -> Result<Vec<u8>, J2kError>,
902) -> Result<LossyAttempt, J2kError> {
903 let tolerance = options.psnr_tolerance_db;
904 let mut low = 1.0f32;
905 let mut high = 1.0f32;
906 let mut best = LossyAttempt {
907 codestream: encode_at_scale(high)?,
908 quantization_scale: high,
909 };
910 let mut best_psnr = decoded_psnr(samples, &best.codestream)?;
911 if best_psnr + tolerance < target_psnr_db {
912 return Err(J2kError::RateTargetUnreachable {
913 target: format!("{target_psnr_db:.3} dB"),
914 best: format!("{best_psnr:.3} dB"),
915 });
916 }
917
918 for _ in 0..options.psnr_iteration_budget.max(1) {
919 high *= 2.0;
920 let codestream = encode_at_scale(high)?;
921 let psnr = decoded_psnr(samples, &codestream)?;
922 if psnr + tolerance >= target_psnr_db {
923 best = LossyAttempt {
924 codestream,
925 quantization_scale: high,
926 };
927 best_psnr = psnr;
928 low = high;
929 } else {
930 break;
931 }
932 }
933
934 for _ in 0..options.psnr_iteration_budget.max(1) {
935 let mid = (low + high) * 0.5;
936 let codestream = encode_at_scale(mid)?;
937 let psnr = decoded_psnr(samples, &codestream)?;
938 if psnr + tolerance >= target_psnr_db {
939 best = LossyAttempt {
940 codestream,
941 quantization_scale: mid,
942 };
943 best_psnr = psnr;
944 low = mid;
945 } else {
946 high = mid;
947 }
948 }
949
950 let _ = best_psnr;
951 Ok(best)
952}
953
954fn native_lossless_options(
955 samples: J2kLosslessSamples<'_>,
956 options: J2kLosslessEncodeOptions,
957) -> EncodeOptions {
958 let progression_order = native_progression_order(options.progression);
959 EncodeOptions {
960 reversible: true,
961 num_decomposition_levels: j2k_lossless_decomposition_levels_for_options(samples, options),
962 use_ht_block_coding: options.block_coding_mode == J2kBlockCodingMode::HighThroughput,
963 progression_order,
964 write_tlm: options.progression == J2kProgressionOrder::Rpcl,
965 use_mct: options.reversible_transform == ReversibleTransform::Rct53,
966 validate_high_throughput_codestream: false,
967 ..EncodeOptions::default()
968 }
969}
970
971fn native_lossy_options(
972 samples: J2kLossySamples<'_>,
973 options: &J2kLossyEncodeOptions,
974 quantization_scale: f32,
975) -> Result<EncodeOptions, J2kError> {
976 let num_layers = lossy_quality_layer_count(options);
977 Ok(EncodeOptions {
978 reversible: false,
979 num_decomposition_levels: j2k_lossy_decomposition_levels_for_options(samples, options),
980 use_ht_block_coding: options.block_coding_mode == J2kBlockCodingMode::HighThroughput,
981 progression_order: native_progression_order(options.progression),
982 write_tlm: options.marker_segments.contains(&J2kMarkerSegment::Tlm),
983 write_plt: options.marker_segments.contains(&J2kMarkerSegment::Plt),
984 write_plm: options.marker_segments.contains(&J2kMarkerSegment::Plm),
985 write_sop: options.marker_segments.contains(&J2kMarkerSegment::Sop),
986 write_eph: options.marker_segments.contains(&J2kMarkerSegment::Eph),
987 use_mct: samples.components >= 3,
988 num_layers,
989 quality_layer_byte_targets: lossy_quality_layer_byte_targets(samples, options)?,
990 tile_size: options.tile_size,
991 precinct_exponents: options.precinct_exponents.clone(),
992 validate_high_throughput_codestream: false,
993 irreversible_quantization_scale: quantization_scale,
994 ..EncodeOptions::default()
995 })
996}
997
998fn lossy_quality_layer_byte_targets(
999 samples: J2kLossySamples<'_>,
1000 options: &J2kLossyEncodeOptions,
1001) -> Result<Vec<u64>, J2kError> {
1002 if options.quality_layers.len() <= 1 {
1003 return Ok(Vec::new());
1004 }
1005
1006 let mut targets = Vec::with_capacity(options.quality_layers.len());
1007 for layer in &options.quality_layers {
1008 match layer.target {
1009 J2kRateTarget::Bytes(bytes) => targets.push(bytes),
1010 J2kRateTarget::BitsPerPixel(bits_per_pixel) => {
1011 targets.push(target_bytes_for_bpp(samples, bits_per_pixel)?);
1012 }
1013 J2kRateTarget::PsnrDb(_) => return Ok(Vec::new()),
1014 }
1015 }
1016 if targets.windows(2).any(|pair| pair[0] > pair[1]) {
1017 return Err(J2kError::Unsupported(Unsupported {
1018 what: "JPEG 2000 lossy quality layer targets must be cumulative and monotonic",
1019 }));
1020 }
1021 Ok(targets)
1022}
1023
1024pub(crate) fn native_progression_order(progression: J2kProgressionOrder) -> EncodeProgressionOrder {
1025 match progression {
1026 J2kProgressionOrder::Lrcp => EncodeProgressionOrder::Lrcp,
1027 J2kProgressionOrder::Rlcp => EncodeProgressionOrder::Rlcp,
1028 J2kProgressionOrder::Rpcl => EncodeProgressionOrder::Rpcl,
1029 J2kProgressionOrder::Pcrl => EncodeProgressionOrder::Pcrl,
1030 J2kProgressionOrder::Cprl => EncodeProgressionOrder::Cprl,
1031 }
1032}
1033
1034const MIN_LOSSLESS_DWT_DIMENSION: u32 = 64;
1035
1036pub fn j2k_lossless_decomposition_levels(samples: J2kLosslessSamples<'_>) -> u8 {
1038 j2k_lossless_decomposition_levels_for_progression(samples, J2kProgressionOrder::Lrcp)
1039}
1040
1041pub fn j2k_lossless_decomposition_levels_for_progression(
1043 samples: J2kLosslessSamples<'_>,
1044 progression: J2kProgressionOrder,
1045) -> u8 {
1046 if matches!(
1047 progression,
1048 J2kProgressionOrder::Rpcl | J2kProgressionOrder::Pcrl | J2kProgressionOrder::Cprl
1049 ) {
1050 return j2k_rpcl_lossless_decomposition_levels(samples);
1051 }
1052
1053 if samples.width.min(samples.height) < MIN_LOSSLESS_DWT_DIMENSION {
1054 return 0;
1055 }
1056
1057 1
1058}
1059
1060fn j2k_lossy_decomposition_levels_for_options(
1061 samples: J2kLossySamples<'_>,
1062 options: &J2kLossyEncodeOptions,
1063) -> u8 {
1064 let levels = if matches!(
1065 options.progression,
1066 J2kProgressionOrder::Rpcl | J2kProgressionOrder::Pcrl | J2kProgressionOrder::Cprl
1067 ) {
1068 j2k_lossy_position_progression_decomposition_levels(samples)
1069 } else {
1070 u8::from(samples.width.min(samples.height) >= MIN_LOSSLESS_DWT_DIMENSION)
1071 };
1072 options.max_decomposition_levels.map_or(levels, |max| {
1073 levels
1074 .min(max)
1075 .min(max_decomposition_levels(samples.width, samples.height))
1076 })
1077}
1078
1079fn j2k_lossy_position_progression_decomposition_levels(samples: J2kLossySamples<'_>) -> u8 {
1080 j2k_rpcl_lossless_decomposition_levels(J2kLosslessSamples {
1081 data: samples.data,
1082 width: samples.width,
1083 height: samples.height,
1084 components: samples.components,
1085 bit_depth: samples.bit_depth,
1086 signed: samples.signed,
1087 })
1088}
1089
1090pub fn j2k_lossless_decomposition_levels_for_options(
1092 samples: J2kLosslessSamples<'_>,
1093 options: J2kLosslessEncodeOptions,
1094) -> u8 {
1095 let levels = j2k_lossless_decomposition_levels_for_progression(samples, options.progression);
1096 options
1097 .max_decomposition_levels
1098 .map_or(levels, |requested| {
1099 if samples.width.min(samples.height) < MIN_LOSSLESS_DWT_DIMENSION {
1100 return 0;
1101 }
1102 requested.min(max_decomposition_levels(samples.width, samples.height))
1103 })
1104}
1105
1106fn j2k_rpcl_lossless_decomposition_levels(samples: J2kLosslessSamples<'_>) -> u8 {
1107 let mut levels = 0u8;
1108 let mut width = samples.width;
1109 let mut height = samples.height;
1110 let max_levels = max_decomposition_levels(samples.width, samples.height);
1111
1112 while width.min(height) > MIN_LOSSLESS_DWT_DIMENSION && levels < max_levels {
1113 width = width.div_ceil(2);
1114 height = height.div_ceil(2);
1115 levels += 1;
1116 }
1117
1118 levels
1119}
1120
1121fn max_decomposition_levels(width: u32, height: u32) -> u8 {
1122 let min_dim = width.min(height);
1123 if min_dim <= 1 {
1124 return 0;
1125 }
1126 min_dim.ilog2() as u8
1127}
1128
1129#[derive(Debug, Clone, Copy)]
1130struct RequiredEncodeStages {
1131 bits: u16,
1132}
1133
1134impl RequiredEncodeStages {
1135 const DEINTERLEAVE: u16 = 1 << 0;
1136 const FORWARD_RCT: u16 = 1 << 1;
1137 const FORWARD_DWT53: u16 = 1 << 2;
1138 const TIER1_CODE_BLOCK: u16 = 1 << 3;
1139 const HT_CODE_BLOCK: u16 = 1 << 4;
1140 const PACKETIZATION: u16 = 1 << 5;
1141 const QUANTIZE_SUBBAND: u16 = 1 << 6;
1142 const FORWARD_ICT: u16 = 1 << 7;
1143 const FORWARD_DWT97: u16 = 1 << 8;
1144
1145 fn satisfied_by(self, dispatch: J2kEncodeDispatchReport) -> bool {
1146 self.missing_stage(dispatch).is_none()
1147 }
1148
1149 fn missing_message(self, dispatch: J2kEncodeDispatchReport) -> &'static str {
1150 match self.missing_stage(dispatch) {
1151 Some("deinterleave") => {
1152 "requested JPEG 2000 device encode backend did not dispatch deinterleave"
1153 }
1154 Some("forward_rct") => {
1155 "requested JPEG 2000 device encode backend did not dispatch forward_rct"
1156 }
1157 Some("forward_ict") => {
1158 "requested JPEG 2000 device encode backend did not dispatch forward_ict"
1159 }
1160 Some("forward_dwt53") => {
1161 "requested JPEG 2000 device encode backend did not dispatch forward_dwt53"
1162 }
1163 Some("forward_dwt97") => {
1164 "requested JPEG 2000 device encode backend did not dispatch forward_dwt97"
1165 }
1166 Some("tier1_code_block") => {
1167 "requested JPEG 2000 device encode backend did not dispatch tier1_code_block"
1168 }
1169 Some("ht_code_block") => {
1170 "requested JPEG 2000 device encode backend did not dispatch ht_code_block"
1171 }
1172 Some("quantize_subband") => {
1173 "requested JPEG 2000 device encode backend did not dispatch quantize_subband"
1174 }
1175 Some("packetization") => {
1176 "requested JPEG 2000 device encode backend did not dispatch packetization"
1177 }
1178 _ => "requested JPEG 2000 device encode backend did not dispatch",
1179 }
1180 }
1181
1182 fn missing_stage(self, dispatch: J2kEncodeDispatchReport) -> Option<&'static str> {
1183 if self.contains(Self::DEINTERLEAVE) && dispatch.deinterleave == 0 {
1184 return Some("deinterleave");
1185 }
1186 if self.contains(Self::FORWARD_RCT) && dispatch.forward_rct == 0 {
1187 return Some("forward_rct");
1188 }
1189 if self.contains(Self::FORWARD_ICT) && dispatch.forward_ict == 0 {
1190 return Some("forward_ict");
1191 }
1192 if self.contains(Self::FORWARD_DWT53) && dispatch.forward_dwt53 == 0 {
1193 return Some("forward_dwt53");
1194 }
1195 if self.contains(Self::FORWARD_DWT97) && dispatch.forward_dwt97 == 0 {
1196 return Some("forward_dwt97");
1197 }
1198 if self.contains(Self::TIER1_CODE_BLOCK) && dispatch.tier1_code_block == 0 {
1199 return Some("tier1_code_block");
1200 }
1201 if self.contains(Self::HT_CODE_BLOCK) && dispatch.ht_code_block == 0 {
1202 return Some("ht_code_block");
1203 }
1204 if self.contains(Self::QUANTIZE_SUBBAND) && dispatch.quantize_subband == 0 {
1205 return Some("quantize_subband");
1206 }
1207 if self.contains(Self::PACKETIZATION) && dispatch.packetization == 0 {
1208 return Some("packetization");
1209 }
1210 None
1211 }
1212
1213 fn contains(self, stage: u16) -> bool {
1214 self.bits & stage != 0
1215 }
1216}
1217
1218fn required_encode_stages(
1219 samples: J2kLosslessSamples<'_>,
1220 options: J2kLosslessEncodeOptions,
1221 accelerated_backend: BackendKind,
1222) -> RequiredEncodeStages {
1223 let decomposition_levels = j2k_lossless_decomposition_levels_for_options(samples, options);
1224 let high_throughput = options.block_coding_mode == J2kBlockCodingMode::HighThroughput;
1225
1226 let mut bits = RequiredEncodeStages::PACKETIZATION;
1227 if accelerated_backend == BackendKind::Cuda {
1228 bits |= RequiredEncodeStages::DEINTERLEAVE | RequiredEncodeStages::QUANTIZE_SUBBAND;
1229 }
1230 if samples.components >= 3 && options.reversible_transform == ReversibleTransform::Rct53 {
1231 bits |= RequiredEncodeStages::FORWARD_RCT;
1232 }
1233 if decomposition_levels > 0 {
1234 bits |= RequiredEncodeStages::FORWARD_DWT53;
1235 }
1236 if high_throughput {
1237 bits |= RequiredEncodeStages::HT_CODE_BLOCK;
1238 } else {
1239 bits |= RequiredEncodeStages::TIER1_CODE_BLOCK;
1240 }
1241
1242 RequiredEncodeStages { bits }
1243}
1244
1245fn required_lossy_encode_stages(
1246 samples: J2kLossySamples<'_>,
1247 options: &J2kLossyEncodeOptions,
1248 accelerated_backend: BackendKind,
1249) -> RequiredEncodeStages {
1250 let decomposition_levels = j2k_lossy_decomposition_levels_for_options(samples, options);
1251 let high_throughput = options.block_coding_mode == J2kBlockCodingMode::HighThroughput;
1252
1253 let scalar_packetization_required = lossy_quality_layer_count(options) > 1
1254 || options.marker_segments.contains(&J2kMarkerSegment::Plt)
1255 || options.marker_segments.contains(&J2kMarkerSegment::Plm)
1256 || options.marker_segments.contains(&J2kMarkerSegment::Sop)
1257 || options.marker_segments.contains(&J2kMarkerSegment::Eph);
1258 let mut bits = 0;
1259 if !scalar_packetization_required {
1260 bits |= RequiredEncodeStages::PACKETIZATION;
1261 }
1262 if accelerated_backend == BackendKind::Cuda {
1263 bits |= RequiredEncodeStages::DEINTERLEAVE | RequiredEncodeStages::QUANTIZE_SUBBAND;
1264 if samples.components >= 3 {
1265 bits |= RequiredEncodeStages::FORWARD_ICT;
1266 }
1267 if decomposition_levels > 0 {
1268 bits |= RequiredEncodeStages::FORWARD_DWT97;
1269 }
1270 }
1271 if high_throughput {
1272 bits |= RequiredEncodeStages::HT_CODE_BLOCK;
1273 } else {
1274 bits |= RequiredEncodeStages::TIER1_CODE_BLOCK;
1275 }
1276
1277 RequiredEncodeStages { bits }
1278}
1279
1280fn validate_lossy_options(options: &J2kLossyEncodeOptions) -> Result<(), J2kError> {
1281 if options.quality_layers.len() > 32 {
1282 return Err(J2kError::Unsupported(Unsupported {
1283 what: "JPEG 2000 lossy encode supports 1-32 quality layers",
1284 }));
1285 }
1286 if let Some((tile_width, tile_height)) = options.tile_size {
1287 if tile_width == 0 || tile_height == 0 {
1288 return Err(J2kError::Unsupported(Unsupported {
1289 what: "JPEG 2000 lossy tile dimensions must be non-zero",
1290 }));
1291 }
1292 }
1293 if options
1294 .precinct_exponents
1295 .iter()
1296 .any(|&(ppx, ppy)| ppx > 15 || ppy > 15)
1297 {
1298 return Err(J2kError::Unsupported(Unsupported {
1299 what: "JPEG 2000 lossy precinct exponents must be 0-15",
1300 }));
1301 }
1302 if !(options.psnr_tolerance_db.is_finite() && options.psnr_tolerance_db >= 0.0) {
1303 return Err(J2kError::Unsupported(Unsupported {
1304 what: "JPEG 2000 lossy PSNR tolerance must be finite and non-negative",
1305 }));
1306 }
1307 if options.psnr_iteration_budget == 0 {
1308 return Err(J2kError::Unsupported(Unsupported {
1309 what: "JPEG 2000 lossy PSNR iteration budget must be greater than zero",
1310 }));
1311 }
1312 validate_rate_target(options.rate_target)?;
1313 for layer in &options.quality_layers {
1314 validate_rate_target(Some(layer.target))?;
1315 }
1316 Ok(())
1317}
1318
1319fn effective_lossy_target(
1320 options: &J2kLossyEncodeOptions,
1321) -> Result<Option<J2kRateTarget>, J2kError> {
1322 match (options.rate_target, options.quality_layers.as_slice()) {
1323 (target, []) => Ok(target),
1324 (None, [layer]) => Ok(Some(layer.target)),
1325 (Some(target), [layer]) if target == layer.target => Ok(Some(target)),
1326 (Some(_), [_]) => Err(J2kError::Unsupported(Unsupported {
1327 what:
1328 "specify either a JPEG 2000 lossy rate target or one quality layer target, not both",
1329 })),
1330 (None, layers) => Ok(layers.last().map(|layer| layer.target)),
1331 (Some(target), layers) if layers.last().is_some_and(|layer| layer.target == target) => {
1332 Ok(Some(target))
1333 }
1334 (Some(_), _) => Err(J2kError::Unsupported(Unsupported {
1335 what: "when multiple JPEG 2000 quality layers are specified, the single rate target must match the final cumulative layer target",
1336 })),
1337 }
1338}
1339
1340fn validate_rate_target(target: Option<J2kRateTarget>) -> Result<(), J2kError> {
1341 match target {
1342 None => Ok(()),
1343 Some(J2kRateTarget::BitsPerPixel(bits_per_pixel))
1344 if bits_per_pixel.is_finite() && bits_per_pixel > 0.0 =>
1345 {
1346 Ok(())
1347 }
1348 Some(J2kRateTarget::Bytes(bytes)) if bytes > 0 => Ok(()),
1349 Some(J2kRateTarget::PsnrDb(psnr_db)) if psnr_db.is_finite() && psnr_db > 0.0 => Ok(()),
1350 Some(J2kRateTarget::BitsPerPixel(_)) => Err(J2kError::Unsupported(Unsupported {
1351 what: "JPEG 2000 lossy bits-per-pixel target must be finite and greater than zero",
1352 })),
1353 Some(J2kRateTarget::Bytes(_)) => Err(J2kError::Unsupported(Unsupported {
1354 what: "JPEG 2000 lossy byte target must be greater than zero",
1355 })),
1356 Some(J2kRateTarget::PsnrDb(_)) => Err(J2kError::Unsupported(Unsupported {
1357 what: "JPEG 2000 lossy PSNR target must be finite and greater than zero",
1358 })),
1359 }
1360}
1361
1362fn lossy_report(
1363 samples: J2kLossySamples<'_>,
1364 options: &J2kLossyEncodeOptions,
1365 target: Option<J2kRateTarget>,
1366 attempt: &LossyAttempt,
1367) -> Result<J2kLossyEncodeReport, J2kError> {
1368 let actual_bytes = attempt.codestream.len() as u64;
1369 Ok(J2kLossyEncodeReport {
1370 target,
1371 quality_layers: u16::from(lossy_quality_layer_count(options)),
1372 quantization_scale: attempt.quantization_scale,
1373 actual_bytes,
1374 actual_bits_per_pixel: bits_per_pixel(samples, actual_bytes),
1375 psnr_db: validate_lossy_roundtrip(samples, &attempt.codestream, options.validation)?,
1376 ht_rate_granularity_bytes: (options.block_coding_mode
1377 == J2kBlockCodingMode::HighThroughput)
1378 .then_some(actual_bytes),
1379 })
1380}
1381
1382fn lossy_quality_layer_count(options: &J2kLossyEncodeOptions) -> u8 {
1383 u8::try_from(options.quality_layers.len().max(1)).unwrap_or(32)
1384}
1385
1386fn validate_lossy_roundtrip(
1387 samples: J2kLossySamples<'_>,
1388 codestream: &[u8],
1389 validation: J2kEncodeValidation,
1390) -> Result<Option<f64>, J2kError> {
1391 if validation == J2kEncodeValidation::External {
1392 return Ok(None);
1393 }
1394
1395 let decoded = Image::new(codestream, &DecodeSettings::default())
1396 .map_err(|err| J2kError::Backend(format!("encoded codestream validation failed: {err}")))?
1397 .decode_native()
1398 .map_err(|err| J2kError::Backend(format!("encoded codestream validation failed: {err}")))?;
1399
1400 if decoded.width != samples.width
1401 || decoded.height != samples.height
1402 || decoded.num_components != samples.components
1403 || decoded.bit_depth != samples.bit_depth
1404 {
1405 return Err(J2kError::InvalidSamples {
1406 what: "JPEG 2000 lossy encode failed round-trip geometry validation".to_string(),
1407 });
1408 }
1409
1410 Ok(Some(psnr_from_decoded(samples, &decoded.data)?))
1411}
1412
1413fn decoded_psnr(samples: J2kLossySamples<'_>, codestream: &[u8]) -> Result<f64, J2kError> {
1414 let decoded = Image::new(codestream, &DecodeSettings::default())
1415 .map_err(|err| J2kError::Backend(format!("encoded codestream validation failed: {err}")))?
1416 .decode_native()
1417 .map_err(|err| J2kError::Backend(format!("encoded codestream validation failed: {err}")))?;
1418 psnr_from_decoded(samples, &decoded.data)
1419}
1420
1421fn psnr_from_decoded(samples: J2kLossySamples<'_>, decoded: &[u8]) -> Result<f64, J2kError> {
1422 if decoded.len() != samples.data.len() {
1423 return Err(J2kError::InvalidSamples {
1424 what: format!(
1425 "JPEG 2000 lossy encode validation length mismatch: expected {} bytes, got {} bytes",
1426 samples.data.len(),
1427 decoded.len()
1428 ),
1429 });
1430 }
1431 let bytes_per_sample = if samples.bit_depth <= 8 {
1432 1usize
1433 } else {
1434 2usize
1435 };
1436 let sample_count = samples.data.len() / bytes_per_sample;
1437 let mut squared_error = 0.0f64;
1438 for sample_idx in 0..sample_count {
1439 let original = sample_value(samples.data, sample_idx, samples.bit_depth, samples.signed);
1440 let decoded = sample_value(decoded, sample_idx, samples.bit_depth, samples.signed);
1441 let error = original - decoded;
1442 squared_error += error * error;
1443 }
1444 if squared_error == 0.0 {
1445 return Ok(f64::INFINITY);
1446 }
1447 let mse = squared_error / usize_to_f64(sample_count);
1448 let peak = f64::from((1u32 << u32::from(samples.bit_depth)) - 1);
1449 Ok(10.0 * ((peak * peak) / mse).log10())
1450}
1451
1452fn sample_value(data: &[u8], sample_idx: usize, bit_depth: u8, signed: bool) -> f64 {
1453 if bit_depth <= 8 {
1454 if signed {
1455 f64::from(data[sample_idx] as i8)
1456 } else {
1457 f64::from(data[sample_idx])
1458 }
1459 } else {
1460 let byte_idx = sample_idx * 2;
1461 let bytes = [data[byte_idx], data[byte_idx + 1]];
1462 if signed {
1463 f64::from(i16::from_le_bytes(bytes))
1464 } else {
1465 f64::from(u16::from_le_bytes(bytes))
1466 }
1467 }
1468}
1469
1470fn target_bytes_for_bpp(
1471 samples: J2kLossySamples<'_>,
1472 bits_per_pixel: f64,
1473) -> Result<u64, J2kError> {
1474 let pixels = f64::from(samples.width) * f64::from(samples.height);
1475 let bytes = (pixels * bits_per_pixel / 8.0).ceil();
1476 if bytes.is_finite() && bytes > 0.0 && bytes <= 18_446_744_073_709_551_615.0 {
1477 Ok(bytes as u64)
1478 } else {
1479 Err(J2kError::Unsupported(Unsupported {
1480 what: "JPEG 2000 lossy bits-per-pixel target overflows byte target",
1481 }))
1482 }
1483}
1484
1485fn byte_target_tolerance(target_bytes: u64) -> u64 {
1486 target_bytes.div_ceil(100).max(512)
1487}
1488
1489fn byte_target_diff(actual: u64, target: u64) -> u64 {
1490 actual.abs_diff(target)
1491}
1492
1493fn bits_per_pixel(samples: J2kLossySamples<'_>, bytes: u64) -> f64 {
1494 (u64_to_f64(bytes) * 8.0) / (f64::from(samples.width) * f64::from(samples.height))
1495}
1496
1497#[allow(clippy::cast_precision_loss)]
1498fn usize_to_f64(value: usize) -> f64 {
1499 value as f64
1500}
1501
1502#[allow(clippy::cast_precision_loss)]
1503fn u64_to_f64(value: u64) -> f64 {
1504 value as f64
1505}
1506
1507fn validate_lossless_roundtrip(
1508 samples: J2kLosslessSamples<'_>,
1509 codestream: &[u8],
1510 validation: J2kEncodeValidation,
1511) -> Result<(), J2kError> {
1512 if validation == J2kEncodeValidation::External {
1513 return Ok(());
1514 }
1515
1516 let decoded = Image::new(codestream, &DecodeSettings::default())
1517 .map_err(|err| J2kError::Backend(format!("encoded codestream validation failed: {err}")))?
1518 .decode_native()
1519 .map_err(|err| J2kError::Backend(format!("encoded codestream validation failed: {err}")))?;
1520
1521 if decoded.width != samples.width
1522 || decoded.height != samples.height
1523 || decoded.num_components != samples.components
1524 || decoded.bit_depth != samples.bit_depth
1525 {
1526 return Err(J2kError::InvalidSamples {
1527 what: "JPEG 2000 lossless encode failed round-trip geometry validation".to_string(),
1528 });
1529 }
1530 if decoded.data != samples.data {
1531 let mismatch = decoded
1532 .data
1533 .iter()
1534 .zip(samples.data.iter())
1535 .position(|(actual, expected)| actual != expected);
1536 return Err(J2kError::InvalidSamples {
1537 what: match mismatch {
1538 Some(index) => format!(
1539 "JPEG 2000 lossless encode failed round-trip validation at byte {index}: expected {}, got {}",
1540 samples.data[index], decoded.data[index]
1541 ),
1542 None => format!(
1543 "JPEG 2000 lossless encode failed round-trip validation: expected {} bytes, got {} bytes",
1544 samples.data.len(),
1545 decoded.data.len()
1546 ),
1547 }});
1548 }
1549 Ok(())
1550}
1551
1552#[cfg(test)]
1553mod tests {
1554 use super::{
1555 encode_j2k_lossless, j2k_lossless_decomposition_levels_for_options,
1556 native_lossless_options, DecodeSettings, EncodeBackendPreference, Image,
1557 J2kBlockCodingMode, J2kEncodeValidation, J2kLosslessEncodeOptions, J2kLosslessSamples,
1558 J2kProgressionOrder, ReversibleTransform,
1559 };
1560
1561 fn cod_mct(codestream: &[u8]) -> u8 {
1562 let cod_offset = codestream
1563 .windows(2)
1564 .position(|window| window == [0xFF, 0x52])
1565 .expect("COD marker");
1566 codestream[cod_offset + 8]
1567 }
1568
1569 #[test]
1570 fn lossless_encode_can_disable_component_transform() {
1571 let pixels: Vec<u8> = (0..4 * 4 * 3)
1572 .map(|value| ((value * 17) & 0xFF) as u8)
1573 .collect();
1574 let samples = J2kLosslessSamples::new(&pixels, 4, 4, 3, 8, false).unwrap();
1575 let encoded = encode_j2k_lossless(
1576 samples,
1577 &J2kLosslessEncodeOptions {
1578 block_coding_mode: J2kBlockCodingMode::Classic,
1579 progression: J2kProgressionOrder::Lrcp,
1580 max_decomposition_levels: Some(0),
1581 reversible_transform: ReversibleTransform::None53,
1582 validation: J2kEncodeValidation::CpuRoundTrip,
1583 ..J2kLosslessEncodeOptions::default()
1584 },
1585 )
1586 .unwrap();
1587
1588 assert_eq!(cod_mct(&encoded.codestream), 0);
1589 }
1590
1591 #[test]
1592 fn explicit_decomposition_levels_override_default_lrcp_policy() {
1593 let pixels = vec![0; 128 * 128];
1594 let samples = J2kLosslessSamples::new(&pixels, 128, 128, 1, 8, false).unwrap();
1595
1596 let levels = j2k_lossless_decomposition_levels_for_options(
1597 samples,
1598 J2kLosslessEncodeOptions {
1599 block_coding_mode: J2kBlockCodingMode::Classic,
1600 progression: J2kProgressionOrder::Lrcp,
1601 max_decomposition_levels: Some(5),
1602 ..J2kLosslessEncodeOptions::default()
1603 },
1604 );
1605
1606 assert_eq!(levels, 5);
1607 }
1608
1609 #[test]
1610 fn facade_native_options_skip_internal_ht_validation_for_external_validation() {
1611 let pixels = vec![0; 64 * 64];
1612 let samples = J2kLosslessSamples::new(&pixels, 64, 64, 1, 8, false).unwrap();
1613
1614 let external = native_lossless_options(
1615 samples,
1616 J2kLosslessEncodeOptions {
1617 block_coding_mode: J2kBlockCodingMode::HighThroughput,
1618 validation: J2kEncodeValidation::External,
1619 ..J2kLosslessEncodeOptions::default()
1620 },
1621 );
1622 let roundtrip = native_lossless_options(
1623 samples,
1624 J2kLosslessEncodeOptions {
1625 block_coding_mode: J2kBlockCodingMode::HighThroughput,
1626 validation: J2kEncodeValidation::CpuRoundTrip,
1627 ..J2kLosslessEncodeOptions::default()
1628 },
1629 );
1630
1631 assert!(!external.validate_high_throughput_codestream);
1632 assert!(!roundtrip.validate_high_throughput_codestream);
1633 }
1634
1635 #[test]
1636 fn lossless_facade_roundtrips_four_component_via_public_api() {
1637 let width: u32 = 32;
1638 let height: u32 = 24;
1639 let components: u8 = 4;
1640
1641 let mut pixels = Vec::with_capacity((width * height * u32::from(components)) as usize);
1643 for y in 0..height {
1644 for x in 0..width {
1645 for c in 0..u32::from(components) {
1646 let value = (x.wrapping_mul(7) ^ y.wrapping_mul(13)).wrapping_add(c * 41);
1647 pixels.push((value & 0xFF) as u8);
1648 }
1649 }
1650 }
1651
1652 let samples = J2kLosslessSamples::new(&pixels, width, height, components, 8, false)
1654 .expect("4-component samples must be accepted by the public constructor");
1655
1656 let encoded = encode_j2k_lossless(
1658 samples,
1659 &J2kLosslessEncodeOptions {
1660 backend: EncodeBackendPreference::CpuOnly,
1661 validation: J2kEncodeValidation::CpuRoundTrip,
1662 ..J2kLosslessEncodeOptions::default()
1663 },
1664 )
1665 .expect("4-component CPU lossless encode must succeed");
1666
1667 assert_eq!(encoded.components, components);
1668
1669 let decoded = Image::new(&encoded.codestream, &DecodeSettings::default())
1671 .expect("native decode of 4-component codestream must construct")
1672 .decode_native()
1673 .expect("native decode of 4-component codestream must succeed");
1674
1675 assert_eq!(decoded.width, width);
1676 assert_eq!(decoded.height, height);
1677 assert_eq!(decoded.num_components, components);
1678 assert_eq!(decoded.bit_depth, 8);
1679 assert_eq!(
1680 decoded.data, pixels,
1681 "4-component pixels must round-trip exactly"
1682 );
1683
1684 let two_component = vec![0u8; (width * height * 2) as usize];
1686 let two_component = J2kLosslessSamples::new(&two_component, width, height, 2, 8, false)
1687 .expect("2-component samples must be accepted by the public constructor");
1688 assert_eq!(two_component.components, 2);
1689 }
1690}