1use ultrahdr_core::color::tonemap::tonemap_image_to_srgb8;
4use ultrahdr_core::gainmap::compute::{compute_gainmap, GainMapConfig};
5use ultrahdr_core::metadata::{
6 mpf::create_mpf_header,
7 xmp::{create_xmp_app1_marker, generate_xmp},
8};
9use ultrahdr_core::{
10 ColorGamut, ColorTransfer, Error, GainMap, GainMapMetadata, PixelFormat, RawImage, Result,
11 Unstoppable,
12};
13
14use crate::jpeg::{
15 create_icc_markers, get_icc_profile_for_gamut, insert_segment_after_soi, JpegSegment,
16};
17
18pub struct Encoder {
26 hdr_image: Option<RawImage>,
27 sdr_image: Option<RawImage>,
28 compressed_sdr: Option<Vec<u8>>,
29 existing_gainmap: Option<GainMap>,
30 existing_metadata: Option<GainMapMetadata>,
31 existing_gainmap_jpeg: Option<Vec<u8>>,
33 base_quality: u8,
34 gainmap_quality: u8,
35 gainmap_scale: u8,
36 target_display_peak: f32,
37 min_content_boost: f32,
38 use_iso_metadata: bool,
39}
40
41impl Default for Encoder {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl Encoder {
48 pub fn new() -> Self {
50 Self {
51 hdr_image: None,
52 sdr_image: None,
53 compressed_sdr: None,
54 existing_gainmap: None,
55 existing_metadata: None,
56 existing_gainmap_jpeg: None,
57 base_quality: 90,
58 gainmap_quality: 85,
59 gainmap_scale: 4,
60 target_display_peak: 10000.0,
61 min_content_boost: 1.0,
62 use_iso_metadata: true,
63 }
64 }
65
66 pub fn set_hdr_image(&mut self, image: RawImage) -> &mut Self {
68 self.hdr_image = Some(image);
69 self
70 }
71
72 pub fn set_sdr_image(&mut self, image: RawImage) -> &mut Self {
76 self.sdr_image = Some(image);
77 self
78 }
79
80 pub fn set_compressed_sdr(&mut self, jpeg: Vec<u8>) -> &mut Self {
85 self.compressed_sdr = Some(jpeg);
86 self
87 }
88
89 pub fn set_existing_gainmap(
99 &mut self,
100 gainmap: GainMap,
101 metadata: GainMapMetadata,
102 ) -> &mut Self {
103 self.existing_gainmap = Some(gainmap);
104 self.existing_metadata = Some(metadata);
105 self
106 }
107
108 pub fn clear_existing_gainmap(&mut self) -> &mut Self {
110 self.existing_gainmap = None;
111 self.existing_metadata = None;
112 self.existing_gainmap_jpeg = None;
113 self
114 }
115
116 pub fn set_existing_gainmap_jpeg(
124 &mut self,
125 jpeg: Vec<u8>,
126 metadata: GainMapMetadata,
127 ) -> &mut Self {
128 self.existing_gainmap_jpeg = Some(jpeg);
129 self.existing_metadata = Some(metadata);
130 self
131 }
132
133 pub fn has_existing_gainmap(&self) -> bool {
135 self.existing_gainmap.is_some() && self.existing_metadata.is_some()
136 }
137
138 pub fn set_quality(&mut self, base: u8, gainmap: u8) -> &mut Self {
142 self.base_quality = base.clamp(1, 100);
143 self.gainmap_quality = gainmap.clamp(1, 100);
144 self
145 }
146
147 pub fn set_gainmap_scale(&mut self, scale: u8) -> &mut Self {
153 self.gainmap_scale = scale.clamp(1, 128);
154 self
155 }
156
157 pub fn set_target_display_peak(&mut self, nits: f32) -> &mut Self {
161 self.target_display_peak = nits.max(100.0);
162 self
163 }
164
165 pub fn set_min_content_boost(&mut self, boost: f32) -> &mut Self {
169 self.min_content_boost = boost.max(1.0);
170 self
171 }
172
173 pub fn set_use_iso_metadata(&mut self, use_iso: bool) -> &mut Self {
177 self.use_iso_metadata = use_iso;
178 self
179 }
180
181 pub fn encode(&self) -> Result<Vec<u8>> {
183 if let (Some(ref gainmap_jpeg), Some(ref metadata)) =
185 (&self.existing_gainmap_jpeg, &self.existing_metadata)
186 {
187 let (base_jpeg, gamut) = if let Some(ref compressed) = self.compressed_sdr {
189 (compressed.clone(), ColorGamut::Bt709)
190 } else if let Some(ref sdr_img) = self.sdr_image {
191 (self.encode_base_jpeg(sdr_img)?, sdr_img.gamut)
192 } else if let Some(ref hdr) = self.hdr_image {
193 let sdr_pixels = tonemap_image_to_srgb8(hdr, ColorGamut::Bt709);
195 let sdr = RawImage {
196 width: hdr.width,
197 height: hdr.height,
198 stride: hdr.width * 4,
199 data: sdr_pixels,
200 format: PixelFormat::Rgba8,
201 gamut: ColorGamut::Bt709,
202 transfer: ColorTransfer::Srgb,
203 };
204 (self.encode_base_jpeg(&sdr)?, sdr.gamut)
205 } else {
206 return Err(Error::EncodeError(
207 "Either HDR image, SDR image, or compressed SDR is required".into(),
208 ));
209 };
210
211 return self.create_ultrahdr_jpeg(&base_jpeg, gainmap_jpeg, metadata, gamut);
212 }
213
214 let hdr = self
216 .hdr_image
217 .as_ref()
218 .ok_or_else(|| Error::EncodeError("HDR image is required".into()))?;
219
220 let sdr = if let Some(ref sdr_img) = self.sdr_image {
222 sdr_img.clone()
223 } else {
224 let sdr_pixels = tonemap_image_to_srgb8(hdr, ColorGamut::Bt709);
226 RawImage {
227 width: hdr.width,
228 height: hdr.height,
229 stride: hdr.width * 4,
230 data: sdr_pixels,
231 format: PixelFormat::Rgba8,
232 gamut: ColorGamut::Bt709,
233 transfer: ColorTransfer::Srgb,
234 }
235 };
236
237 let (gainmap, metadata) =
239 if let (Some(gm), Some(meta)) = (&self.existing_gainmap, &self.existing_metadata) {
240 let expected_scale = self.gainmap_scale.max(1) as u32;
243 let expected_width = sdr.width.div_ceil(expected_scale);
244 let expected_height = sdr.height.div_ceil(expected_scale);
245
246 let width_ok =
248 gm.width >= expected_width.saturating_sub(1) && gm.width <= expected_width + 1;
249 let height_ok = gm.height >= expected_height.saturating_sub(1)
250 && gm.height <= expected_height + 1;
251
252 if width_ok && height_ok {
253 (gm.clone(), meta.clone())
255 } else {
256 self.compute_new_gainmap(hdr, &sdr)?
258 }
259 } else {
260 self.compute_new_gainmap(hdr, &sdr)?
262 };
263
264 let base_jpeg = if let Some(ref compressed) = self.compressed_sdr {
266 compressed.clone()
267 } else {
268 self.encode_base_jpeg(&sdr)?
269 };
270
271 let gainmap_jpeg = self.encode_gainmap_jpeg(&gainmap)?;
273
274 self.create_ultrahdr_jpeg(&base_jpeg, &gainmap_jpeg, &metadata, sdr.gamut)
276 }
277
278 fn compute_new_gainmap(
280 &self,
281 hdr: &RawImage,
282 sdr: &RawImage,
283 ) -> Result<(GainMap, GainMapMetadata)> {
284 let config = GainMapConfig {
285 scale_factor: self.gainmap_scale,
286 gamma: 1.0,
287 multi_channel: false,
288 min_content_boost: self.min_content_boost,
289 max_content_boost: self.target_display_peak / 203.0, offset_sdr: 1.0 / 64.0,
291 offset_hdr: 1.0 / 64.0,
292 hdr_capacity_min: 1.0,
293 hdr_capacity_max: self.target_display_peak / 203.0,
294 };
295
296 compute_gainmap(hdr, sdr, &config, Unstoppable)
297 }
298
299 fn encode_base_jpeg(&self, sdr: &RawImage) -> Result<Vec<u8>> {
301 use jpegli::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout, Unstoppable};
302
303 let (pixel_layout, data): (PixelLayout, std::borrow::Cow<[u8]>) = match sdr.format {
304 PixelFormat::Rgba8 => {
305 let rgb: Vec<u8> = sdr
307 .data
308 .chunks(4)
309 .flat_map(|rgba| [rgba[0], rgba[1], rgba[2]])
310 .collect();
311 (PixelLayout::Rgb8Srgb, std::borrow::Cow::Owned(rgb))
312 }
313 PixelFormat::Rgb8 => (
314 PixelLayout::Rgb8Srgb,
315 std::borrow::Cow::Borrowed(&sdr.data[..]),
316 ),
317 _ => {
318 return Err(Error::EncodeError(format!(
319 "Unsupported SDR pixel format: {:?}",
320 sdr.format
321 )))
322 }
323 };
324
325 let config = EncoderConfig::ycbcr(self.base_quality as f32, ChromaSubsampling::Quarter);
326 let mut enc = config
327 .encode_from_bytes(sdr.width, sdr.height, pixel_layout)
328 .map_err(|e| Error::JpegEncode(e.to_string()))?;
329 enc.push_packed(&data, Unstoppable)
330 .map_err(|e| Error::JpegEncode(e.to_string()))?;
331 enc.finish().map_err(|e| Error::JpegEncode(e.to_string()))
332 }
333
334 fn encode_gainmap_jpeg(&self, gainmap: &crate::GainMap) -> Result<Vec<u8>> {
336 use jpegli::encoder::{EncoderConfig, PixelLayout, Unstoppable};
337
338 let config = EncoderConfig::grayscale(self.gainmap_quality as f32);
339 let mut enc = config
340 .encode_from_bytes(gainmap.width, gainmap.height, PixelLayout::Gray8Srgb)
341 .map_err(|e| Error::JpegEncode(e.to_string()))?;
342 enc.push_packed(&gainmap.data, Unstoppable)
343 .map_err(|e| Error::JpegEncode(e.to_string()))?;
344 enc.finish().map_err(|e| Error::JpegEncode(e.to_string()))
345 }
346
347 fn create_ultrahdr_jpeg(
349 &self,
350 base_jpeg: &[u8],
351 gainmap_jpeg: &[u8],
352 metadata: &GainMapMetadata,
353 gamut: ColorGamut,
354 ) -> Result<Vec<u8>> {
355 let xmp = generate_xmp(metadata, gainmap_jpeg.len());
357 let xmp_marker = create_xmp_app1_marker(&xmp);
358
359 let icc_profile = get_icc_profile_for_gamut(gamut);
361 let icc_markers = create_icc_markers(&icc_profile);
362
363 let xmp_segment = JpegSegment {
365 marker: 0xE1,
366 data: xmp_marker[4..].to_vec(), offset: 0,
368 };
369 let mut primary = insert_segment_after_soi(base_jpeg, &xmp_segment)?;
370
371 for icc_marker in &icc_markers {
373 let icc_segment = JpegSegment {
374 marker: 0xE2,
375 data: icc_marker[4..].to_vec(),
376 offset: 0,
377 };
378 primary = insert_segment_after_soi(&primary, &icc_segment)?;
379 }
380
381 let mpf_estimate = create_mpf_header(0, 0).len();
384 let primary_with_mpf_len = primary.len() + mpf_estimate;
385
386 let mpf_header = create_mpf_header(primary_with_mpf_len, gainmap_jpeg.len());
388
389 let mpf_segment = JpegSegment {
391 marker: 0xE2,
392 data: mpf_header[4..].to_vec(),
393 offset: 0,
394 };
395 let primary_final = insert_segment_after_soi(&primary, &mpf_segment)?;
396
397 let mut result = primary_final;
399 result.extend_from_slice(gainmap_jpeg);
400
401 Ok(result)
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_encoder_creation() {
411 let encoder = Encoder::new();
412 assert_eq!(encoder.base_quality, 90);
413 assert_eq!(encoder.gainmap_quality, 85);
414 assert_eq!(encoder.gainmap_scale, 4);
415 }
416
417 #[test]
418 fn test_encoder_builder() {
419 let mut encoder = Encoder::new();
420 encoder
421 .set_quality(95, 90)
422 .set_gainmap_scale(2)
423 .set_target_display_peak(4000.0);
424
425 assert_eq!(encoder.base_quality, 95);
426 assert_eq!(encoder.gainmap_quality, 90);
427 assert_eq!(encoder.gainmap_scale, 2);
428 assert_eq!(encoder.target_display_peak, 4000.0);
429 }
430
431 #[test]
432 fn test_encode_requires_hdr() {
433 let encoder = Encoder::new();
434 let result = encoder.encode();
435 assert!(result.is_err());
436 }
437
438 #[test]
439 fn test_existing_gainmap_methods() {
440 let mut encoder = Encoder::new();
441
442 assert!(!encoder.has_existing_gainmap());
444
445 let gainmap = GainMap::new(100, 100).unwrap();
447 let metadata = GainMapMetadata::new();
448 encoder.set_existing_gainmap(gainmap, metadata);
449 assert!(encoder.has_existing_gainmap());
450
451 encoder.clear_existing_gainmap();
453 assert!(!encoder.has_existing_gainmap());
454 }
455}