1#[cfg(feature = "_test-helpers")]
4use ultrahdr_core::gainmap::apply::{HdrOutputFormat, apply_gainmap};
5use ultrahdr_core::metadata::{mpf::find_jpeg_boundaries, xmp::parse_xmp};
6#[cfg(feature = "_test-helpers")]
7use ultrahdr_core::{ColorGamut, ColorTransfer, PixelFormat, Unstoppable};
8use ultrahdr_core::{Error, GainMap, GainMapMetadata, RawImage, Result};
9
10use crate::container::{self, AppSegment};
11
12pub struct Decoder<'a> {
20 data: &'a [u8],
21 metadata: Option<GainMapMetadata>,
22 primary_jpeg: Option<(usize, usize)>,
23 gainmap_jpeg: Option<(usize, usize)>,
24 is_ultrahdr: bool,
25}
26
27impl<'a> Decoder<'a> {
28 pub fn new(data: &'a [u8]) -> Result<Self> {
32 let mut decoder = Self {
33 data,
34 metadata: None,
35 primary_jpeg: None,
36 gainmap_jpeg: None,
37 is_ultrahdr: false,
38 };
39
40 decoder.parse()?;
41 Ok(decoder)
42 }
43
44 pub fn is_ultrahdr(&self) -> bool {
46 self.is_ultrahdr
47 }
48
49 pub fn metadata(&self) -> Option<&GainMapMetadata> {
51 self.metadata.as_ref()
52 }
53
54 pub fn primary_jpeg(&self) -> Option<&[u8]> {
58 self.primary_jpeg
59 .and_then(|(start, end)| self.data.get(start..end))
60 }
61
62 pub fn gainmap_jpeg(&self) -> Option<&[u8]> {
66 self.gainmap_jpeg
67 .and_then(|(start, end)| self.data.get(start..end))
68 }
69
70 #[cfg(feature = "_test-helpers")]
76 pub fn decode_sdr(&self) -> Result<RawImage> {
77 let primary_data = self
78 .primary_jpeg()
79 .ok_or_else(|| Error::DecodeError("No primary image found".into()))?;
80 decode_jpeg_to_rgb(primary_data)
81 }
82
83 #[cfg(not(feature = "_test-helpers"))]
88 pub fn decode_sdr(&self) -> Result<RawImage> {
89 Err(Error::DecodeError(
90 "decode_sdr() requires a JPEG codec. Use primary_jpeg() to get raw bytes \
91 and decode with your own codec"
92 .into(),
93 ))
94 }
95
96 #[cfg(feature = "_test-helpers")]
102 pub fn decode_gainmap(&self) -> Result<GainMap> {
103 let gainmap_data = self
104 .gainmap_jpeg()
105 .ok_or_else(|| Error::DecodeError("No gain map found".into()))?;
106 let decoded = decode_jpeg_to_grayscale(gainmap_data)?;
107
108 Ok(GainMap {
109 width: decoded.width,
110 height: decoded.height,
111 channels: 1,
112 data: decoded.data,
113 })
114 }
115
116 #[cfg(not(feature = "_test-helpers"))]
121 pub fn decode_gainmap(&self) -> Result<GainMap> {
122 Err(Error::DecodeError(
123 "decode_gainmap() requires a JPEG codec. Use gainmap_jpeg() to get raw bytes \
124 and decode with your own codec"
125 .into(),
126 ))
127 }
128
129 #[cfg(feature = "_test-helpers")]
141 pub fn decode_hdr(&self, display_boost: f32) -> Result<RawImage> {
142 self.decode_hdr_with_format(display_boost, HdrOutputFormat::LinearFloat)
143 }
144
145 #[cfg(feature = "_test-helpers")]
149 pub fn decode_hdr_with_format(
150 &self,
151 display_boost: f32,
152 format: HdrOutputFormat,
153 ) -> Result<RawImage> {
154 if !self.is_ultrahdr {
155 return Err(Error::DecodeError("Not an Ultra HDR image".into()));
156 }
157
158 if !display_boost.is_finite() || display_boost < 1.0 {
159 return Err(Error::DecodeError(format!(
160 "display_boost must be >= 1.0, got {}",
161 display_boost
162 )));
163 }
164
165 let metadata = self
166 .metadata
167 .as_ref()
168 .ok_or_else(|| Error::DecodeError("No gain map metadata".into()))?;
169
170 let sdr = self.decode_sdr()?;
171 let gainmap = self.decode_gainmap()?;
172
173 apply_gainmap(&sdr, &gainmap, metadata, display_boost, format, Unstoppable)
174 }
175
176 fn parse(&mut self) -> Result<()> {
181 if self.data.len() < 4 || self.data[0] != 0xFF || self.data[1] != 0xD8 {
183 return Err(Error::DecodeError("Not a valid JPEG".into()));
184 }
185
186 let segments = container::scan_segments(self.data);
188
189 if let Some(xmp_str) = find_xmp_in_segments(&segments)
191 && (xmp_str.contains("hdrgm:") || xmp_str.contains("http://ns.adobe.com/hdr-gain-map/"))
192 {
193 self.is_ultrahdr = true;
194 if let Ok((metadata, _gainmap_len)) = parse_xmp(&xmp_str)
196 && (metadata.alternate_hdr_headroom != 0.0 || metadata.gain_map_max != [0.0; 3])
197 {
198 self.metadata = Some(metadata);
199 }
200 }
201
202 if let Some(mpf_seg) = segments.iter().find(|s| s.is_mpf())
204 && let Ok(mpf_dir) = container::parse_mpf_segment(&mpf_seg.data, mpf_seg.offset)
205 && mpf_dir.entries.len() >= 2
206 {
207 let primary_size = mpf_dir.entries[0].size as usize;
209 self.primary_jpeg = Some((0, primary_size));
210
211 let secondaries = container::extract_secondary_images(self.data, &mpf_dir);
213 if let Some(gm) = secondaries.first() {
214 let gm_start = gm.as_ptr() as usize - self.data.as_ptr() as usize;
215 self.gainmap_jpeg = Some((gm_start, gm_start + gm.len()));
216 self.is_ultrahdr = true;
217
218 if self.metadata.is_none() {
221 let gm_segments = container::scan_segments(gm);
222 if let Some(gm_xmp) = find_xmp_in_segments(&gm_segments)
223 && gm_xmp.contains("hdrgm:")
224 && let Ok((gm_metadata, _)) = parse_xmp(&gm_xmp)
225 {
226 self.metadata = Some(gm_metadata);
227 }
228 }
229 }
230 }
231
232 if self.gainmap_jpeg.is_none() {
234 let boundaries = find_jpeg_boundaries(self.data);
235 if boundaries.len() >= 2 {
236 self.primary_jpeg = Some(boundaries[0]);
237 self.gainmap_jpeg = Some(boundaries[1]);
238
239 if self.metadata.is_none()
241 && let (gm_start, gm_end) = boundaries[1]
242 && let Some(gm_data) = self.data.get(gm_start..gm_end)
243 {
244 let gm_segments = container::scan_segments(gm_data);
245 if let Some(gm_xmp) = find_xmp_in_segments(&gm_segments)
246 && gm_xmp.contains("hdrgm:")
247 && let Ok((gm_metadata, _)) = parse_xmp(&gm_xmp)
248 {
249 self.metadata = Some(gm_metadata);
250 }
251 }
252 }
253 }
254
255 if self.primary_jpeg.is_none() {
257 self.primary_jpeg = Some((0, self.data.len()));
258 }
259
260 Ok(())
261 }
262
263 pub fn icc_profile(&self) -> Option<Vec<u8>> {
265 crate::jpeg::extract_icc_profile(self.data)
266 }
267
268 #[cfg(feature = "_test-helpers")]
272 pub fn dimensions(&self) -> Result<(u32, u32)> {
273 let sdr = self.decode_sdr()?;
274 Ok((sdr.width, sdr.height))
275 }
276}
277
278fn find_xmp_in_segments(segments: &[AppSegment]) -> Option<String> {
283 let xmp_ns = b"http://ns.adobe.com/xap/1.0/\0";
284
285 for seg in segments {
286 if seg.is_xmp() && seg.data.len() > xmp_ns.len() {
287 let xmp_bytes = &seg.data[xmp_ns.len()..];
288 if let Ok(xmp) = std::str::from_utf8(xmp_bytes) {
289 return Some(xmp.to_string());
290 }
291 }
292 }
293
294 None
295}
296
297#[cfg(feature = "_test-helpers")]
299fn decode_jpeg_to_rgb(jpeg_data: &[u8]) -> Result<RawImage> {
300 use zenjpeg::decoder::{Decoder as JpegDecoder, PixelFormat as JpegPixelFormat};
301 let decoded = JpegDecoder::new()
302 .output_format(JpegPixelFormat::Rgb)
303 .decode(jpeg_data, Unstoppable)
304 .map_err(|e| Error::DecodeError(format!("JPEG decode failed: {}", e)))?;
305
306 let width = decoded.width();
307 let height = decoded.height();
308 let pixels = decoded
309 .pixels_u8()
310 .ok_or_else(|| Error::DecodeError("No pixel data in decoded JPEG".into()))?;
311 let bpp = decoded.bytes_per_pixel();
312
313 let data = if bpp == 3 {
315 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
317 for chunk in pixels.chunks(3) {
318 rgba.push(chunk[0]);
319 rgba.push(chunk[1]);
320 rgba.push(chunk[2]);
321 rgba.push(255);
322 }
323 rgba
324 } else if bpp == 4 {
325 pixels.to_vec()
326 } else if bpp == 1 {
327 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
329 for &g in pixels {
330 rgba.push(g);
331 rgba.push(g);
332 rgba.push(g);
333 rgba.push(255);
334 }
335 rgba
336 } else {
337 return Err(Error::DecodeError(format!(
338 "Unsupported bytes per pixel: {}",
339 bpp
340 )));
341 };
342
343 Ok(RawImage {
344 width,
345 height,
346 stride: width * 4,
347 data,
348 format: PixelFormat::Rgba8,
349 gamut: ColorGamut::Bt709, transfer: ColorTransfer::Srgb,
351 })
352}
353
354#[cfg(feature = "_test-helpers")]
356fn decode_jpeg_to_grayscale(jpeg_data: &[u8]) -> Result<RawImage> {
357 use zenjpeg::decoder::{Decoder as JpegDecoder, PixelFormat as JpegPixelFormat};
358 let decoded = JpegDecoder::new()
359 .output_format(JpegPixelFormat::Gray)
360 .decode(jpeg_data, Unstoppable)
361 .map_err(|e| Error::DecodeError(format!("JPEG decode failed: {}", e)))?;
362
363 let width = decoded.width();
364 let height = decoded.height();
365 let pixels = decoded
366 .pixels_u8()
367 .ok_or_else(|| Error::DecodeError("No pixel data in decoded JPEG".into()))?;
368 let bpp = decoded.bytes_per_pixel();
369
370 let data = if bpp == 1 {
372 pixels.to_vec()
373 } else if bpp == 3 {
374 pixels
376 .chunks(3)
377 .map(|rgb| {
378 let r = rgb[0] as f32;
379 let g = rgb[1] as f32;
380 let b = rgb[2] as f32;
381 (0.2126_f32 * r + 0.7152 * g + 0.0722 * b).clamp(0.0, 255.0) as u8
383 })
384 .collect()
385 } else {
386 return Err(Error::DecodeError(format!(
387 "Unsupported bytes per pixel for grayscale: {}",
388 bpp
389 )));
390 };
391
392 Ok(RawImage {
393 width,
394 height,
395 stride: width,
396 data,
397 format: PixelFormat::Gray8,
398 gamut: ColorGamut::Bt709,
399 transfer: ColorTransfer::Srgb,
400 })
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_decoder_invalid_data() {
409 let result = Decoder::new(&[0, 1, 2, 3]);
410 assert!(result.is_err());
411 }
412
413 #[test]
414 fn test_decoder_minimal_jpeg() {
415 let data = vec![0xFF, 0xD8, 0xFF, 0xD9];
417 let decoder = Decoder::new(&data);
418 assert!(decoder.is_ok());
419 assert!(!decoder.unwrap().is_ultrahdr());
420 }
421
422 #[test]
423 fn test_decoder_not_ultrahdr() {
424 let data = vec![
426 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xD9, ];
431 let decoder = Decoder::new(&data).unwrap();
432 assert!(!decoder.is_ultrahdr());
433 assert!(decoder.metadata().is_none());
434 assert!(decoder.gainmap_jpeg().is_none());
435 assert!(decoder.primary_jpeg().is_some());
437 }
438
439 #[test]
440 fn test_decoder_borrows_data() {
441 let data = vec![0xFF, 0xD8, 0xFF, 0xD9];
442 let decoder = Decoder::new(&data).unwrap();
443 let primary = decoder.primary_jpeg().unwrap();
445 assert_eq!(primary.as_ptr(), data.as_ptr());
446 }
447
448 #[test]
449 fn test_decoder_empty_too_short() {
450 assert!(Decoder::new(&[]).is_err());
451 assert!(Decoder::new(&[0xFF]).is_err());
452 assert!(Decoder::new(&[0xFF, 0xD8]).is_err()); }
454
455 #[test]
456 fn test_decoder_icc_profile_none() {
457 let data = vec![0xFF, 0xD8, 0xFF, 0xD9];
458 let decoder = Decoder::new(&data).unwrap();
459 assert!(decoder.icc_profile().is_none());
460 }
461
462 #[test]
463 fn test_decoder_two_jpeg_fallback() {
464 let data = vec![
466 0xFF, 0xD8, 0xFF, 0xD9, 0xFF, 0xD8, 0xFF, 0xD9, ];
471 let decoder = Decoder::new(&data).unwrap();
473 assert!(decoder.primary_jpeg().is_some());
474 assert!(decoder.gainmap_jpeg().is_some());
475 }
476
477 #[test]
478 fn test_find_xmp_in_segments_none() {
479 let segments: Vec<AppSegment> = vec![];
480 assert!(find_xmp_in_segments(&segments).is_none());
481 }
482
483 #[test]
484 fn test_decoder_xmp_without_hdrgm() {
485 let xmp_ns = b"http://ns.adobe.com/xap/1.0/\0";
487 let xmp_body = b"<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"><rdf:Description rdf:about=\"\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"><dc:creator>test</dc:creator></rdf:Description></rdf:RDF></x:xmpmeta>";
488 let segment_data_len = xmp_ns.len() + xmp_body.len();
489 let segment_len = (segment_data_len + 2) as u16; let mut data = Vec::new();
492 data.extend_from_slice(&[0xFF, 0xD8]); data.push(0xFF);
494 data.push(0xE1); data.extend_from_slice(&segment_len.to_be_bytes());
496 data.extend_from_slice(xmp_ns);
497 data.extend_from_slice(xmp_body);
498 data.extend_from_slice(&[0xFF, 0xD9]); let decoder = Decoder::new(&data).unwrap();
501 assert!(!decoder.is_ultrahdr());
502 assert!(decoder.metadata().is_none());
503 }
504
505 #[test]
506 fn test_decoder_primary_jpeg_is_full_data_when_no_mpf() {
507 let data = vec![
509 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xD9, ];
514 let decoder = Decoder::new(&data).unwrap();
515 let primary = decoder.primary_jpeg().unwrap();
516 assert_eq!(primary.len(), data.len());
517 assert_eq!(primary, &data[..]);
518 }
519
520 #[test]
521 fn test_decoder_gainmap_none_on_plain_jpeg() {
522 let data = vec![
524 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x07, b'J', b'F', b'I', b'F', 0x00, 0xFF, 0xD9, ];
529 let decoder = Decoder::new(&data).unwrap();
530 assert!(decoder.gainmap_jpeg().is_none());
531 }
532
533 #[test]
534 fn test_find_xmp_in_segments_with_non_xmp() {
535 let segments = vec![AppSegment {
537 marker_num: 1,
538 data: b"Exif\0\0some_exif_data_here".to_vec(),
539 offset: 0,
540 }];
541 assert!(find_xmp_in_segments(&segments).is_none());
542
543 let segments = vec![AppSegment {
545 marker_num: 1,
546 data: b"SomeRandomPrefix\0and_data".to_vec(),
547 offset: 0,
548 }];
549 assert!(find_xmp_in_segments(&segments).is_none());
550 }
551
552 #[test]
553 fn test_find_xmp_in_segments_with_xmp() {
554 let xmp_ns = b"http://ns.adobe.com/xap/1.0/\0";
555 let xmp_xml = b"<x:xmpmeta><rdf:RDF><rdf:Description/></rdf:RDF></x:xmpmeta>";
556
557 let mut segment_data = Vec::new();
558 segment_data.extend_from_slice(xmp_ns);
559 segment_data.extend_from_slice(xmp_xml);
560
561 let segments = vec![AppSegment {
562 marker_num: 1,
563 data: segment_data,
564 offset: 10,
565 }];
566
567 let result = find_xmp_in_segments(&segments);
568 assert!(result.is_some());
569 let xmp_str = result.unwrap();
570 assert!(xmp_str.contains("<x:xmpmeta>"));
571 assert!(xmp_str.contains("<rdf:RDF>"));
572 }
573}