1#[cfg(feature = "_test-helpers")]
4use ultrahdr_core::color::tonemap::tonemap_image_to_srgb8;
5#[cfg(feature = "_test-helpers")]
6use ultrahdr_core::gainmap::compute::{GainMapConfig, compute_gainmap};
7use ultrahdr_core::metadata::{
8 iso21496::create_version_only_iso_app2,
9 mpf::create_mpf_header,
10 xmp::{build_gainmap_metadata_markers, create_xmp_app1_marker, generate_primary_xmp},
11};
12use ultrahdr_core::{ColorGamut, Error, GainMapEncodingFormat, GainMapMetadata, Result};
13#[cfg(feature = "_test-helpers")]
14use ultrahdr_core::{ColorTransfer, PixelFormat, Unstoppable};
15#[cfg(feature = "_test-helpers")]
16use ultrahdr_core::{GainMap, RawImage};
17
18use crate::jpeg::{
19 JpegSegment, create_icc_markers, get_icc_profile_for_gamut, insert_segment_after_soi,
20};
21
22pub fn encode_ultrahdr(
27 base_jpeg: &[u8],
28 gainmap_jpeg: &[u8],
29 metadata: &GainMapMetadata,
30 gamut: ColorGamut,
31) -> Result<Vec<u8>> {
32 encode_ultrahdr_with_format(
33 base_jpeg,
34 gainmap_jpeg,
35 metadata,
36 gamut,
37 GainMapEncodingFormat::Both,
38 )
39}
40
41pub fn encode_ultrahdr_with_format(
49 base_jpeg: &[u8],
50 gainmap_jpeg: &[u8],
51 metadata: &GainMapMetadata,
52 gamut: ColorGamut,
53 format: GainMapEncodingFormat,
54) -> Result<Vec<u8>> {
55 let metadata_markers = build_gainmap_metadata_markers(metadata, format);
57
58 let mut gainmap_final = gainmap_jpeg.to_vec();
60 for marker in metadata_markers.iter().rev() {
62 let segment = JpegSegment {
63 marker: marker[1], data: marker[4..].to_vec(),
65 offset: 0,
66 };
67 gainmap_final = insert_segment_after_soi(&gainmap_final, &segment)?;
68 }
69
70 let primary_xmp = generate_primary_xmp(gainmap_final.len());
72 let primary_xmp_marker = create_xmp_app1_marker(&primary_xmp);
73
74 let icc_profile = get_icc_profile_for_gamut(gamut);
76 let icc_markers = create_icc_markers(&icc_profile);
77
78 let xmp_segment = JpegSegment {
80 marker: 0xE1,
81 data: primary_xmp_marker[4..].to_vec(),
82 offset: 0,
83 };
84 let mut primary = insert_segment_after_soi(base_jpeg, &xmp_segment)?;
85
86 let include_iso = matches!(
90 format,
91 GainMapEncodingFormat::Iso21496 | GainMapEncodingFormat::Both
92 );
93 if include_iso {
94 let version_marker = create_version_only_iso_app2();
95 let iso_segment = JpegSegment {
96 marker: 0xE2,
97 data: version_marker[4..].to_vec(),
98 offset: 0,
99 };
100 primary = insert_segment_after_soi(&primary, &iso_segment)?;
101 }
102
103 for icc_marker in &icc_markers {
105 let icc_segment = JpegSegment {
106 marker: 0xE2,
107 data: icc_marker[4..].to_vec(),
108 offset: 0,
109 };
110 primary = insert_segment_after_soi(&primary, &icc_segment)?;
111 }
112
113 let mpf_insert_pos = 2;
115 let mpf_estimate = create_mpf_header(0, 0, Some(mpf_insert_pos)).len();
116 let primary_with_mpf_len = primary.len() + mpf_estimate;
117
118 let mpf_header = create_mpf_header(
120 primary_with_mpf_len,
121 gainmap_final.len(),
122 Some(mpf_insert_pos),
123 );
124
125 let mpf_segment = JpegSegment {
127 marker: 0xE2,
128 data: mpf_header[4..].to_vec(),
129 offset: 0,
130 };
131 let primary_final = insert_segment_after_soi(&primary, &mpf_segment)?;
132
133 let mut result = primary_final;
135 result.extend_from_slice(&gainmap_final);
136
137 Ok(result)
138}
139
140#[derive(Default)]
147pub struct Encoder {
148 #[cfg(feature = "_test-helpers")]
149 hdr_image: Option<RawImage>,
150 #[cfg(feature = "_test-helpers")]
151 sdr_image: Option<RawImage>,
152 compressed_sdr: Option<Vec<u8>>,
153 #[cfg(feature = "_test-helpers")]
154 existing_gainmap: Option<GainMap>,
155 existing_metadata: Option<GainMapMetadata>,
156 existing_gainmap_jpeg: Option<Vec<u8>>,
157 base_quality: u8,
158 gainmap_quality: u8,
159 gainmap_scale: u8,
160 target_display_peak: f32,
161 gain_map_min: f32,
162 #[cfg(feature = "_test-helpers")]
163 use_iso_metadata: bool,
164}
165
166impl Encoder {
167 pub fn new() -> Self {
169 Self {
170 #[cfg(feature = "_test-helpers")]
171 hdr_image: None,
172 #[cfg(feature = "_test-helpers")]
173 sdr_image: None,
174 compressed_sdr: None,
175 #[cfg(feature = "_test-helpers")]
176 existing_gainmap: None,
177 existing_metadata: None,
178 existing_gainmap_jpeg: None,
179 base_quality: 90,
180 gainmap_quality: 85,
181 gainmap_scale: 4,
182 target_display_peak: 10000.0,
183 gain_map_min: 1.0,
184 #[cfg(feature = "_test-helpers")]
185 use_iso_metadata: true,
186 }
187 }
188
189 #[cfg(feature = "_test-helpers")]
191 pub fn set_hdr_image(&mut self, image: RawImage) -> &mut Self {
192 self.hdr_image = Some(image);
193 self
194 }
195
196 #[cfg(feature = "_test-helpers")]
198 pub fn set_sdr_image(&mut self, image: RawImage) -> &mut Self {
199 self.sdr_image = Some(image);
200 self
201 }
202
203 pub fn set_compressed_sdr(&mut self, jpeg: Vec<u8>) -> &mut Self {
205 self.compressed_sdr = Some(jpeg);
206 self
207 }
208
209 pub fn set_base_jpeg(&mut self, jpeg: Vec<u8>) -> &mut Self {
211 self.set_compressed_sdr(jpeg)
212 }
213
214 #[cfg(feature = "_test-helpers")]
216 pub fn set_existing_gainmap(
217 &mut self,
218 gainmap: GainMap,
219 metadata: GainMapMetadata,
220 ) -> &mut Self {
221 self.existing_gainmap = Some(gainmap);
222 self.existing_metadata = Some(metadata);
223 self
224 }
225
226 #[cfg(feature = "_test-helpers")]
228 pub fn clear_existing_gainmap(&mut self) -> &mut Self {
229 self.existing_gainmap = None;
230 self.existing_metadata = None;
231 self.existing_gainmap_jpeg = None;
232 self
233 }
234
235 pub fn set_existing_gainmap_jpeg(
237 &mut self,
238 jpeg: Vec<u8>,
239 metadata: GainMapMetadata,
240 ) -> &mut Self {
241 self.existing_gainmap_jpeg = Some(jpeg);
242 self.existing_metadata = Some(metadata);
243 self
244 }
245
246 pub fn set_gainmap_jpeg(&mut self, jpeg: Vec<u8>, metadata: GainMapMetadata) -> &mut Self {
248 self.set_existing_gainmap_jpeg(jpeg, metadata)
249 }
250
251 #[cfg(feature = "_test-helpers")]
253 pub fn has_existing_gainmap(&self) -> bool {
254 self.existing_gainmap.is_some() && self.existing_metadata.is_some()
255 }
256
257 pub fn set_quality(&mut self, base: u8, gainmap: u8) -> &mut Self {
259 self.base_quality = base.clamp(1, 100);
260 self.gainmap_quality = gainmap.clamp(1, 100);
261 self
262 }
263
264 pub fn set_gainmap_scale(&mut self, scale: u8) -> &mut Self {
266 self.gainmap_scale = scale.clamp(1, 128);
267 self
268 }
269
270 pub fn set_target_display_peak(&mut self, nits: f32) -> &mut Self {
272 self.target_display_peak = nits.max(100.0);
273 self
274 }
275
276 pub fn set_min_content_boost(&mut self, boost: f32) -> &mut Self {
278 self.gain_map_min = boost.max(1.0);
279 self
280 }
281
282 #[cfg(feature = "_test-helpers")]
284 pub fn set_use_iso_metadata(&mut self, use_iso: bool) -> &mut Self {
285 self.use_iso_metadata = use_iso;
286 self
287 }
288
289 #[cfg(feature = "_test-helpers")]
291 pub fn encode(&self) -> Result<Vec<u8>> {
292 if let (Some(gainmap_jpeg), Some(metadata)) =
294 (&self.existing_gainmap_jpeg, &self.existing_metadata)
295 {
296 let (base_jpeg, gamut) = if let Some(ref compressed) = self.compressed_sdr {
297 (compressed.clone(), ColorGamut::Bt709)
298 } else if let Some(ref sdr_img) = self.sdr_image {
299 (self.encode_base_jpeg(sdr_img)?, sdr_img.gamut)
300 } else if let Some(ref hdr) = self.hdr_image {
301 let sdr_pixels = tonemap_image_to_srgb8(hdr, ColorGamut::Bt709)?;
302 let sdr = RawImage {
303 width: hdr.width,
304 height: hdr.height,
305 stride: hdr.width * 4,
306 data: sdr_pixels,
307 format: PixelFormat::Rgba8,
308 gamut: ColorGamut::Bt709,
309 transfer: ColorTransfer::Srgb,
310 };
311 (self.encode_base_jpeg(&sdr)?, sdr.gamut)
312 } else {
313 return Err(Error::EncodeError(
314 "Either HDR image, SDR image, or compressed SDR is required".into(),
315 ));
316 };
317
318 return encode_ultrahdr(&base_jpeg, gainmap_jpeg, metadata, gamut);
319 }
320
321 let hdr = self
323 .hdr_image
324 .as_ref()
325 .ok_or_else(|| Error::EncodeError("HDR image is required".into()))?;
326
327 let sdr = if let Some(ref sdr_img) = self.sdr_image {
329 sdr_img.clone()
330 } else {
331 let sdr_pixels = tonemap_image_to_srgb8(hdr, ColorGamut::Bt709)?;
332 RawImage {
333 width: hdr.width,
334 height: hdr.height,
335 stride: hdr.width * 4,
336 data: sdr_pixels,
337 format: PixelFormat::Rgba8,
338 gamut: ColorGamut::Bt709,
339 transfer: ColorTransfer::Srgb,
340 }
341 };
342
343 let (gainmap, metadata) =
345 if let (Some(gm), Some(meta)) = (&self.existing_gainmap, &self.existing_metadata) {
346 let expected_scale = self.gainmap_scale.max(1) as u32;
347 let expected_width = sdr.width.div_ceil(expected_scale);
348 let expected_height = sdr.height.div_ceil(expected_scale);
349
350 let width_ok =
351 gm.width >= expected_width.saturating_sub(1) && gm.width <= expected_width + 1;
352 let height_ok = gm.height >= expected_height.saturating_sub(1)
353 && gm.height <= expected_height + 1;
354
355 if width_ok && height_ok {
356 (gm.clone(), meta.clone())
357 } else {
358 self.compute_new_gainmap(hdr, &sdr)?
359 }
360 } else {
361 self.compute_new_gainmap(hdr, &sdr)?
362 };
363
364 let base_jpeg = if let Some(ref compressed) = self.compressed_sdr {
366 compressed.clone()
367 } else {
368 self.encode_base_jpeg(&sdr)?
369 };
370
371 let gainmap_jpeg = self.encode_gainmap_jpeg(&gainmap)?;
373
374 encode_ultrahdr(&base_jpeg, &gainmap_jpeg, &metadata, sdr.gamut)
375 }
376
377 pub fn encode_from_jpegs(&self) -> Result<Vec<u8>> {
379 let base_jpeg = self
380 .compressed_sdr
381 .as_ref()
382 .ok_or_else(|| Error::EncodeError("Base JPEG not set".into()))?;
383
384 let gainmap_jpeg = self
385 .existing_gainmap_jpeg
386 .as_ref()
387 .ok_or_else(|| Error::EncodeError("Gainmap JPEG not set".into()))?;
388
389 let metadata = self
390 .existing_metadata
391 .as_ref()
392 .ok_or_else(|| Error::EncodeError("Metadata not set".into()))?;
393
394 encode_ultrahdr(base_jpeg, gainmap_jpeg, metadata, ColorGamut::Bt709)
395 }
396
397 #[cfg(feature = "_test-helpers")]
399 fn compute_new_gainmap(
400 &self,
401 hdr: &RawImage,
402 sdr: &RawImage,
403 ) -> Result<(GainMap, GainMapMetadata)> {
404 let config = GainMapConfig {
405 scale_factor: self.gainmap_scale,
406 gamma: 1.0,
407 multi_channel: false,
408 min_boost: self.gain_map_min,
409 max_boost: self.target_display_peak / 203.0,
410 base_offset: 1.0 / 64.0,
411 alternate_offset: 1.0 / 64.0,
412 base_hdr_headroom: 1.0, alternate_hdr_headroom: self.target_display_peak / 203.0,
414 };
415
416 compute_gainmap(hdr, sdr, &config, Unstoppable)
417 }
418
419 #[cfg(feature = "_test-helpers")]
421 fn encode_base_jpeg(&self, sdr: &RawImage) -> Result<Vec<u8>> {
422 use zenjpeg::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout, Unstoppable};
423
424 let (pixel_layout, data): (PixelLayout, std::borrow::Cow<[u8]>) = match sdr.format {
425 PixelFormat::Rgba8 => {
426 let rgb: Vec<u8> = sdr
427 .data
428 .chunks(4)
429 .flat_map(|rgba| [rgba[0], rgba[1], rgba[2]])
430 .collect();
431 (PixelLayout::Rgb8Srgb, std::borrow::Cow::Owned(rgb))
432 }
433 PixelFormat::Rgb8 => (
434 PixelLayout::Rgb8Srgb,
435 std::borrow::Cow::Borrowed(&sdr.data[..]),
436 ),
437 _ => {
438 return Err(Error::EncodeError(format!(
439 "Unsupported SDR pixel format: {:?}",
440 sdr.format
441 )));
442 }
443 };
444
445 let config = EncoderConfig::ycbcr(self.base_quality as f32, ChromaSubsampling::Quarter);
446 let mut enc = config
447 .encode_from_bytes(sdr.width, sdr.height, pixel_layout)
448 .map_err(|e| Error::JpegEncode(e.to_string()))?;
449 enc.push_packed(&data, Unstoppable)
450 .map_err(|e| Error::JpegEncode(e.to_string()))?;
451 enc.finish().map_err(|e| Error::JpegEncode(e.to_string()))
452 }
453
454 #[cfg(feature = "_test-helpers")]
456 fn encode_gainmap_jpeg(&self, gainmap: &GainMap) -> Result<Vec<u8>> {
457 use zenjpeg::encoder::{EncoderConfig, PixelLayout, Unstoppable};
458
459 let config = EncoderConfig::grayscale(self.gainmap_quality as f32);
460 let mut enc = config
461 .encode_from_bytes(gainmap.width, gainmap.height, PixelLayout::Gray8Srgb)
462 .map_err(|e| Error::JpegEncode(e.to_string()))?;
463 enc.push_packed(&gainmap.data, Unstoppable)
464 .map_err(|e| Error::JpegEncode(e.to_string()))?;
465 enc.finish().map_err(|e| Error::JpegEncode(e.to_string()))
466 }
467}
468
469#[cfg(all(test, feature = "_test-helpers"))]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_encoder_creation() {
475 let encoder = Encoder::new();
476 assert_eq!(encoder.base_quality, 90);
477 assert_eq!(encoder.gainmap_quality, 85);
478 assert_eq!(encoder.gainmap_scale, 4);
479 }
480
481 #[test]
482 fn test_encoder_builder() {
483 let mut encoder = Encoder::new();
484 encoder
485 .set_quality(95, 90)
486 .set_gainmap_scale(2)
487 .set_target_display_peak(4000.0);
488
489 assert_eq!(encoder.base_quality, 95);
490 assert_eq!(encoder.gainmap_quality, 90);
491 assert_eq!(encoder.gainmap_scale, 2);
492 assert_eq!(encoder.target_display_peak, 4000.0);
493 }
494
495 #[test]
496 fn test_encode_requires_hdr() {
497 let encoder = Encoder::new();
498 let result = encoder.encode();
499 assert!(result.is_err());
500 }
501
502 #[test]
503 fn test_existing_gainmap_methods() {
504 let mut encoder = Encoder::new();
505
506 assert!(!encoder.has_existing_gainmap());
507
508 let gainmap = GainMap::new(100, 100).unwrap();
509 let metadata = GainMapMetadata::new();
510 encoder.set_existing_gainmap(gainmap, metadata);
511 assert!(encoder.has_existing_gainmap());
512
513 encoder.clear_existing_gainmap();
514 assert!(!encoder.has_existing_gainmap());
515 }
516}