Skip to main content

jxl_encoder/headers/
file_header.rs

1// Copyright (c) Imazen LLC and the JPEG XL Project Authors.
2// Algorithms and constants derived from libjxl (BSD-3-Clause).
3// Licensed under AGPL-3.0-or-later. Commercial licenses at https://www.imazen.io/pricing
4
5//! JXL file header (SizeHeader + ImageMetadata).
6
7use crate::JXL_SIGNATURE;
8use crate::bit_writer::BitWriter;
9use crate::error::Result;
10
11use super::color_encoding::ColorEncoding;
12use super::extra_channels::ExtraChannelInfo;
13
14/// Orientation of the image.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16#[repr(u8)]
17pub enum Orientation {
18    #[default]
19    Identity = 1,
20    FlipHorizontal = 2,
21    Rotate180 = 3,
22    FlipVertical = 4,
23    Transpose = 5,
24    Rotate90CW = 6,
25    AntiTranspose = 7,
26    Rotate90CCW = 8,
27}
28
29/// Bit depth specification.
30#[derive(Debug, Clone, Copy)]
31pub struct BitDepth {
32    /// True if floating point, false if integer.
33    pub float_sample: bool,
34    /// Bits per sample (for integer) or exponent bits (for float).
35    pub bits_per_sample: u32,
36    /// Exponent bits for floating point samples.
37    pub exponent_bits: u32,
38}
39
40impl Default for BitDepth {
41    fn default() -> Self {
42        Self {
43            float_sample: false,
44            bits_per_sample: 8,
45            exponent_bits: 0,
46        }
47    }
48}
49
50impl BitDepth {
51    /// Creates an 8-bit integer depth.
52    pub fn uint8() -> Self {
53        Self::default()
54    }
55
56    /// Creates a 16-bit integer depth.
57    pub fn uint16() -> Self {
58        Self {
59            float_sample: false,
60            bits_per_sample: 16,
61            exponent_bits: 0,
62        }
63    }
64
65    /// Creates a 32-bit float depth.
66    pub fn float32() -> Self {
67        Self {
68            float_sample: true,
69            bits_per_sample: 32,
70            exponent_bits: 8,
71        }
72    }
73
74    /// Creates a 16-bit half-float depth.
75    pub fn float16() -> Self {
76        Self {
77            float_sample: true,
78            bits_per_sample: 16,
79            exponent_bits: 5,
80        }
81    }
82}
83
84/// Animation parameters.
85#[derive(Debug, Clone, Default)]
86pub struct AnimationHeader {
87    /// Ticks per second numerator.
88    pub tps_numerator: u32,
89    /// Ticks per second denominator.
90    pub tps_denominator: u32,
91    /// Number of loops (0 = infinite).
92    pub num_loops: u32,
93    /// Whether frames have varying durations.
94    pub have_timecodes: bool,
95}
96
97impl AnimationHeader {
98    /// Writes the AnimationHeader to the bitstream.
99    ///
100    /// Matches libjxl's `AnimationHeader::VisitFields`:
101    /// - tps_numerator: u2S(100, 1000, Bits(10)+1, Bits(30)+1)
102    /// - tps_denominator: u2S(1, 1001, Bits(8)+1, Bits(10)+1)
103    /// - num_loops: u2S(0, Bits(3), Bits(16), Bits(32))
104    /// - have_timecodes: Bool(false)
105    pub fn write(&self, writer: &mut BitWriter) -> Result<()> {
106        // tps_numerator: u2S(100, 1000, BitsOffset(10,1), BitsOffset(30,1))
107        match self.tps_numerator {
108            100 => writer.write(2, 0)?,
109            1000 => writer.write(2, 1)?,
110            v if (1..=1024).contains(&v) => {
111                writer.write(2, 2)?;
112                writer.write(10, (v - 1) as u64)?;
113            }
114            v => {
115                debug_assert!(v >= 1, "tps_numerator must be >= 1");
116                writer.write(2, 3)?;
117                writer.write(30, (v - 1) as u64)?;
118            }
119        }
120
121        // tps_denominator: u2S(1, 1001, BitsOffset(8,1), BitsOffset(10,1))
122        match self.tps_denominator {
123            1 => writer.write(2, 0)?,
124            1001 => writer.write(2, 1)?,
125            v @ 2..=256 => {
126                writer.write(2, 2)?;
127                writer.write(8, (v - 1) as u64)?;
128            }
129            v => {
130                debug_assert!((1..=1025).contains(&v), "tps_denominator {v} out of range");
131                writer.write(2, 3)?;
132                writer.write(10, (v - 1) as u64)?;
133            }
134        }
135
136        // num_loops: u2S(0, Bits(3), Bits(16), Bits(32))
137        match self.num_loops {
138            0 => writer.write(2, 0)?,
139            v @ 1..=7 => {
140                writer.write(2, 1)?;
141                writer.write(3, v as u64)?;
142            }
143            v @ 8..=65535 => {
144                writer.write(2, 2)?;
145                writer.write(16, v as u64)?;
146            }
147            v => {
148                writer.write(2, 3)?;
149                writer.write(32, v as u64)?;
150            }
151        }
152
153        // have_timecodes: Bool(default=false)
154        writer.write_bit(self.have_timecodes)?;
155
156        Ok(())
157    }
158}
159
160/// Image metadata that appears once per file.
161#[derive(Debug, Clone)]
162pub struct ImageMetadata {
163    /// Bit depth configuration.
164    pub bit_depth: BitDepth,
165    /// Color encoding (color space, transfer function, etc.).
166    pub color_encoding: ColorEncoding,
167    /// Extra channels (alpha, depth, etc.).
168    pub extra_channels: Vec<ExtraChannelInfo>,
169    /// Image orientation.
170    pub orientation: Orientation,
171    /// Animation parameters (None if not animated).
172    pub animation: Option<AnimationHeader>,
173    /// Intensity target for HDR in nits.
174    pub intensity_target: f32,
175    /// Minimum nits for tone mapping.
176    pub min_nits: f32,
177    /// Whether intrinsic size differs from coded size.
178    pub have_intrinsic_size: bool,
179    /// Intrinsic width (if have_intrinsic_size).
180    pub intrinsic_width: u32,
181    /// Intrinsic height (if have_intrinsic_size).
182    pub intrinsic_height: u32,
183    /// Whether image uses XYB color encoding (true for lossy, false for lossless).
184    pub xyb_encoded: bool,
185}
186
187impl Default for ImageMetadata {
188    fn default() -> Self {
189        Self {
190            bit_depth: BitDepth::default(),
191            color_encoding: ColorEncoding::default(),
192            extra_channels: Vec::new(),
193            orientation: Orientation::default(),
194            animation: None,
195            intensity_target: 255.0,
196            min_nits: 0.0,
197            have_intrinsic_size: false,
198            intrinsic_width: 0,
199            intrinsic_height: 0,
200            xyb_encoded: false, // Default to lossless (non-XYB)
201        }
202    }
203}
204
205/// Complete JXL file header.
206#[derive(Debug, Clone)]
207pub struct FileHeader {
208    /// Image width in pixels.
209    pub width: u32,
210    /// Image height in pixels.
211    pub height: u32,
212    /// Image metadata.
213    pub metadata: ImageMetadata,
214}
215
216impl FileHeader {
217    /// Creates a new file header for an RGB image.
218    pub fn new_rgb(width: u32, height: u32) -> Self {
219        Self {
220            width,
221            height,
222            metadata: ImageMetadata::default(),
223        }
224    }
225
226    /// Creates a new file header for an RGBA image.
227    pub fn new_rgba(width: u32, height: u32) -> Self {
228        let mut header = Self::new_rgb(width, height);
229        header
230            .metadata
231            .extra_channels
232            .push(ExtraChannelInfo::alpha());
233        header
234    }
235
236    /// Creates a new file header for a grayscale image.
237    pub fn new_gray(width: u32, height: u32) -> Self {
238        let mut header = Self::new_rgb(width, height);
239        header.metadata.color_encoding = ColorEncoding::gray();
240        header
241    }
242
243    /// Creates a new file header for a lossy RGB image (VarDCT/XYB encoded).
244    pub fn new_rgb_lossy(width: u32, height: u32) -> Self {
245        let mut header = Self::new_rgb(width, height);
246        header.metadata.xyb_encoded = true;
247        header
248    }
249
250    /// Writes the JXL signature.
251    pub fn write_signature(writer: &mut BitWriter) -> Result<()> {
252        writer.write_u8(JXL_SIGNATURE[0])?;
253        writer.write_u8(JXL_SIGNATURE[1])?;
254        Ok(())
255    }
256
257    /// Writes the size header.
258    ///
259    /// JXL Size format:
260    /// - small: Bool (1 bit) - true if both dimensions are multiples of 8 and <= 256
261    /// - If small:
262    ///   - ysize_div8: Bits(5) + 1 (height/8, range 1-32)
263    ///   - ratio: Bits(3)
264    ///   - If ratio == 0: xsize_div8: Bits(5) + 1 (width/8, range 1-32)
265    /// - If !small:
266    ///   - ysize: 1 + u2S(Bits(9), Bits(13), Bits(18), Bits(30))
267    ///   - ratio: Bits(3)
268    ///   - If ratio == 0: xsize: 1 + u2S(Bits(9), Bits(13), Bits(18), Bits(30))
269    fn write_size_header(&self, writer: &mut BitWriter) -> Result<()> {
270        // small = true if both dimensions are multiples of 8 and fit in 5 bits (8-256)
271        let h_div8 = self.height.is_multiple_of(8) && self.height / 8 >= 1 && self.height / 8 <= 32;
272        let w_div8 = self.width.is_multiple_of(8) && self.width / 8 >= 1 && self.width / 8 <= 32;
273        let small = h_div8 && w_div8;
274
275        crate::trace::debug_eprintln!(
276            "SIZE_HDR: {}x{}, small={}, h_div8={}, w_div8={}",
277            self.width,
278            self.height,
279            small,
280            h_div8,
281            w_div8
282        );
283        writer.write_bit(small)?;
284
285        if small {
286            // ysize_div8_minus_1: Bits(5), decoder adds 1 then multiplies by 8
287            crate::trace::debug_eprintln!("SIZE_HDR: ysize_div8_minus_1 = {}", self.height / 8 - 1);
288            writer.write(5, (self.height / 8 - 1) as u64)?;
289
290            let ratio = self.compute_ratio();
291            crate::trace::debug_eprintln!("SIZE_HDR: ratio = {}", ratio);
292            writer.write(3, ratio as u64)?;
293
294            if ratio == 0 {
295                // xsize_div8_minus_1: Bits(5), decoder adds 1 then multiplies by 8
296                crate::trace::debug_eprintln!(
297                    "SIZE_HDR: xsize_div8_minus_1 = {}",
298                    self.width / 8 - 1
299                );
300                writer.write(5, (self.width / 8 - 1) as u64)?;
301            }
302        } else {
303            // ysize: 1 + u2S(Bits(9), Bits(13), Bits(18), Bits(30))
304            // Write height - 1 using u2S encoding
305            self.write_size_u2s(writer, self.height - 1)?;
306
307            let ratio = self.compute_ratio();
308            writer.write(3, ratio as u64)?;
309
310            if ratio == 0 {
311                // xsize: 1 + u2S(Bits(9), Bits(13), Bits(18), Bits(30))
312                self.write_size_u2s(writer, self.width - 1)?;
313            }
314        }
315
316        Ok(())
317    }
318
319    /// Writes a size value using u2S(Bits(9), Bits(13), Bits(18), Bits(30)) encoding.
320    /// The decoder adds 1 to the result, so we write value directly (not value-1).
321    fn write_size_u2s(&self, writer: &mut BitWriter, value: u32) -> Result<()> {
322        if value < (1 << 9) {
323            writer.write(2, 0)?; // selector 0
324            writer.write(9, value as u64)?;
325        } else if value < (1 << 13) {
326            writer.write(2, 1)?; // selector 1
327            writer.write(13, value as u64)?;
328        } else if value < (1 << 18) {
329            writer.write(2, 2)?; // selector 2
330            writer.write(18, value as u64)?;
331        } else {
332            writer.write(2, 3)?; // selector 3
333            writer.write(30, value as u64)?;
334        }
335        Ok(())
336    }
337
338    /// Computes the aspect ratio selector (0 = explicit width).
339    fn compute_ratio(&self) -> u8 {
340        // Ratio selectors: 1=1:1, 2=12:10, 3=4:3, 4=3:2, 5=16:9, 6=5:4, 7=2:1
341        if self.width == self.height {
342            1 // 1:1
343        } else if self.width * 10 == self.height * 12 {
344            2 // 12:10
345        } else if self.width * 3 == self.height * 4 {
346            3 // 4:3
347        } else if self.width * 2 == self.height * 3 {
348            4 // 3:2
349        } else if self.width * 9 == self.height * 16 {
350            5 // 16:9
351        } else if self.width * 4 == self.height * 5 {
352            6 // 5:4
353        } else if self.width == self.height * 2 {
354            7 // 2:1
355        } else {
356            0 // Explicit
357        }
358    }
359
360    /// Writes the complete file header (signature + size + metadata + transform_data).
361    pub fn write(&self, writer: &mut BitWriter) -> Result<()> {
362        crate::trace::debug_eprintln!("FHDR [bit {}]: Starting file header", writer.bits_written());
363        Self::write_signature(writer)?;
364        crate::trace::debug_eprintln!("FHDR [bit {}]: After signature", writer.bits_written());
365        self.write_size_header(writer)?;
366        crate::trace::debug_eprintln!("FHDR [bit {}]: After size header", writer.bits_written());
367        self.write_image_metadata(writer)?;
368        crate::trace::debug_eprintln!("FHDR [bit {}]: After metadata", writer.bits_written());
369        // CustomTransformData - written after ImageMetadata
370        // For simple images, all_default = true (just 1 bit)
371        self.write_transform_data(writer)?;
372        crate::trace::debug_eprintln!("FHDR [bit {}]: After transform_data", writer.bits_written());
373        Ok(())
374    }
375
376    /// Writes the CustomTransformData bundle.
377    /// For basic encoding (no custom transform settings), this is just all_default=true (1 bit).
378    fn write_transform_data(&self, writer: &mut BitWriter) -> Result<()> {
379        // CustomTransformData.all_default = true
380        // This is the default case - no custom upsampling weights or opsin matrix
381        crate::trace::debug_eprintln!(
382            "XFRM [bit {}]: transform_data.all_default = true",
383            writer.bits_written()
384        );
385        writer.write_bit(true)?;
386        Ok(())
387    }
388
389    /// Writes the image metadata.
390    fn write_image_metadata(&self, writer: &mut BitWriter) -> Result<()> {
391        let meta = &self.metadata;
392
393        // all_default flag
394        let all_default = self.is_metadata_default();
395        crate::trace::debug_eprintln!(
396            "META [bit {}]: all_default = {}",
397            writer.bits_written(),
398            all_default
399        );
400        writer.write_bit(all_default)?;
401
402        if all_default {
403            return Ok(());
404        }
405
406        // extra_fields flag
407        let extra_fields = meta.animation.is_some()
408            || meta.orientation != Orientation::Identity
409            || meta.have_intrinsic_size
410            || meta.intensity_target != 255.0
411            || meta.min_nits != 0.0;
412        crate::trace::debug_eprintln!(
413            "META [bit {}]: extra_fields = {}",
414            writer.bits_written(),
415            extra_fields
416        );
417        writer.write_bit(extra_fields)?;
418
419        if extra_fields {
420            // orientation - 1 (3 bits)
421            writer.write(3, (meta.orientation as u8 - 1) as u64)?;
422
423            // have_intrinsic_size
424            writer.write_bit(meta.have_intrinsic_size)?;
425            if meta.have_intrinsic_size {
426                // Intrinsic size uses same u2S encoding as Size
427                self.write_size_u2s(writer, meta.intrinsic_width - 1)?;
428                self.write_size_u2s(writer, meta.intrinsic_height - 1)?;
429            }
430
431            // have_preview (not implemented)
432            writer.write_bit(false)?;
433
434            // have_animation
435            writer.write_bit(meta.animation.is_some())?;
436            if let Some(ref anim) = meta.animation {
437                anim.write(writer)?;
438            }
439        }
440
441        // bit_depth
442        crate::trace::debug_eprintln!("META [bit {}]: Writing bit_depth", writer.bits_written());
443        meta.bit_depth.write(writer)?;
444        crate::trace::debug_eprintln!("META [bit {}]: After bit_depth", writer.bits_written());
445
446        // modular_16_bit_buffer_sufficient
447        // Default is true for bit depths <= 12
448        let mod16_sufficient = meta.bit_depth.bits_per_sample <= 12;
449        crate::trace::debug_eprintln!(
450            "META [bit {}]: modular_16_bit_buffer_sufficient = {}",
451            writer.bits_written(),
452            mod16_sufficient
453        );
454        writer.write_bit(mod16_sufficient)?;
455
456        // num_extra_channels
457        let num_extra = meta.extra_channels.len() as u32;
458        crate::trace::debug_eprintln!(
459            "META [bit {}]: num_extra_channels = {}",
460            writer.bits_written(),
461            num_extra
462        );
463        writer.write_u32_coder(num_extra, 0, 1, 2, 1, 12)?;
464
465        for ec in &meta.extra_channels {
466            ec.write(writer)?;
467        }
468
469        // xyb_encoded (true for lossy, false for lossless)
470        crate::trace::debug_eprintln!(
471            "META [bit {}]: xyb_encoded = {}",
472            writer.bits_written(),
473            meta.xyb_encoded
474        );
475        writer.write_bit(meta.xyb_encoded)?;
476
477        // color_encoding
478        crate::trace::debug_eprintln!(
479            "META [bit {}]: Writing color_encoding",
480            writer.bits_written()
481        );
482        meta.color_encoding.write(writer)?;
483        crate::trace::debug_eprintln!("META [bit {}]: After color_encoding", writer.bits_written());
484
485        // tone_mapping - only if extra_fields
486        if extra_fields {
487            let tone_all_default = meta.intensity_target == 255.0 && meta.min_nits == 0.0;
488            writer.write_bit(tone_all_default)?;
489            if !tone_all_default {
490                crate::f16::write_f16(meta.intensity_target, writer)?;
491                crate::f16::write_f16(meta.min_nits, writer)?;
492                writer.write_bit(false)?; // relative_to_max_display
493                crate::f16::write_f16(0.0, writer)?; // linear_below
494            }
495        }
496
497        // extensions (u64 selector, 0 = no extensions)
498        // u64 encoding: 2-bit selector, 0 means value 0
499        writer.write(2, 0)?;
500
501        Ok(())
502    }
503
504    /// Checks if all metadata is default.
505    /// Per JXL spec, all_default=true implies xyb_encoded=false (lossless mode).
506    fn is_metadata_default(&self) -> bool {
507        // For now, always return false to write explicit metadata.
508        // This ensures compatibility while we investigate the all_default parsing issue.
509        // TODO: Enable all_default optimization once we confirm decoder compatibility.
510        false
511    }
512}
513
514impl BitDepth {
515    /// Writes the bit depth to the bitstream.
516    pub fn write(&self, writer: &mut BitWriter) -> Result<()> {
517        writer.write_bit(self.float_sample)?;
518        if self.float_sample {
519            // bits_per_sample for float: u2S(32, 16, 24, 1 + Bits(6))
520            writer.write_u32_coder(self.bits_per_sample, 32, 16, 24, 1, 6)?;
521            // exponent_bits: 1 + Bits(4)
522            writer.write(4, (self.exponent_bits - 1) as u64)?;
523        } else {
524            // bits_per_sample for int: u2S(8, 10, 12, 1 + Bits(6))
525            writer.write_u32_coder(self.bits_per_sample, 8, 10, 12, 1, 6)?;
526        }
527        Ok(())
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534
535    #[test]
536    fn test_signature() {
537        let mut writer = BitWriter::new();
538        FileHeader::write_signature(&mut writer).unwrap();
539        let bytes = writer.finish();
540        assert_eq!(bytes, vec![0xFF, 0x0A]);
541    }
542
543    #[test]
544    fn test_simple_header() {
545        let header = FileHeader::new_rgb(256, 256);
546        let mut writer = BitWriter::new();
547        header.write(&mut writer).unwrap();
548
549        let bytes = writer.finish_with_padding();
550        // Should start with JXL signature
551        assert_eq!(&bytes[0..2], &[0xFF, 0x0A]);
552    }
553}