1use alloc::vec::Vec;
13
14use j2k_core::{
15 Colorspace, CompressedPayloadKind, CompressedTransferSyntax, PassthroughRequirements,
16 Unsupported,
17};
18use j2k_native::{DecodeSettings, EncodeOptions, Image};
19
20use crate::{
21 encode::{native_progression_order, J2kEncodeValidation, J2kProgressionOrder},
22 parse::{parse_image_info, ParsedImageInfo},
23 J2kError, J2kView,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub struct J2kToHtj2kOptions {
30 pub output_payload_kind: CompressedPayloadKind,
36 pub progression: J2kProgressionOrder,
38 pub validation: J2kEncodeValidation,
40}
41
42impl Default for J2kToHtj2kOptions {
43 fn default() -> Self {
44 Self {
45 output_payload_kind: CompressedPayloadKind::Jpeg2000Codestream,
46 progression: J2kProgressionOrder::Lrcp,
47 validation: J2kEncodeValidation::CpuRoundTrip,
48 }
49 }
50}
51
52impl J2kToHtj2kOptions {
53 pub const fn new(
55 output_payload_kind: CompressedPayloadKind,
56 progression: J2kProgressionOrder,
57 validation: J2kEncodeValidation,
58 ) -> Self {
59 Self {
60 output_payload_kind,
61 progression,
62 validation,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69pub enum J2kToHtj2kMode {
70 Passthrough,
73 CoefficientPreserving,
76 PixelPreserving,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct J2kToHtj2kReport {
83 pub mode: J2kToHtj2kMode,
85 pub input_transfer_syntax: CompressedTransferSyntax,
87 pub output_transfer_syntax: CompressedTransferSyntax,
89 pub input_payload_kind: CompressedPayloadKind,
91 pub output_payload_kind: CompressedPayloadKind,
93 pub width: u32,
95 pub height: u32,
97 pub components: u8,
99 pub bit_depth: u8,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct ReencodedHtj2k {
106 pub bytes: Vec<u8>,
108 pub report: J2kToHtj2kReport,
110}
111
112pub fn recode_j2k_to_htj2k_lossless(
120 bytes: &[u8],
121 options: J2kToHtj2kOptions,
122) -> Result<ReencodedHtj2k, J2kError> {
123 let view = J2kView::parse(bytes)?;
124 let parsed = parse_image_info(bytes)?;
125 let info = view.info().clone();
126 let output_transfer_syntax = CompressedTransferSyntax::HtJpeg2000Lossless;
127
128 if let Some(candidate) = view.passthrough_candidate() {
129 let requirements =
130 PassthroughRequirements::new(output_transfer_syntax, options.output_payload_kind);
131 if let Ok(copy) = candidate.copy_bytes_if_eligible(&requirements) {
132 return Ok(ReencodedHtj2k {
133 bytes: copy.to_vec(),
134 report: J2kToHtj2kReport {
135 mode: J2kToHtj2kMode::Passthrough,
136 input_transfer_syntax: candidate.transfer_syntax(),
137 output_transfer_syntax,
138 input_payload_kind: candidate.payload_kind(),
139 output_payload_kind: options.output_payload_kind,
140 width: info.dimensions.0,
141 height: info.dimensions.1,
142 components: info.components,
143 bit_depth: info.bit_depth,
144 },
145 });
146 }
147 }
148
149 validate_recode_request(&parsed, options)?;
150
151 let source = Image::new(bytes, &DecodeSettings::default())
152 .map_err(|err| map_native_decode_error(err, "source JPEG 2000 parse failed"))?;
153 let coefficients = source
154 .decode_reversible_53_coefficients()
155 .map_err(|err| map_native_decode_error(err, "source coefficient extraction failed"))?;
156
157 let encode_options = native_encode_options(options, &coefficients);
158 let codestream = j2k_native::encode_precomputed_htj2k_53_with_mct(
159 &coefficients.image,
160 &encode_options,
161 coefficients.use_mct,
162 )
163 .map_err(|err| J2kError::Backend(format!("HTJ2K coefficient recode failed: {err}")))?;
164
165 if options.validation == J2kEncodeValidation::CpuRoundTrip {
166 validate_recode_roundtrip(bytes, &codestream)?;
167 }
168
169 Ok(ReencodedHtj2k {
170 bytes: codestream,
171 report: J2kToHtj2kReport {
172 mode: J2kToHtj2kMode::CoefficientPreserving,
173 input_transfer_syntax: parsed.transfer_syntax,
174 output_transfer_syntax,
175 input_payload_kind: parsed.payload_kind,
176 output_payload_kind: options.output_payload_kind,
177 width: parsed.info.dimensions.0,
178 height: parsed.info.dimensions.1,
179 components: parsed.info.components,
180 bit_depth: parsed.info.bit_depth,
181 },
182 })
183}
184
185fn validate_recode_request(
186 parsed: &ParsedImageInfo,
187 options: J2kToHtj2kOptions,
188) -> Result<(), J2kError> {
189 if options.output_payload_kind != CompressedPayloadKind::Jpeg2000Codestream {
190 return Err(Unsupported {
191 what: "coefficient-domain J2K to HTJ2K recode currently emits only raw codestreams",
192 }
193 .into());
194 }
195 if parsed.transfer_syntax != CompressedTransferSyntax::Jpeg2000Lossless {
196 return Err(Unsupported {
197 what: "coefficient-domain lossless recode currently supports only classic lossless J2K",
198 }
199 .into());
200 }
201 if !matches!(parsed.info.components, 1 | 3) {
202 return Err(Unsupported {
203 what: "coefficient-domain lossless recode supports only grayscale or RGB component counts",
204 }
205 .into());
206 }
207 if !matches!(parsed.info.bit_depth, 8 | 16) {
208 return Err(Unsupported {
209 what: "coefficient-domain lossless recode supports only 8-bit or 16-bit sources",
210 }
211 .into());
212 }
213 if !matches!(
214 parsed.info.colorspace,
215 Colorspace::Grayscale
216 | Colorspace::SGray
217 | Colorspace::Rgb
218 | Colorspace::SRgb
219 | Colorspace::Rct
220 ) {
221 return Err(Unsupported {
222 what: "coefficient-domain lossless recode supports only Gray/RGB/RCT colorspaces",
223 }
224 .into());
225 }
226 if parsed.components.iter().any(|component| component.signed) {
227 return Err(Unsupported {
228 what: "signed JPEG 2000 sources are not supported for coefficient-domain recode yet",
229 }
230 .into());
231 }
232 if parsed
233 .components
234 .iter()
235 .any(|component| component.bit_depth != parsed.info.bit_depth)
236 {
237 return Err(Unsupported {
238 what: "mixed component bit depths are not supported for coefficient-domain recode",
239 }
240 .into());
241 }
242 if parsed
243 .components
244 .iter()
245 .any(|component| component.x_rsiz != 1 || component.y_rsiz != 1)
246 {
247 return Err(Unsupported {
248 what: "component subsampling is not supported for coefficient-domain recode yet",
249 }
250 .into());
251 }
252 Ok(())
253}
254
255fn native_encode_options(
256 options: J2kToHtj2kOptions,
257 coefficients: &j2k_native::Reversible53CoefficientImage,
258) -> EncodeOptions {
259 EncodeOptions {
260 reversible: true,
261 use_ht_block_coding: true,
262 use_mct: coefficients.use_mct,
263 code_block_width_exp: coefficients.code_block_width_exp,
264 code_block_height_exp: coefficients.code_block_height_exp,
265 guard_bits: coefficients.guard_bits,
266 progression_order: native_progression_order(options.progression),
267 write_tlm: options.progression == J2kProgressionOrder::Rpcl,
268 validate_high_throughput_codestream: false,
269 ..EncodeOptions::default()
270 }
271}
272
273fn validate_recode_roundtrip(source: &[u8], encoded: &[u8]) -> Result<(), J2kError> {
274 let source = Image::new(source, &DecodeSettings::default())
275 .map_err(|err| map_native_decode_error(err, "source JPEG 2000 validation parse failed"))?
276 .decode_native()
277 .map_err(|err| map_native_decode_error(err, "source JPEG 2000 validation decode failed"))?;
278 let encoded = Image::new(encoded, &DecodeSettings::default())
279 .map_err(|err| map_native_decode_error(err, "HTJ2K validation parse failed"))?
280 .decode_native()
281 .map_err(|err| map_native_decode_error(err, "HTJ2K validation decode failed"))?;
282
283 if source.width != encoded.width
284 || source.height != encoded.height
285 || source.bit_depth != encoded.bit_depth
286 || source.num_components != encoded.num_components
287 || source.data != encoded.data
288 {
289 return Err(J2kError::Backend(
290 "HTJ2K coefficient recode failed pixel validation".to_string(),
291 ));
292 }
293 Ok(())
294}
295
296fn map_native_decode_error(err: j2k_native::DecodeError, context: &'static str) -> J2kError {
297 match err {
298 j2k_native::DecodeError::Decoding(j2k_native::DecodingError::UnsupportedFeature(what)) => {
299 J2kError::Unsupported(Unsupported { what })
300 }
301 _ => J2kError::Backend(format!("{context}: {err}")),
302 }
303}