Skip to main content

signinum_j2k/
encode.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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/// Backend preference for JPEG 2000 lossless encoding.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
17pub enum EncodeBackendPreference {
18    /// Pick the fastest safe backend exposed by the caller, falling back to CPU.
19    #[default]
20    Auto,
21    /// Require the pure Rust CPU encoder.
22    CpuOnly,
23    /// Require a device encoder and fail if unavailable or unsupported.
24    RequireDevice,
25}
26
27/// Supported JPEG 2000 progression orders for the lossless encode facade.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
29pub enum J2kProgressionOrder {
30    /// Layer-resolution-component-position progression.
31    #[default]
32    Lrcp,
33    /// Resolution-layer-component-position progression.
34    Rlcp,
35    /// Resolution-position-component-layer progression.
36    Rpcl,
37    /// Position-component-resolution-layer progression.
38    Pcrl,
39    /// Component-position-resolution-layer progression.
40    Cprl,
41}
42
43/// Supported code-block coding modes for the lossless encode facade.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub enum J2kBlockCodingMode {
46    /// Classic JPEG 2000 Part 1 EBCOT block coding.
47    #[default]
48    Classic,
49    /// High-throughput JPEG 2000 Part 15 block coding.
50    HighThroughput,
51}
52
53/// Reversible transform profile for lossless JPEG 2000 output.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
55pub enum ReversibleTransform {
56    /// Reversible color transform with 5/3 wavelet transform.
57    #[default]
58    Rct53,
59    /// No color transform with 5/3 wavelet transform.
60    None53,
61}
62
63/// Validation policy for the lossless encode facade.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
65pub enum J2kEncodeValidation {
66    /// Decode the produced codestream with the native CPU decoder and compare
67    /// decoded samples before returning.
68    #[default]
69    CpuRoundTrip,
70    /// Skip facade validation because the caller performs equivalent external
71    /// validation, for example by decoding on a device backend.
72    External,
73}
74
75/// Options controlling JPEG 2000 lossless encoding.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77#[non_exhaustive]
78pub struct J2kLosslessEncodeOptions {
79    /// Backend preference for encode stages.
80    pub backend: EncodeBackendPreference,
81    /// Code-block coding mode for the codestream.
82    pub block_coding_mode: J2kBlockCodingMode,
83    /// Packet progression order.
84    pub progression: J2kProgressionOrder,
85    /// Optional explicit lossless decomposition level request.
86    ///
87    /// Requests are clamped to the geometry-safe maximum for the tile.
88    pub max_decomposition_levels: Option<u8>,
89    /// Reversible transform profile.
90    pub reversible_transform: ReversibleTransform,
91    /// Validation policy applied before returning encoded bytes.
92    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    /// Create JPEG 2000 lossless encode options.
110    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    /// Return options with a different backend preference.
129    #[must_use]
130    pub const fn with_backend(mut self, backend: EncodeBackendPreference) -> Self {
131        self.backend = backend;
132        self
133    }
134
135    /// Return options using adaptive accelerated routing.
136    #[must_use]
137    pub const fn with_accelerated_backend(self) -> Self {
138        self.with_backend(EncodeBackendPreference::Auto)
139    }
140
141    /// Return options using the portable CPU route.
142    #[must_use]
143    pub const fn with_cpu_only_backend(self) -> Self {
144        self.with_backend(EncodeBackendPreference::CpuOnly)
145    }
146
147    /// Return options requiring a strict device route.
148    #[must_use]
149    pub const fn with_strict_device_backend(self) -> Self {
150        self.with_backend(EncodeBackendPreference::RequireDevice)
151    }
152
153    /// Return options with a different code-block coding mode.
154    #[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    /// Return options with a different packet progression order.
161    #[must_use]
162    pub const fn with_progression(mut self, progression: J2kProgressionOrder) -> Self {
163        self.progression = progression;
164        self
165    }
166
167    /// Return options with a different maximum decomposition-level request.
168    #[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    /// Return options with a different reversible transform.
178    #[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    /// Return options with a different validation policy.
188    #[must_use]
189    pub const fn with_validation(mut self, validation: J2kEncodeValidation) -> Self {
190        self.validation = validation;
191        self
192    }
193}
194
195/// Rate target for stable lossy JPEG 2000 encoding.
196#[derive(Debug, Clone, Copy, PartialEq)]
197pub enum J2kRateTarget {
198    /// Target total codestream bits per image pixel.
199    BitsPerPixel(f64),
200    /// Target total codestream byte size.
201    Bytes(u64),
202    /// Target decoded peak signal-to-noise ratio in dB.
203    PsnrDb(f64),
204}
205
206/// One cumulative lossy quality layer request.
207#[derive(Debug, Clone, Copy, PartialEq)]
208pub struct J2kQualityLayer {
209    /// Cumulative target for this quality layer.
210    pub target: J2kRateTarget,
211}
212
213impl J2kQualityLayer {
214    /// Create a cumulative lossy quality layer target.
215    #[must_use]
216    pub const fn new(target: J2kRateTarget) -> Self {
217        Self { target }
218    }
219}
220
221/// Optional JPEG 2000 marker segment requested for lossy encode output.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
223pub enum J2kMarkerSegment {
224    /// SOP packet marker segments.
225    Sop,
226    /// EPH packet header termination markers.
227    Eph,
228    /// TLM tile-part length marker segment.
229    Tlm,
230    /// PLT packet length marker segments.
231    Plt,
232    /// PLM packet length marker segments.
233    Plm,
234}
235
236/// Options controlling stable lossy JPEG 2000 encoding.
237#[derive(Debug, Clone, PartialEq)]
238#[non_exhaustive]
239pub struct J2kLossyEncodeOptions {
240    /// Backend preference for encode stages.
241    pub backend: EncodeBackendPreference,
242    /// Code-block coding mode for the codestream.
243    pub block_coding_mode: J2kBlockCodingMode,
244    /// Packet progression order.
245    pub progression: J2kProgressionOrder,
246    /// Optional explicit lossy decomposition level request.
247    pub max_decomposition_levels: Option<u8>,
248    /// Single codestream rate target.
249    pub rate_target: Option<J2kRateTarget>,
250    /// Cumulative quality layer targets.
251    pub quality_layers: Vec<J2kQualityLayer>,
252    /// Optional tile width and height.
253    pub tile_size: Option<(u32, u32)>,
254    /// Optional precinct exponents in COD/COC order.
255    pub precinct_exponents: Vec<(u8, u8)>,
256    /// Optional marker segments requested for the codestream.
257    pub marker_segments: Vec<J2kMarkerSegment>,
258    /// Allowed PSNR target tolerance in dB.
259    pub psnr_tolerance_db: f64,
260    /// Iteration budget for lossy target searches.
261    pub psnr_iteration_budget: u8,
262    /// Validation policy applied before returning encoded bytes.
263    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    /// Return options with a different backend preference.
287    #[must_use]
288    pub fn with_backend(mut self, backend: EncodeBackendPreference) -> Self {
289        self.backend = backend;
290        self
291    }
292
293    /// Return options using adaptive accelerated routing.
294    #[must_use]
295    pub fn with_accelerated_backend(self) -> Self {
296        self.with_backend(EncodeBackendPreference::Auto)
297    }
298
299    /// Return options using the portable CPU route.
300    #[must_use]
301    pub fn with_cpu_only_backend(self) -> Self {
302        self.with_backend(EncodeBackendPreference::CpuOnly)
303    }
304
305    /// Return options requiring a strict device route.
306    #[must_use]
307    pub fn with_strict_device_backend(self) -> Self {
308        self.with_backend(EncodeBackendPreference::RequireDevice)
309    }
310
311    /// Return options with a different code-block coding mode.
312    #[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    /// Return options with a different packet progression order.
319    #[must_use]
320    pub fn with_progression(mut self, progression: J2kProgressionOrder) -> Self {
321        self.progression = progression;
322        self
323    }
324
325    /// Return options with a different maximum decomposition-level request.
326    #[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    /// Return options with a different single codestream rate target.
333    #[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    /// Return options with different cumulative quality layer targets.
340    #[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    /// Return options with different optional marker segment requests.
347    #[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    /// Return options with a different validation policy.
354    #[must_use]
355    pub fn with_validation(mut self, validation: J2kEncodeValidation) -> Self {
356        self.validation = validation;
357        self
358    }
359}
360
361/// Borrowed interleaved samples and image geometry for lossless encoding.
362#[derive(Debug, Clone, Copy)]
363pub struct J2kLosslessSamples<'a> {
364    /// Interleaved sample bytes.
365    pub data: &'a [u8],
366    /// Image width in pixels.
367    pub width: u32,
368    /// Image height in pixels.
369    pub height: u32,
370    /// Component count. The stable facade accepts 1-4 independent component
371    /// samples. Two-component output is written without MCT.
372    pub components: u8,
373    /// Significant bits per component sample.
374    pub bit_depth: u8,
375    /// Whether component samples are signed.
376    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    /// Validate and construct a sample descriptor.
433    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/// Borrowed interleaved samples and image geometry for lossy encoding.
463#[derive(Debug, Clone, Copy)]
464pub struct J2kLossySamples<'a> {
465    /// Interleaved sample bytes.
466    pub data: &'a [u8],
467    /// Image width in pixels.
468    pub width: u32,
469    /// Image height in pixels.
470    pub height: u32,
471    /// Component count. The stable facade accepts 1-4 component samples.
472    pub components: u8,
473    /// Significant bits per component sample.
474    pub bit_depth: u8,
475    /// Whether component samples are signed.
476    pub signed: bool,
477}
478
479impl<'a> J2kLossySamples<'a> {
480    /// Validate and construct a lossy sample descriptor.
481    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/// Encoded JPEG 2000 lossless codestream and encode metadata.
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct EncodedJ2k {
513    /// Raw JPEG 2000 codestream bytes.
514    pub codestream: Vec<u8>,
515    /// Backend that satisfied the encode contract.
516    pub backend: BackendKind,
517    /// Encode-stage dispatches observed while producing this codestream.
518    ///
519    /// This can be nonzero even when [`Self::backend`] is [`BackendKind::Cpu`]
520    /// for Auto routes that used one or more device stages but did not satisfy
521    /// every stage required for a fully device-backed encode contract.
522    pub dispatch_report: J2kEncodeDispatchReport,
523    /// Encoded image width in pixels.
524    pub width: u32,
525    /// Encoded image height in pixels.
526    pub height: u32,
527    /// Encoded component count.
528    pub components: u8,
529    /// Encoded significant bits per sample.
530    pub bit_depth: u8,
531    /// Whether encoded samples are signed.
532    pub signed: bool,
533}
534
535/// Metrics reported by stable lossy JPEG 2000 encoding.
536#[derive(Debug, Clone, PartialEq)]
537pub struct J2kLossyEncodeReport {
538    /// Requested effective rate target.
539    pub target: Option<J2kRateTarget>,
540    /// Number of cumulative quality layers emitted.
541    pub quality_layers: u16,
542    /// Final native irreversible quantization scale.
543    pub quantization_scale: f32,
544    /// Total encoded codestream bytes.
545    pub actual_bytes: u64,
546    /// Total codestream bits per image pixel.
547    pub actual_bits_per_pixel: f64,
548    /// Decoded PSNR in dB when CPU validation was requested.
549    pub psnr_db: Option<f64>,
550    /// HTJ2K rate granularity in bytes when HT block coding is used.
551    pub ht_rate_granularity_bytes: Option<u64>,
552}
553
554/// Encoded JPEG 2000 lossy codestream and encode metadata.
555#[derive(Debug, Clone, PartialEq)]
556pub struct EncodedLossyJ2k {
557    /// Raw JPEG 2000 codestream bytes.
558    pub codestream: Vec<u8>,
559    /// Backend that satisfied the encode contract.
560    pub backend: BackendKind,
561    /// Encode-stage dispatches observed while producing this codestream.
562    ///
563    /// This can be nonzero even when [`Self::backend`] is [`BackendKind::Cpu`]
564    /// for Auto routes that used one or more device stages but did not satisfy
565    /// every stage required for a fully device-backed encode contract.
566    pub dispatch_report: J2kEncodeDispatchReport,
567    /// Encoded image width in pixels.
568    pub width: u32,
569    /// Encoded image height in pixels.
570    pub height: u32,
571    /// Encoded component count.
572    pub components: u8,
573    /// Encoded significant bits per sample.
574    pub bit_depth: u8,
575    /// Whether encoded samples are signed.
576    pub signed: bool,
577    /// Lossy encode metrics.
578    pub report: J2kLossyEncodeReport,
579}
580
581/// Encode interleaved samples into a raw JPEG 2000 lossless codestream.
582pub 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
601/// Encode interleaved samples with an optional device encode-stage accelerator.
602///
603/// Accelerators return CPU fallback by reporting no dispatch. `Auto` accepts
604/// that fallback; `RequireDevice` requires at least one dispatch. Any
605/// accelerator error or codestream validation error is returned to the caller.
606pub 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
640/// Encode interleaved samples into a raw JPEG 2000 lossy codestream.
641pub 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
664/// Encode interleaved lossy samples with an optional device encode-stage accelerator.
665pub 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
1036/// Return the default lossless decomposition level policy used by the facade.
1037pub fn j2k_lossless_decomposition_levels(samples: J2kLosslessSamples<'_>) -> u8 {
1038    j2k_lossless_decomposition_levels_for_progression(samples, J2kProgressionOrder::Lrcp)
1039}
1040
1041/// Return the default lossless decomposition level policy for a progression.
1042pub 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
1090/// Return the effective lossless decomposition level policy for encode options.
1091pub 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        // Deterministic 4-component (RGBA/CMYK) 8-bit input, distinct per plane.
1642        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        // MUST go through the real public constructor.
1653        let samples = J2kLosslessSamples::new(&pixels, width, height, components, 8, false)
1654            .expect("4-component samples must be accepted by the public constructor");
1655
1656        // Encode via the public CPU lossless entry.
1657        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        // Decode the bytes with the native decoder and assert an exact round-trip.
1670        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        // 2-component is accepted and handled as independent channels without MCT.
1685        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}