ultrajpeg 0.5.0-rc8

JPEG encoder/decoder with mozjpeg support and Ultra HDR Image Format v1.1 support.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
use crate::{Error, Result, reconstruct::reconstruct_hdr_image};
use ultrahdr_core::{
    ColorGamut, ColorTransfer, GainMap, GainMapMetadata, RawImage, gainmap::HdrOutputFormat,
};

/// Chroma subsampling modes exposed by the public API.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ChromaSubsampling {
    /// 4:2:0 chroma subsampling.
    #[default]
    Yuv420,
    /// 4:2:2 chroma subsampling.
    Yuv422,
    /// 4:4:4 chroma subsampling.
    Yuv444,
    /// 4:4:0 chroma subsampling.
    Yuv440,
}

/// JPEG compression effort.
///
/// This setting is orthogonal to whether the JPEG is encoded as sequential or
/// progressive.
///
/// `Smallest` requests the most size-oriented encoder configuration available
/// for the chosen scan mode.
///
/// With the current mozjpeg-based backend, the additional size-oriented scan
/// optimization only affects progressive output. Sequential output still
/// accepts `Smallest` for API consistency, but currently uses the same
/// effective backend settings as `Balanced`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CompressionEffort {
    /// Balanced size and encode time.
    #[default]
    Balanced,
    /// Favor smaller output over encode time.
    Smallest,
}

/// An xy chromaticity coordinate.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Chromaticity {
    /// Horizontal chromaticity coordinate.
    pub x: f32,
    /// Vertical chromaticity coordinate.
    pub y: f32,
}

/// Structured gamut information derived from explicit signaling or an ICC
/// profile.
///
/// This type always carries explicit chromaticity coordinates when gamut data
/// could be recovered.
///
/// [`GamutInfo::standard`] is only a convenience classification:
///
/// - `Some(...)` means the recovered primaries and white point match one of the
///   crate's named RGB standards within tolerance
/// - `None` means gamut coordinates were recovered successfully, but they do
///   not match one of the crate's named RGB standards
#[derive(Debug, Clone, PartialEq)]
pub struct GamutInfo {
    /// Matching named gamut standard, if the primaries and white point match
    /// one of the crate's known RGB standards within tolerance.
    pub standard: Option<ColorGamut>,
    /// Red primary xy chromaticity.
    pub red: Chromaticity,
    /// Green primary xy chromaticity.
    pub green: Chromaticity,
    /// Blue primary xy chromaticity.
    pub blue: Chromaticity,
    /// White-point xy chromaticity.
    pub white: Chromaticity,
}

/// Color-related metadata attached to the primary JPEG image.
///
/// This struct is used on both encode and decode paths.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ColorMetadata {
    /// ICC profile bytes embedded in the primary JPEG, if present.
    ///
    /// On encode, setting this field requests that the given profile be written
    /// into the primary JPEG.
    pub icc_profile: Option<Vec<u8>>,
    /// Explicit primary-image named gamut tracked by the crate alongside the
    /// JPEG.
    ///
    /// This field is a convenience classification, not the authoritative gamut
    /// model for the stable API.
    pub gamut: Option<ColorGamut>,
    /// Structured gamut information recovered from explicit signaling or the
    /// embedded ICC profile.
    ///
    /// This is the authoritative stable gamut representation.
    ///
    /// The stable API distinguishes:
    ///
    /// - `None` when no trustworthy gamut data could be recovered
    /// - `Some(GamutInfo { standard: None, .. })` when gamut coordinates were
    ///   recovered but do not match a named standard
    /// - `Some(GamutInfo { standard: Some(...), .. })` when gamut coordinates
    ///   were recovered and also match a named standard
    pub gamut_info: Option<GamutInfo>,
    /// Explicit primary-image transfer tracked by the crate alongside the JPEG.
    pub transfer: Option<ColorTransfer>,
}

/// Primary-JPEG metadata handled by the crate.
///
/// This type covers metadata physically attached to the primary JPEG itself.
/// Ultra HDR gain-map metadata is represented separately by
/// [`UltraHdrMetadata`] on decode and by [`GainMapBundle`] on encode.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PrimaryMetadata {
    /// Primary-image color signaling and ICC payload.
    pub color: ColorMetadata,
    /// EXIF payload to embed in or extract from the primary JPEG.
    pub exif: Option<Vec<u8>>,
}

/// Location from which Ultra HDR metadata was resolved.
///
/// For spec-shaped files this is usually:
///
/// - container or directory metadata on the primary JPEG
/// - gain-map metadata on the secondary JPEG
///
/// For malformed but recoverable files, `ultrajpeg` may recover effective
/// metadata from whichever location remains valid.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetadataLocation {
    /// Metadata came from the primary JPEG.
    Primary,
    /// Metadata came from the embedded gain-map JPEG.
    GainMap,
}

/// Representation from which effective gain-map metadata was parsed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GainMapMetadataSource {
    /// Effective metadata was parsed from ISO 21496-1.
    Iso21496_1,
    /// Effective metadata was parsed from Ultra HDR XMP.
    Xmp,
}

/// Structured effective Ultra HDR metadata resolved by the crate.
///
/// This struct exposes both the effective raw payloads that the crate used and
/// where they came from.
///
/// When both ISO 21496-1 and XMP are present and valid, the crate prefers
/// ISO 21496-1 for [`UltraHdrMetadata::gain_map_metadata`].
#[derive(Debug, Clone, Default)]
pub struct UltraHdrMetadata {
    /// Effective XMP payload used for Ultra HDR metadata parsing.
    ///
    /// For spec-shaped files this may come from the gain-map JPEG rather than
    /// from the primary JPEG's container XMP.
    pub xmp: Option<String>,
    /// Location from which [`UltraHdrMetadata::xmp`] was resolved.
    pub xmp_location: Option<MetadataLocation>,
    /// Effective ISO 21496-1 payload used for Ultra HDR metadata parsing.
    ///
    /// The primary JPEG may also carry a four-byte version-only ISO APP2
    /// block. That structural block is not treated as effective gain-map
    /// metadata here.
    pub iso_21496_1: Option<Vec<u8>>,
    /// Location from which [`UltraHdrMetadata::iso_21496_1`] was resolved.
    pub iso_21496_1_location: Option<MetadataLocation>,
    /// Parsed gain-map metadata after the crate's source-selection rules.
    pub gain_map_metadata: Option<GainMapMetadata>,
    /// Representation from which [`UltraHdrMetadata::gain_map_metadata`] was
    /// parsed.
    pub gain_map_metadata_source: Option<GainMapMetadataSource>,
}

/// Decoded gain-map payload and associated metadata.
#[derive(Debug, Clone)]
pub struct DecodedGainMap {
    /// Decoded secondary JPEG pixels.
    pub image: RawImage,
    /// Gain-map representation derived from [`DecodedGainMap::image`].
    pub gain_map: GainMap,
    /// Effective parsed gain-map metadata used for HDR reconstruction, if it
    /// could be resolved.
    ///
    /// This is the effective metadata selected by the crate's decode-time
    /// precedence and recovery rules, not necessarily a payload parsed only
    /// from the secondary JPEG itself.
    ///
    /// [`DecodedImage::reconstruct_hdr`](crate::DecodedImage::reconstruct_hdr)
    /// validates the effective metadata before use. If a caller mutates this
    /// value into a non-finite or structurally invalid state, reconstruction
    /// returns [`crate::Error::InvalidInput`].
    pub metadata: Option<GainMapMetadata>,
    /// Raw gain-map JPEG bytes, retained only when requested via
    /// [`DecodeOptions::retain_gain_map_jpeg`].
    pub jpeg_bytes: Option<Vec<u8>>,
}

/// Fully decoded JPEG/UltraHDR image.
#[derive(Debug, Clone)]
pub struct DecodedImage {
    /// Decoded primary-image pixels.
    ///
    /// When parsed primary-image color metadata is available, `ultrajpeg`
    /// applies the resolved gamut and transfer to this image value rather than
    /// leaving the decoder defaults in place.
    pub image: RawImage,
    /// Raw primary JPEG bytes, retained only when requested via
    /// [`DecodeOptions::retain_primary_jpeg`].
    pub primary_jpeg: Option<Vec<u8>>,
    /// Primary-JPEG metadata exposed by the crate.
    pub primary_metadata: PrimaryMetadata,
    /// Effective Ultra HDR metadata, if the image is gain-map based or
    /// recoverable as such.
    pub ultra_hdr: Option<UltraHdrMetadata>,
    /// Decoded gain-map JPEG payload, if present and enabled by
    /// [`DecodeOptions::decode_gain_map`].
    pub gain_map: Option<DecodedGainMap>,
}

/// Metadata-only JPEG or Ultra HDR inspection result.
///
/// This type never contains decoded pixel buffers.
#[derive(Debug, Clone)]
pub struct Inspection {
    /// Length in bytes of the primary JPEG codestream.
    pub primary_jpeg_len: usize,
    /// Length in bytes of the embedded gain-map JPEG codestream, if present.
    pub gain_map_jpeg_len: Option<usize>,
    /// Primary-JPEG metadata exposed by the crate.
    pub primary_metadata: PrimaryMetadata,
    /// Effective Ultra HDR metadata, if present.
    pub ultra_hdr: Option<UltraHdrMetadata>,
}

/// Parsed `hdrgm:*` XMP payload.
///
/// This type exposes the gain-map metadata carried by a raw XMP payload and
/// the optional bundled gain-map JPEG length signaled by the container
/// directory.
///
/// This is a raw-payload view, not the crate's effective decode-time metadata
/// view.
#[derive(Debug, Clone)]
pub struct ParsedGainMapXmp {
    /// Parsed gain-map metadata.
    pub metadata: GainMapMetadata,
    /// Gain-map JPEG length recovered from container-directory XMP, if present.
    pub gain_map_jpeg_len: Option<usize>,
}

/// Structural classification of a JPEG container.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContainerKind {
    /// A single JPEG codestream with no additional embedded JPEG payloads.
    Jpeg,
    /// An MPF-bundled multi-image JPEG.
    Mpf,
    /// Multiple concatenated JPEG codestreams were found, but no MPF directory
    /// could be parsed.
    ConcatenatedJpegs,
}

/// Byte range of one embedded JPEG codestream inside an input buffer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CodestreamLayout {
    /// Byte offset of the codestream start in the original input.
    pub offset: usize,
    /// Byte length of the codestream.
    ///
    /// Together with [`CodestreamLayout::offset`], this forms a sliceable
    /// byte range into the original input buffer.
    pub len: usize,
}

/// Structural layout of a JPEG or multi-image JPEG container.
///
/// This type exposes codestream boundaries and the indices that `ultrajpeg`
/// treats as the primary and gain-map JPEG payloads.
///
/// The layout is structural only. It does not imply that all embedded
/// codestreams are semantically valid Ultra HDR payloads.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerLayout {
    /// Structural classification of the input container.
    pub kind: ContainerKind,
    /// Embedded JPEG codestreams in container order.
    pub codestreams: Vec<CodestreamLayout>,
    /// Index of the primary JPEG codestream in [`ContainerLayout::codestreams`].
    pub primary_index: usize,
    /// Index of the gain-map JPEG codestream in
    /// [`ContainerLayout::codestreams`], if one was identified.
    pub gain_map_index: Option<usize>,
}

/// Decode configuration.
///
/// The default configuration decodes the primary image and any gain-map JPEG,
/// but retains no raw JPEG codestream bytes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecodeOptions {
    /// Whether to decode the embedded gain-map JPEG when present.
    ///
    /// This only affects gain-map pixel decode. Ultra HDR metadata inspection
    /// and recovery still run.
    pub decode_gain_map: bool,
    /// Whether to retain the primary JPEG codestream in
    /// [`DecodedImage::primary_jpeg`].
    pub retain_primary_jpeg: bool,
    /// Whether to retain the gain-map JPEG codestream in
    /// [`DecodedGainMap::jpeg_bytes`].
    ///
    /// This takes effect only when [`DecodeOptions::decode_gain_map`] is `true`
    /// and a gain-map JPEG was decoded successfully.
    pub retain_gain_map_jpeg: bool,
}

impl Default for DecodeOptions {
    fn default() -> Self {
        Self {
            decode_gain_map: true,
            retain_primary_jpeg: false,
            retain_gain_map_jpeg: false,
        }
    }
}

/// Gain-map payload and metadata to bundle into the final output.
#[derive(Debug, Clone)]
pub struct GainMapBundle {
    /// Gain-map image pixels to encode as the secondary JPEG.
    pub image: RawImage,
    /// Gain-map metadata to serialize into the secondary gain-map JPEG's
    /// `hdrgm:*` XMP and ISO 21496-1 payloads.
    pub metadata: GainMapMetadata,
    /// JPEG quality for the secondary gain-map codestream.
    pub quality: u8,
    /// Whether to emit the secondary gain-map JPEG as progressive.
    ///
    /// This controls scan mode only. Use [`GainMapBundle::compression`] for
    /// the size-vs-time policy.
    pub progressive: bool,
    /// Compression effort for the secondary gain-map codestream.
    ///
    /// For sequential output, [`CompressionEffort::Smallest`] is currently a
    /// best-effort request that maps to the same effective backend settings as
    /// [`CompressionEffort::Balanced`].
    pub compression: CompressionEffort,
}

/// Opt-out controls for Ultra HDR metadata emission during gain-map packaging.
///
/// The default policy is to emit every supported metadata path:
///
/// - primary container/directory XMP
/// - gain-map `hdrgm:*` XMP
/// - ISO 21496-1 payloads
///
/// These flags exist so callers can selectively omit one metadata path for
/// interoperability testing or debugging while still bundling the gain-map
/// JPEG itself.
///
/// This is an advanced testing/debugging control rather than the normal
/// packaging path. Production Ultra HDR output should usually leave every flag
/// enabled.
///
/// Omitting metadata here does not remove the gain-map image. That remains
/// controlled by whether [`EncodeOptions::gain_map`] is `Some`. When every
/// flag is `false`, the secondary gain-map JPEG is still bundled, but the
/// resulting container no longer carries effective Ultra HDR metadata.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UltraHdrMetadataEmission {
    /// Whether to emit the primary JPEG's container/directory XMP.
    ///
    /// When enabled, this is the primary-side XMP that advertises the bundled
    /// gain-map codestream and declares the `hdrgm` namespace/version.
    ///
    /// Disabling this leaves the MPF-gain-map bundle intact; it only suppresses
    /// the primary-side XMP advertisement.
    pub emit_primary_container_xmp: bool,
    /// Whether to emit `hdrgm:*` XMP on the gain-map JPEG.
    ///
    /// This controls only the gain-map JPEG's metadata payload, not the
    /// presence of the gain-map JPEG codestream itself.
    pub emit_gain_map_xmp: bool,
    /// Whether to emit ISO 21496-1 APP2 payloads.
    ///
    /// When enabled, this applies to both:
    ///
    /// - the primary JPEG's four-byte version-only structural ISO block
    /// - the gain-map JPEG's canonical ISO 21496-1 metadata payload
    ///
    /// When disabled, both ISO payloads are omitted together.
    pub emit_iso_21496_1: bool,
}

/// Gain-map channel layout for computed Ultra HDR metadata.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GainMapChannels {
    /// Compute a single-channel luminance gain map.
    #[default]
    Single,
    /// Compute a multichannel RGB gain map.
    Multi,
}

/// Supported spatial scales for computed gain maps.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GainMapScale {
    /// Compute the gain map at full primary-image resolution.
    ///
    /// This maps to a scale factor of `1` and is recommended for best
    /// quality.
    Full,
    /// Compute the gain map at half primary-image width and height.
    ///
    /// This maps to a scale factor of `2`, matches the default 1:2 scale used
    /// by Adaptive HEIC images, and is often a good compromise between quality
    /// and size.
    #[default]
    Default,
    /// Compute the gain map at quarter primary-image width and height.
    ///
    /// This maps to a scale factor of `4`, aligns with Android Ultra HDR's
    /// most aggressive recommendation, and may noticeably reduce quality.
    Smallest,
}

/// Options for gain-map computation from HDR and SDR primary images.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComputeGainMapOptions {
    /// Whether the computed gain map should be single-channel or multichannel.
    ///
    /// The default is [`GainMapChannels::Single`].
    pub channels: GainMapChannels,
    /// Spatial scale for the computed gain map.
    ///
    /// The default is [`GainMapScale::Default`].
    pub scale: GainMapScale,
}

/// Result of computing an Ultra HDR gain map from HDR and SDR images.
#[derive(Debug, Clone)]
pub struct ComputedGainMap {
    /// Gain-map image pixels ready to be JPEG-encoded and bundled.
    pub image: RawImage,
    /// Gain-map metadata ready to be serialized into the secondary gain-map
    /// JPEG's `hdrgm:*` XMP and ISO 21496-1 payloads.
    pub metadata: GainMapMetadata,
}

/// Options for deriving an SDR primary image from source pixels.
///
/// The prepared primary image is always:
///
/// - `Rgb8`
/// - tagged as [`ColorTransfer::Srgb`]
/// - tagged for the requested [`PreparePrimaryOptions::target_gamut`]
/// - brightness-floored so the default [`crate::compute_gain_map`] path stays
///   within the crate's default gain-map boost envelope
///
/// `source_peak_nits` controls how source luminance is interpreted:
///
/// - for PQ input, `None` defaults to `10000`
/// - for HLG input, `None` defaults to `1000`
/// - for linear input, `None` defaults to `1000`
/// - for sRGB input, `None` defaults to `203`
///
/// This option controls how bright the source HDR image is assumed to be. If a
/// caller knows the source mastering or editing peak more precisely, it should
/// pass an explicit value instead of relying on the transfer-based default.
#[derive(Debug, Clone, PartialEq)]
pub struct PreparePrimaryOptions {
    /// Target gamut for the SDR primary image.
    ///
    /// The current high-level helper supports [`ColorGamut::Bt709`] and
    /// [`ColorGamut::DisplayP3`].
    pub target_gamut: ColorGamut,
    /// Source peak luminance in nits.
    ///
    /// When set to `None`, `ultrajpeg` picks a transfer-specific default as
    /// described on [`PreparePrimaryOptions`].
    ///
    /// This affects the tone-mapping policy used by
    /// [`crate::prepare_sdr_primary`].
    ///
    /// When set explicitly, the value must be finite and positive.
    pub source_peak_nits: Option<f32>,
    /// Target SDR peak luminance in nits.
    ///
    /// The default `203` nits matches the crate's current SDR preparation
    /// policy for Ultra HDR workflows.
    ///
    /// The value must be finite and positive.
    pub target_peak_nits: f32,
}

/// Prepared SDR primary image and matching primary-JPEG metadata.
///
/// This type is produced by [`crate::prepare_sdr_primary`] for workflows where
/// the caller manages geometry or pixel edits before computing a gain map and
/// packaging the final JPEG.
///
/// [`PreparedPrimary::image`] and [`PreparedPrimary::metadata`] are intended to
/// be used together on subsequent encode calls.
///
/// Callers that discard the returned metadata are responsible for recreating a
/// consistent primary-image metadata policy themselves.
#[derive(Debug, Clone)]
pub struct PreparedPrimary {
    /// Prepared SDR primary image pixels.
    ///
    /// The image is always `Rgb8` with [`ColorTransfer::Srgb`].
    pub image: RawImage,
    /// Primary metadata that matches [`PreparedPrimary::image`].
    ///
    /// For Display-P3 output this includes the crate's bundled Display-P3 ICC
    /// profile. For BT.709 output, no ICC profile is attached automatically.
    pub metadata: PrimaryMetadata,
}

/// Encode configuration for the primary image and optional bundled gain map.
#[derive(Debug, Clone)]
pub struct EncodeOptions {
    /// JPEG quality for the primary image.
    pub quality: u8,
    /// Whether to emit the primary JPEG as progressive.
    ///
    /// This controls scan mode only. Use [`EncodeOptions::compression`] for
    /// the size-vs-time policy.
    pub progressive: bool,
    /// Compression effort for the primary image.
    ///
    /// For sequential output, [`CompressionEffort::Smallest`] is currently a
    /// best-effort request that maps to the same effective backend settings as
    /// [`CompressionEffort::Balanced`].
    pub compression: CompressionEffort,
    /// Chroma subsampling for the primary image.
    pub chroma_subsampling: ChromaSubsampling,
    /// Primary-JPEG metadata to embed in the output.
    pub primary_metadata: PrimaryMetadata,
    /// Opt-out controls for Ultra HDR metadata emission when a gain map is
    /// bundled.
    ///
    /// The default emits all supported metadata paths. Use this only when a
    /// caller intentionally wants an `XMP-only`, `ISO-only`, or metadata-free
    /// gain-map bundle for interoperability testing.
    pub ultra_hdr_metadata_emission: UltraHdrMetadataEmission,
    /// Optional gain-map image and metadata to bundle into an Ultra HDR
    /// container.
    pub gain_map: Option<GainMapBundle>,
}

/// High-level convenience options for direct Ultra HDR packaging from HDR and
/// SDR inputs.
#[derive(Debug, Clone)]
pub struct UltraHdrEncodeOptions {
    /// Primary JPEG encoding options.
    ///
    /// [`EncodeOptions::gain_map`] must be `None`; the gain map is computed
    /// from the `hdr_image` and `primary_image` inputs supplied to
    /// [`crate::encode_ultra_hdr`].
    pub primary: EncodeOptions,
    /// Gain-map computation policy.
    pub gain_map: ComputeGainMapOptions,
    /// JPEG quality for the computed secondary gain-map codestream.
    pub gain_map_quality: u8,
    /// Whether to emit the computed secondary gain-map JPEG as progressive.
    ///
    /// This controls scan mode only. Use
    /// [`UltraHdrEncodeOptions::gain_map_compression`] for the size-vs-time
    /// policy.
    pub gain_map_progressive: bool,
    /// Compression effort for the computed secondary gain-map JPEG.
    ///
    /// For sequential output, [`CompressionEffort::Smallest`] is currently a
    /// best-effort request that maps to the same effective backend settings as
    /// [`CompressionEffort::Balanced`].
    pub gain_map_compression: CompressionEffort,
}

impl Default for EncodeOptions {
    fn default() -> Self {
        Self {
            quality: 90,
            progressive: true,
            compression: CompressionEffort::Balanced,
            chroma_subsampling: ChromaSubsampling::Yuv420,
            primary_metadata: PrimaryMetadata::default(),
            ultra_hdr_metadata_emission: UltraHdrMetadataEmission::default(),
            gain_map: None,
        }
    }
}

impl Default for UltraHdrMetadataEmission {
    fn default() -> Self {
        Self {
            emit_primary_container_xmp: true,
            emit_gain_map_xmp: true,
            emit_iso_21496_1: true,
        }
    }
}

impl Default for UltraHdrEncodeOptions {
    fn default() -> Self {
        Self {
            primary: EncodeOptions::ultra_hdr_defaults(),
            gain_map: ComputeGainMapOptions::default(),
            gain_map_quality: 90,
            gain_map_progressive: false,
            gain_map_compression: CompressionEffort::Balanced,
        }
    }
}

impl Default for PreparePrimaryOptions {
    fn default() -> Self {
        Self {
            target_gamut: ColorGamut::Bt709,
            source_peak_nits: None,
            target_peak_nits: 203.0,
        }
    }
}

impl Default for ComputeGainMapOptions {
    fn default() -> Self {
        Self {
            channels: GainMapChannels::Single,
            scale: GainMapScale::Default,
        }
    }
}

impl ColorMetadata {
    /// Build Display-P3 primary-image metadata using the crate's bundled ICC
    /// profile.
    ///
    /// The returned metadata:
    ///
    /// - embeds the crate's built-in Display-P3 ICC profile
    /// - sets [`ColorMetadata::gamut`] to [`ColorGamut::DisplayP3`]
    /// - sets [`ColorMetadata::gamut_info`] to matching Display-P3 structural
    ///   coordinates
    /// - sets [`ColorMetadata::transfer`] to [`ColorTransfer::Srgb`]
    #[must_use]
    pub fn display_p3() -> Self {
        Self {
            icc_profile: Some(crate::icc::display_p3().to_vec()),
            gamut: Some(ColorGamut::DisplayP3),
            gamut_info: Some(GamutInfo::from_standard(ColorGamut::DisplayP3)),
            transfer: Some(ColorTransfer::Srgb),
        }
    }

    #[must_use]
    pub(crate) fn bt709_srgb() -> Self {
        Self {
            icc_profile: None,
            gamut: Some(ColorGamut::Bt709),
            gamut_info: Some(GamutInfo::from_standard(ColorGamut::Bt709)),
            transfer: Some(ColorTransfer::Srgb),
        }
    }
}

impl EncodeOptions {
    /// Build default encoder options for an Ultra HDR primary image.
    ///
    /// This keeps the crate's regular JPEG defaults and preconfigures
    /// [`EncodeOptions::primary_metadata`] with a Display-P3 color
    /// configuration built from [`ColorMetadata::display_p3()`].
    ///
    /// In other words, the returned options already include:
    ///
    /// - the built-in Display-P3 ICC profile
    /// - [`ColorGamut::DisplayP3`] as the explicit primary-image gamut
    /// - [`ColorTransfer::Srgb`] as the explicit primary-image transfer
    ///
    /// Use it as a struct update base when bundling a gain map:
    ///
    /// ```rust
    /// # use ultrahdr_core::{GainMapMetadata, PixelFormat, RawImage};
    /// # use ultrajpeg::{CompressionEffort, EncodeOptions, GainMapBundle};
    /// let gain_map = RawImage::new(8, 8, PixelFormat::Gray8)?;
    /// let options = EncodeOptions {
    ///     gain_map: Some(GainMapBundle {
    ///         image: gain_map,
    ///         metadata: GainMapMetadata::new(),
    ///         quality: 85,
    ///         progressive: false,
    ///         compression: CompressionEffort::Balanced,
    ///     }),
    ///     ..EncodeOptions::ultra_hdr_defaults()
    /// };
    /// # let _ = options;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    #[must_use]
    pub fn ultra_hdr_defaults() -> Self {
        Self {
            primary_metadata: PrimaryMetadata {
                color: ColorMetadata::display_p3(),
                exif: None,
            },
            ..Self::default()
        }
    }
}

impl PreparePrimaryOptions {
    /// Build SDR-primary preparation defaults for Ultra HDR packaging.
    ///
    /// The returned options target a Display-P3 primary image with the usual
    /// SDR reference peak of `203` nits.
    ///
    /// This is a high-level policy default, not a guarantee of source-image
    /// conformance checking.
    ///
    /// It is intended for the common case where the caller wants a prepared SDR
    /// primary that composes cleanly with the crate's default
    /// [`crate::compute_gain_map`] path.
    #[must_use]
    pub fn ultra_hdr_defaults() -> Self {
        Self {
            target_gamut: ColorGamut::DisplayP3,
            ..Self::default()
        }
    }
}

impl ComputedGainMap {
    /// Convert a computed gain map into bundling options for
    /// [`EncodeOptions::gain_map`].
    ///
    /// The gain-map JPEG is always encoded as a secondary JPEG payload inside
    /// the final container.
    ///
    /// `progressive` selects the scan mode. `compression` selects the
    /// size-vs-time policy for that scan mode.
    #[must_use]
    pub fn into_bundle(
        self,
        quality: u8,
        progressive: bool,
        compression: CompressionEffort,
    ) -> GainMapBundle {
        GainMapBundle {
            image: self.image,
            metadata: self.metadata,
            quality,
            progressive,
            compression,
        }
    }
}

impl GamutInfo {
    #[must_use]
    pub(crate) fn from_standard(standard: ColorGamut) -> Self {
        let (red, green, blue, white) = match standard {
            ColorGamut::Bt709 => (
                Chromaticity { x: 0.64, y: 0.33 },
                Chromaticity { x: 0.30, y: 0.60 },
                Chromaticity { x: 0.15, y: 0.06 },
                Chromaticity {
                    x: 0.3127,
                    y: 0.3290,
                },
            ),
            ColorGamut::DisplayP3 => (
                Chromaticity { x: 0.68, y: 0.32 },
                Chromaticity { x: 0.265, y: 0.69 },
                Chromaticity { x: 0.15, y: 0.06 },
                Chromaticity {
                    x: 0.3127,
                    y: 0.3290,
                },
            ),
            ColorGamut::Bt2100 => (
                Chromaticity { x: 0.708, y: 0.292 },
                Chromaticity { x: 0.170, y: 0.797 },
                Chromaticity { x: 0.131, y: 0.046 },
                Chromaticity {
                    x: 0.3127,
                    y: 0.3290,
                },
            ),
        };

        Self {
            standard: Some(standard),
            red,
            green,
            blue,
            white,
        }
    }
}

/// Reusable stateful encoder.
///
/// This type exists for callers that want to reuse one configuration across
/// many images without repeatedly passing the same [`EncodeOptions`] value.
#[derive(Debug, Clone)]
pub struct Encoder {
    pub(crate) options: EncodeOptions,
}

impl DecodedImage {
    pub(crate) fn reconstruct_hdr_with(
        &self,
        display_boost: f32,
        output_format: HdrOutputFormat,
    ) -> Result<RawImage> {
        let gain_map = self.gain_map.as_ref().ok_or(Error::MissingGainMap)?;
        let metadata = gain_map
            .metadata
            .as_ref()
            .or_else(|| {
                self.ultra_hdr
                    .as_ref()
                    .and_then(|metadata| metadata.gain_map_metadata.as_ref())
            })
            .ok_or(Error::MissingGainMapMetadata)?;

        reconstruct_hdr_image(
            &self.image,
            &gain_map.gain_map,
            metadata,
            display_boost,
            output_format,
        )
    }
}