1use crate::error::DecodeError;
10
11pub const MAX_RESOLUTIONS_TO_SKIP: u8 = 32;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum JpxColorSpaceOption {
21 None,
23 #[default]
25 Normal,
26 Indexed,
28}
29
30#[derive(Debug, Clone, Default)]
34pub struct JpxDecodeOptions {
35 pub target_resolution: Option<(u32, u32)>,
42
43 pub color_space_option: JpxColorSpaceOption,
45
46 pub strict: bool,
51}
52
53#[derive(Debug, Clone)]
55pub struct Jpeg2000Info {
56 pub width: u32,
58 pub height: u32,
60 pub has_alpha: bool,
62 pub original_bit_depth: u8,
64 pub num_components: u8,
67}
68
69fn build_decode_settings(options: &JpxDecodeOptions) -> hayro_jpeg2000::DecodeSettings {
71 let mut settings = hayro_jpeg2000::DecodeSettings::default();
72 let _ = options.target_resolution;
76 settings.strict = options.strict;
77 settings.resolve_palette_indices = match options.color_space_option {
78 JpxColorSpaceOption::None => false,
79 JpxColorSpaceOption::Normal => true,
80 JpxColorSpaceOption::Indexed => true,
81 };
82 settings
83}
84
85pub fn decode(input: &[u8]) -> Result<Vec<u8>, DecodeError> {
93 let bitmap = hayro_jpeg2000::decode(input, &hayro_jpeg2000::DecodeSettings::default())
94 .map_err(|e| DecodeError::InvalidInput(format!("JPEG2000: {e}")))?;
95 Ok(bitmap.data)
96}
97
98pub fn decode_with_options(
103 input: &[u8],
104 options: &JpxDecodeOptions,
105) -> Result<Vec<u8>, DecodeError> {
106 let settings = build_decode_settings(options);
107 let bitmap = hayro_jpeg2000::decode(input, &settings)
108 .map_err(|e| DecodeError::InvalidInput(format!("JPEG2000: {e}")))?;
109 Ok(bitmap.data)
110}
111
112pub fn decode_with_info(input: &[u8]) -> Result<(Vec<u8>, Jpeg2000Info), DecodeError> {
117 let bitmap = hayro_jpeg2000::decode(input, &hayro_jpeg2000::DecodeSettings::default())
118 .map_err(|e| DecodeError::InvalidInput(format!("JPEG2000: {e}")))?;
119
120 let num_components = bitmap.color_space.num_channels();
124
125 let info = Jpeg2000Info {
126 width: bitmap.width,
127 height: bitmap.height,
128 has_alpha: bitmap.has_alpha,
129 original_bit_depth: bitmap.original_bit_depth,
130 num_components,
131 };
132
133 Ok((bitmap.data, info))
134}
135
136pub fn decode_with_info_and_options(
138 input: &[u8],
139 options: &JpxDecodeOptions,
140) -> Result<(Vec<u8>, Jpeg2000Info), DecodeError> {
141 let settings = build_decode_settings(options);
142 let bitmap = hayro_jpeg2000::decode(input, &settings)
143 .map_err(|e| DecodeError::InvalidInput(format!("JPEG2000: {e}")))?;
144
145 let num_components = bitmap.color_space.num_channels();
146 let info = Jpeg2000Info {
147 width: bitmap.width,
148 height: bitmap.height,
149 has_alpha: bitmap.has_alpha,
150 original_bit_depth: bitmap.original_bit_depth,
151 num_components,
152 };
153
154 Ok((bitmap.data, info))
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub struct ComponentPrecision {
160 pub bits: u8,
162 pub is_signed: bool,
164}
165
166pub fn parse_jp2_precision(data: &[u8]) -> Option<Vec<ComponentPrecision>> {
171 if data.len() < 12 {
174 return None;
175 }
176 if &data[4..8] != b"jP " && &data[4..8] != b"jP\x1a\x1a" {
177 return parse_codestream_precision(data);
179 }
180
181 let mut pos = 0;
183 while pos + 8 <= data.len() {
184 let box_len =
185 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
186 let box_type = &data[pos + 4..pos + 8];
187
188 if box_len < 8 || pos + box_len > data.len() {
189 break;
190 }
191
192 if box_type == b"jp2h" {
193 return parse_ihdr_in_jp2h(&data[pos + 8..pos + box_len]);
195 }
196
197 pos += box_len;
198 }
199
200 None
201}
202
203fn parse_ihdr_in_jp2h(data: &[u8]) -> Option<Vec<ComponentPrecision>> {
205 let mut pos = 0;
206 while pos + 8 <= data.len() {
207 let box_len =
208 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
209 let box_type = &data[pos + 4..pos + 8];
210
211 if box_len < 8 || pos + box_len > data.len() {
212 break;
213 }
214
215 if box_type == b"ihdr" {
216 let content = &data[pos + 8..pos + box_len];
218 if content.len() >= 11 {
219 let num_components = u16::from_be_bytes([content[8], content[9]]) as usize;
220 let bpc_byte = content[10];
221
222 if bpc_byte == 0xFF {
223 return None;
226 }
227
228 let bits = (bpc_byte & 0x7F) + 1;
229 let is_signed = (bpc_byte & 0x80) != 0;
230 let precision = ComponentPrecision { bits, is_signed };
231 return Some(vec![precision; num_components]);
232 }
233 }
234
235 pos += box_len;
236 }
237
238 None
239}
240
241fn parse_codestream_precision(data: &[u8]) -> Option<Vec<ComponentPrecision>> {
243 if data.len() < 4 || data[0] != 0xFF || data[1] != 0x4F {
245 return None;
246 }
247 if data[2] != 0xFF || data[3] != 0x51 {
248 return None;
249 }
250
251 if data.len() < 6 {
255 return None;
256 }
257 let siz_len = u16::from_be_bytes([data[4], data[5]]) as usize;
258 if data.len() < 4 + siz_len {
259 return None;
260 }
261
262 let siz_data = &data[6..4 + siz_len];
264 if siz_data.len() < 36 {
267 return None;
268 }
269
270 let csiz = u16::from_be_bytes([siz_data[34], siz_data[35]]) as usize;
271 let comp_start = 36;
272
273 let mut result = Vec::with_capacity(csiz);
274 for i in 0..csiz {
275 let offset = comp_start + i * 3;
276 if offset >= siz_data.len() {
277 break;
278 }
279 let ssiz = siz_data[offset];
280 let bits = (ssiz & 0x7F) + 1;
281 let is_signed = (ssiz & 0x80) != 0;
282 result.push(ComponentPrecision { bits, is_signed });
283 }
284
285 if result.is_empty() {
286 None
287 } else {
288 Some(result)
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_decode_invalid_data_returns_error() {
298 let result = decode(&[1, 2, 3]);
299 assert!(result.is_err());
300 let err = result.unwrap_err();
301 assert!(
302 err.to_string().contains("JPEG2000"),
303 "error should mention JPEG2000: {err}"
304 );
305 }
306
307 #[test]
308 fn test_decode_empty_data_returns_error() {
309 let result = decode(&[]);
310 assert!(result.is_err());
311 let err = result.unwrap_err();
312 assert!(
313 err.to_string().contains("JPEG2000"),
314 "error should mention JPEG2000: {err}"
315 );
316 }
317
318 #[test]
319 fn test_decode_truncated_jp2_header_returns_error() {
320 let data = b"\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A";
322 let result = decode(data);
323 assert!(result.is_err());
324 }
325
326 #[test]
327 fn test_decode_truncated_codestream_returns_error() {
328 let data = b"\xFF\x4F\xFF\x51\x00\x00";
330 let result = decode(data);
331 assert!(result.is_err());
332 }
333
334 #[test]
335 fn test_parse_codestream_precision_basic() {
336 let mut data = Vec::new();
338 data.extend_from_slice(&[0xFF, 0x4F]);
340 data.extend_from_slice(&[0xFF, 0x51]);
342 data.extend_from_slice(&[0x00, 44u8]);
344 data.extend_from_slice(&[0x00, 0x00]);
346 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x64]);
348 data.extend_from_slice(&[0x00, 0x00, 0x00, 0xC8]);
350 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
352 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
354 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x64]);
356 data.extend_from_slice(&[0x00, 0x00, 0x00, 0xC8]);
358 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
360 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
362 data.extend_from_slice(&[0x00, 0x02]);
364 data.extend_from_slice(&[0x07, 0x01, 0x01]);
366 data.extend_from_slice(&[0x8F, 0x01, 0x01]);
368
369 let result = parse_jp2_precision(&data).unwrap();
370 assert_eq!(result.len(), 2);
371 assert_eq!(result[0].bits, 8);
372 assert!(!result[0].is_signed);
373 assert_eq!(result[1].bits, 16);
374 assert!(result[1].is_signed);
375 }
376
377 #[test]
378 fn test_parse_jp2_precision_with_ihdr() {
379 let mut data = Vec::new();
381
382 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x0C]);
384 data.extend_from_slice(b"jP ");
385 data.extend_from_slice(&[0x0D, 0x0A, 0x87, 0x0A]);
386
387 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x14]);
389 data.extend_from_slice(b"ftyp");
390 data.extend_from_slice(b"jp2 ");
391 data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
392 data.extend_from_slice(b"jp2 ");
393
394 let ihdr_len: u32 = 22;
398 let jp2h_len: u32 = 8 + ihdr_len;
399
400 data.extend_from_slice(&jp2h_len.to_be_bytes());
401 data.extend_from_slice(b"jp2h");
402
403 data.extend_from_slice(&ihdr_len.to_be_bytes());
404 data.extend_from_slice(b"ihdr");
405 data.extend_from_slice(&480u32.to_be_bytes());
407 data.extend_from_slice(&640u32.to_be_bytes());
409 data.extend_from_slice(&3u16.to_be_bytes());
411 data.push(0x07);
413 data.push(0x07);
415 data.push(0x00);
417 data.push(0x00);
419
420 let result = parse_jp2_precision(&data).unwrap();
421 assert_eq!(result.len(), 3);
422 for comp in &result {
423 assert_eq!(comp.bits, 8);
424 assert!(!comp.is_signed);
425 }
426 }
427
428 #[test]
429 fn test_parse_jp2_precision_not_jp2() {
430 let data = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
432 assert!(parse_jp2_precision(&data).is_none());
433 }
434
435 #[test]
436 fn test_parse_jp2_precision_empty() {
437 assert!(parse_jp2_precision(&[]).is_none());
438 }
439
440 #[test]
441 fn test_component_precision_eq() {
442 let a = ComponentPrecision {
443 bits: 8,
444 is_signed: false,
445 };
446 let b = ComponentPrecision {
447 bits: 8,
448 is_signed: false,
449 };
450 assert_eq!(a, b);
451 }
452
453 #[test]
458 fn test_jpx_decode_options_default() {
459 let opts = JpxDecodeOptions::default();
460 assert_eq!(opts.target_resolution, None);
461 assert_eq!(opts.color_space_option, JpxColorSpaceOption::Normal);
462 assert!(!opts.strict);
463 }
464
465 #[test]
466 fn test_jpx_color_space_option_default() {
467 assert_eq!(JpxColorSpaceOption::default(), JpxColorSpaceOption::Normal);
468 }
469
470 #[test]
471 fn test_build_settings_default() {
472 let opts = JpxDecodeOptions::default();
473 let settings = build_decode_settings(&opts);
474 assert!(!settings.strict);
475 assert!(settings.resolve_palette_indices);
476 }
477
478 #[test]
479 fn test_build_settings_with_target_resolution() {
480 let opts = JpxDecodeOptions {
483 target_resolution: Some((320, 240)),
484 ..Default::default()
485 };
486 let settings = build_decode_settings(&opts);
487 assert!(!settings.strict);
489 }
490
491 #[test]
492 fn test_build_settings_color_space_none() {
493 let opts = JpxDecodeOptions {
494 color_space_option: JpxColorSpaceOption::None,
495 ..Default::default()
496 };
497 let settings = build_decode_settings(&opts);
498 assert!(!settings.resolve_palette_indices);
499 }
500
501 #[test]
502 fn test_build_settings_color_space_indexed() {
503 let opts = JpxDecodeOptions {
504 color_space_option: JpxColorSpaceOption::Indexed,
505 ..Default::default()
506 };
507 let settings = build_decode_settings(&opts);
508 assert!(settings.resolve_palette_indices);
509 }
510
511 #[test]
512 fn test_build_settings_strict() {
513 let opts = JpxDecodeOptions {
514 strict: true,
515 ..Default::default()
516 };
517 let settings = build_decode_settings(&opts);
518 assert!(settings.strict);
519 }
520
521 #[test]
522 fn test_decode_with_options_invalid_data() {
523 let opts = JpxDecodeOptions::default();
524 let result = decode_with_options(&[1, 2, 3], &opts);
525 assert!(result.is_err());
526 }
527
528 #[test]
529 fn test_decode_with_info_and_options_invalid_data() {
530 let opts = JpxDecodeOptions {
531 target_resolution: Some((100, 100)),
532 ..Default::default()
533 };
534 let result = decode_with_info_and_options(&[1, 2, 3], &opts);
535 assert!(result.is_err());
536 }
537
538 #[test]
539 fn test_max_resolutions_to_skip_constant() {
540 assert_eq!(MAX_RESOLUTIONS_TO_SKIP, 32);
541 }
542
543 #[test]
555 fn test_jpx_decode_null_data() {
556 let result = decode(&[]);
558 assert!(result.is_err());
559 let result2 = decode_with_options(&[], &JpxDecodeOptions::default());
560 assert!(result2.is_err());
561 }
562
563 #[test]
567 fn test_jpx_decode_null_stream_equivalent() {
568 let result = decode_with_info(&[]);
570 assert!(result.is_err());
571 let err = result.unwrap_err();
572 assert!(err.to_string().contains("JPEG2000"));
573 }
574
575 #[test]
579 fn test_jpx_decode_zero_size_data() {
580 let result = decode_with_info_and_options(&[], &JpxDecodeOptions::default());
581 assert!(result.is_err());
582 }
583
584 #[test]
586 #[ignore = "requires OpenJPEG stream adapter internals not exposed in hayro-jpeg2000"]
587 fn test_jpx_decode_data_read_in_bounds() {
588 }
591
592 #[test]
594 #[ignore = "requires OpenJPEG stream adapter internals not exposed in hayro-jpeg2000"]
595 fn test_jpx_decode_data_read_beyond_bounds() {
596 }
598
599 #[test]
601 #[ignore = "requires OpenJPEG stream adapter internals not exposed in hayro-jpeg2000"]
602 fn test_jpx_decode_data_skip() {
603 }
605
606 #[test]
608 #[ignore = "requires OpenJPEG stream adapter internals not exposed in hayro-jpeg2000"]
609 fn test_jpx_decode_data_seek() {
610 }
612
613 #[test]
615 #[ignore = "requires CJPX_Decoder::Sycc420ToRgbForTesting internals not exposed in hayro-jpeg2000"]
616 fn test_jpx_yuv420_to_rgb() {
617 }
620}