Skip to main content

oximedia_codec/jpegxl/
encoder.rs

1//! JPEG-XL encoder implementation.
2//!
3//! Encodes raw pixel data into JPEG-XL codestreams. Currently supports
4//! lossless Modular mode for 8-bit and 16-bit images in grayscale, RGB,
5//! and RGBA color spaces.
6
7use super::bitreader::BitWriter;
8use super::modular::{ModularEncoder, ModularTransform};
9use super::types::{JxlAnimation, JxlColorSpace, JxlConfig, JxlHeader, JXL_CODESTREAM_SIGNATURE};
10use crate::container::isobmff::make_box;
11use crate::error::{CodecError, CodecResult};
12
13/// JPEG-XL encoder.
14///
15/// Encodes images to JPEG-XL format. Currently optimized for lossless encoding
16/// using the Modular sub-codec with Reversible Color Transform and adaptive
17/// prediction.
18pub struct JxlEncoder {
19    config: JxlConfig,
20}
21
22impl JxlEncoder {
23    /// Create a new encoder with the given configuration.
24    pub fn new(config: JxlConfig) -> Self {
25        Self { config }
26    }
27
28    /// Create a lossless encoder with default effort.
29    pub fn lossless() -> Self {
30        Self {
31            config: JxlConfig::new_lossless(),
32        }
33    }
34
35    /// Create a lossless encoder with specified effort level.
36    pub fn lossless_with_effort(effort: u8) -> Self {
37        Self {
38            config: JxlConfig::new_lossless().with_effort(effort),
39        }
40    }
41
42    /// Encode an image to JPEG-XL format.
43    ///
44    /// # Arguments
45    ///
46    /// * `data` - Interleaved pixel data (e.g., RGBRGBRGB... for 8-bit RGB)
47    /// * `width` - Image width in pixels
48    /// * `height` - Image height in pixels
49    /// * `channels` - Number of channels (1=gray, 3=RGB, 4=RGBA)
50    /// * `bit_depth` - Bits per sample (8 or 16)
51    ///
52    /// # Errors
53    ///
54    /// Returns error if parameters are invalid or encoding fails.
55    pub fn encode(
56        &self,
57        data: &[u8],
58        width: u32,
59        height: u32,
60        channels: u8,
61        bit_depth: u8,
62    ) -> CodecResult<Vec<u8>> {
63        // Validate inputs
64        let mut header = JxlHeader::srgb(width, height, channels)?;
65        header.bits_per_sample = bit_depth;
66        let expected_size = width as usize
67            * height as usize
68            * channels as usize
69            * (if bit_depth > 8 { 2 } else { 1 });
70
71        if data.len() < expected_size {
72            return Err(CodecError::BufferTooSmall {
73                needed: expected_size,
74                have: data.len(),
75            });
76        }
77
78        self.config.validate()?;
79
80        // Convert interleaved input to separate channel buffers
81        let channels_data = self.deinterleave(data, width, height, channels, bit_depth)?;
82
83        // Encode using modular mode
84        let modular_data = self.encode_modular(&channels_data, width, height, &header)?;
85
86        // Build the final codestream
87        let mut writer = BitWriter::with_capacity(modular_data.len() + 32);
88
89        // Write signature
90        self.write_signature(&mut writer);
91
92        // Write size header
93        self.write_size_header(&mut writer, width, height);
94
95        // Write image metadata
96        self.write_image_metadata(&mut writer, &header);
97
98        // Align to byte boundary before modular data
99        writer.align_to_byte();
100
101        // Append modular-encoded data
102        for &byte in &modular_data {
103            writer.write_bits(byte as u32, 8);
104        }
105
106        Ok(writer.finish())
107    }
108
109    /// Write the JPEG-XL codestream signature (0xFF 0x0A).
110    pub(crate) fn write_signature(&self, writer: &mut BitWriter) {
111        writer.write_bits(JXL_CODESTREAM_SIGNATURE[0] as u32, 8);
112        writer.write_bits(JXL_CODESTREAM_SIGNATURE[1] as u32, 8);
113    }
114
115    /// Write the SizeHeader.
116    ///
117    /// Uses the small encoding when possible (dimensions divisible by 8
118    /// and <= 256), otherwise uses the full U32 encoding.
119    pub(crate) fn write_size_header(&self, writer: &mut BitWriter, width: u32, height: u32) {
120        let can_use_small = width > 0
121            && height > 0
122            && width % 8 == 0
123            && height % 8 == 0
124            && width / 8 <= 32
125            && height / 8 <= 32;
126
127        if can_use_small {
128            writer.write_bool(true); // small = true
129            let height_div8 = height / 8;
130            let width_div8 = width / 8;
131            writer.write_bits(height_div8 - 1, 5);
132            if width_div8 == height_div8 {
133                writer.write_bits(0, 5); // 0 means same as height
134            } else {
135                writer.write_bits(width_div8, 5);
136            }
137        } else {
138            writer.write_bool(false); // small = false
139            self.write_size_u32(writer, height);
140            self.write_size_u32(writer, width);
141        }
142    }
143
144    /// Write a size value using JPEG-XL's SizeHeader U32 encoding.
145    pub(crate) fn write_size_u32(&self, writer: &mut BitWriter, value: u32) {
146        if value == 1 {
147            writer.write_bits(0, 2); // selector 0
148        } else if value <= 512 {
149            writer.write_bits(1, 2); // selector 1
150            writer.write_bits(value - 1, 9);
151        } else if value <= 8192 {
152            writer.write_bits(2, 2); // selector 2
153            writer.write_bits(value - 1, 13);
154        } else {
155            writer.write_bits(3, 2); // selector 3
156            writer.write_bits(value - 1, 18);
157        }
158    }
159
160    /// Write image metadata.
161    pub(crate) fn write_image_metadata(&self, writer: &mut BitWriter, header: &JxlHeader) {
162        // Check if we can use all_default (8-bit sRGB, no alpha, orientation 1, no animation)
163        let is_default = header.bits_per_sample == 8
164            && !header.is_float
165            && header.color_space == JxlColorSpace::Srgb
166            && !header.has_alpha
167            && header.orientation == 1
168            && header.num_channels == 3
169            && header.animation.is_none();
170
171        if is_default {
172            writer.write_bool(true); // all_default = true
173            return;
174        }
175
176        writer.write_bool(false); // all_default = false
177
178        // Extra fields (orientation)
179        let has_extra = header.orientation != 1;
180        writer.write_bool(has_extra);
181        if has_extra {
182            writer.write_bits((header.orientation - 1) as u32, 3);
183        }
184
185        // Bit depth
186        writer.write_bool(header.is_float); // float flag
187        if !header.is_float {
188            match header.bits_per_sample {
189                8 => writer.write_bits(0, 2),
190                10 => writer.write_bits(1, 2),
191                12 => writer.write_bits(2, 2),
192                other => {
193                    writer.write_bits(3, 2); // custom
194                    writer.write_bits((other - 1) as u32, 6);
195                }
196            }
197        }
198
199        // Color space
200        let cs_selector = match header.color_space {
201            JxlColorSpace::Srgb => 0u32,
202            JxlColorSpace::LinearSrgb => 1,
203            JxlColorSpace::Gray => 2,
204            JxlColorSpace::Xyb => 3,
205        };
206        writer.write_bits(cs_selector, 2);
207
208        // Alpha
209        writer.write_bool(header.has_alpha);
210
211        // Animation header
212        writer.write_bool(header.animation.is_some());
213        if let Some(ref anim) = header.animation {
214            Self::write_animation_header(writer, anim);
215        }
216    }
217
218    /// Write the animation header fields.
219    pub(crate) fn write_animation_header(writer: &mut BitWriter, anim: &JxlAnimation) {
220        writer.write_bits(anim.tps_numerator, 32);
221        writer.write_bits(anim.tps_denominator, 32);
222        writer.write_bits(anim.num_loops, 32);
223        writer.write_bool(anim.have_timecodes);
224    }
225
226    /// Write a per-frame header for animated codestreams.
227    pub(crate) fn write_frame_header(writer: &mut BitWriter, duration_ticks: u32, is_last: bool) {
228        writer.write_bits(duration_ticks, 32);
229        writer.write_bool(is_last);
230    }
231
232    /// Encode image channels using the Modular sub-codec.
233    pub(crate) fn encode_modular(
234        &self,
235        channels: &[Vec<i32>],
236        width: u32,
237        height: u32,
238        header: &JxlHeader,
239    ) -> CodecResult<Vec<u8>> {
240        let mut encoder = ModularEncoder::new().with_effort(self.config.effort);
241
242        // Add RCT for RGB/RGBA images (3+ color channels)
243        if header.color_channels() >= 3 {
244            encoder.add_transform(ModularTransform::Rct {
245                begin_channel: 0,
246                rct_type: 0,
247            });
248        }
249
250        encoder.encode_image(channels, width, height, header.bits_per_sample)
251    }
252
253    /// Convert interleaved pixel data to separate channel buffers.
254    pub(crate) fn deinterleave(
255        &self,
256        data: &[u8],
257        width: u32,
258        height: u32,
259        channels: u8,
260        bit_depth: u8,
261    ) -> CodecResult<Vec<Vec<i32>>> {
262        let pixel_count = width as usize * height as usize;
263        let num_channels = channels as usize;
264        let bytes_per_sample = if bit_depth > 8 { 2usize } else { 1usize };
265
266        let mut channel_data: Vec<Vec<i32>> = (0..num_channels)
267            .map(|_| Vec::with_capacity(pixel_count))
268            .collect();
269
270        for i in 0..pixel_count {
271            for ch in 0..num_channels {
272                let offset = (i * num_channels + ch) * bytes_per_sample;
273
274                let value = if bytes_per_sample == 1 {
275                    data[offset] as i32
276                } else {
277                    // 16-bit little-endian
278                    let lo = data[offset] as i32;
279                    let hi = data.get(offset + 1).copied().unwrap_or(0) as i32;
280                    lo | (hi << 8)
281                };
282
283                channel_data[ch].push(value);
284            }
285        }
286
287        Ok(channel_data)
288    }
289}
290
291impl Default for JxlEncoder {
292    fn default() -> Self {
293        Self::lossless()
294    }
295}
296
297/// Pending frame data for the animated encoder.
298struct PendingFrame {
299    /// Modular-encoded data for this frame.
300    modular_data: Vec<u8>,
301    /// Duration in ticks for this frame.
302    duration_ticks: u32,
303}
304
305/// Animated JPEG-XL encoder.
306///
307/// Builds a multi-frame JPEG-XL codestream by accumulating frames via
308/// [`add_frame`](Self::add_frame) and then producing the final codestream
309/// with [`finish`](Self::finish).
310///
311/// # Codestream Layout
312///
313/// ```text
314/// [signature (2 bytes)] [size_header] [image_metadata with animation]
315/// [frame_header_0] [align] [frame_data_0]
316/// [frame_header_1] [align] [frame_data_1]
317/// ...
318/// [frame_header_N] [align] [frame_data_N]   (is_last = true)
319/// ```
320///
321/// # Examples
322///
323/// ```ignore
324/// use oximedia_codec::jpegxl::{AnimatedJxlEncoder, JxlAnimation};
325///
326/// let anim = JxlAnimation::millisecond();
327/// let mut encoder = AnimatedJxlEncoder::new(anim, 8, 8, 3, 8)?;
328/// encoder.add_frame(&frame0_data, 100)?;  // 100ms
329/// encoder.add_frame(&frame1_data, 200)?;  // 200ms
330/// let codestream = encoder.finish()?;
331/// ```
332pub struct AnimatedJxlEncoder {
333    /// Animation header.
334    animation: JxlAnimation,
335    /// Image width (all frames must share).
336    width: u32,
337    /// Image height (all frames must share).
338    height: u32,
339    /// Number of channels (all frames must share).
340    channels: u8,
341    /// Bit depth (all frames must share).
342    bit_depth: u8,
343    /// Encoding effort.
344    effort: u8,
345    /// Accumulated frames.
346    frames: Vec<PendingFrame>,
347}
348
349impl AnimatedJxlEncoder {
350    /// Create a new animated encoder.
351    ///
352    /// # Arguments
353    ///
354    /// * `animation` - Animation timing configuration
355    /// * `width` - Frame width in pixels (shared by all frames)
356    /// * `height` - Frame height in pixels (shared by all frames)
357    /// * `channels` - Number of channels (1=gray, 3=RGB, 4=RGBA)
358    /// * `bit_depth` - Bits per sample (8 or 16)
359    ///
360    /// # Errors
361    ///
362    /// Returns error if dimensions, channels, or bit depth are invalid.
363    pub fn new(
364        animation: JxlAnimation,
365        width: u32,
366        height: u32,
367        channels: u8,
368        bit_depth: u8,
369    ) -> CodecResult<Self> {
370        // Validate via JxlHeader::srgb which checks channels and dimensions
371        let mut header = JxlHeader::srgb(width, height, channels)?;
372        header.bits_per_sample = bit_depth;
373        header.validate()?;
374
375        Ok(Self {
376            animation,
377            width,
378            height,
379            channels,
380            bit_depth,
381            effort: 7,
382            frames: Vec::new(),
383        })
384    }
385
386    /// Set encoding effort (1-9).
387    pub fn with_effort(mut self, effort: u8) -> Self {
388        self.effort = effort.clamp(1, 9);
389        self
390    }
391
392    /// Add a frame to the animation.
393    ///
394    /// # Arguments
395    ///
396    /// * `data` - Interleaved pixel data for this frame (same layout as `JxlEncoder::encode`)
397    /// * `duration_ticks` - Duration of this frame in ticks (relative to animation tick rate)
398    ///
399    /// # Errors
400    ///
401    /// Returns error if the data size is wrong or encoding fails.
402    pub fn add_frame(&mut self, data: &[u8], duration_ticks: u32) -> CodecResult<()> {
403        let bytes_per_sample: usize = if self.bit_depth > 8 { 2 } else { 1 };
404        let expected_size =
405            self.width as usize * self.height as usize * self.channels as usize * bytes_per_sample;
406
407        if data.len() < expected_size {
408            return Err(CodecError::BufferTooSmall {
409                needed: expected_size,
410                have: data.len(),
411            });
412        }
413
414        // Use a temporary single-frame JxlEncoder for the modular encoding pipeline
415        let single_encoder = JxlEncoder::lossless_with_effort(self.effort);
416        let channels_data = single_encoder.deinterleave(
417            data,
418            self.width,
419            self.height,
420            self.channels,
421            self.bit_depth,
422        )?;
423
424        let mut header = JxlHeader::srgb(self.width, self.height, self.channels)?;
425        header.bits_per_sample = self.bit_depth;
426
427        let modular_data =
428            single_encoder.encode_modular(&channels_data, self.width, self.height, &header)?;
429
430        self.frames.push(PendingFrame {
431            modular_data,
432            duration_ticks,
433        });
434
435        Ok(())
436    }
437
438    /// Number of frames added so far.
439    pub fn frame_count(&self) -> usize {
440        self.frames.len()
441    }
442
443    /// Finalize and wrap the animated codestream in an ISOBMFF container.
444    ///
445    /// Produces a file beginning with a `ftyp` box (major brand `jxl `),
446    /// followed by a `jxll` box and a single `jxlp` box containing the bare
447    /// codestream with the `is_last` flag set.
448    ///
449    /// # Errors
450    ///
451    /// Returns error if no frames have been added or encoding fails.
452    pub fn finish_isobmff(self) -> CodecResult<Vec<u8>> {
453        let codestream = self.finish()?;
454
455        // ftyp: major_brand="jxl ", minor_version=0, compatible_brands=["jxl ", "isom"]
456        let mut ftyp_payload = Vec::with_capacity(16);
457        ftyp_payload.extend_from_slice(b"jxl ");
458        ftyp_payload.extend_from_slice(&0u32.to_be_bytes());
459        ftyp_payload.extend_from_slice(b"jxl ");
460        ftyp_payload.extend_from_slice(b"isom");
461        let ftyp = make_box(*b"ftyp", &ftyp_payload);
462
463        // jxll: level 5
464        let jxll = make_box(*b"jxll", &[5u8]);
465
466        // jxlp: index/flags with is_last bit set (bit 31), followed by bare codestream.
467        let index_val: u32 = 0u32 | 0x8000_0000;
468        let mut jxlp_payload = Vec::with_capacity(4 + codestream.len());
469        jxlp_payload.extend_from_slice(&index_val.to_be_bytes());
470        jxlp_payload.extend_from_slice(&codestream);
471        let jxlp = make_box(*b"jxlp", &jxlp_payload);
472
473        let mut out = Vec::with_capacity(ftyp.len() + jxll.len() + jxlp.len());
474        out.extend_from_slice(&ftyp);
475        out.extend_from_slice(&jxll);
476        out.extend_from_slice(&jxlp);
477        Ok(out)
478    }
479
480    /// Finalize the animated codestream.
481    ///
482    /// Produces the complete multi-frame JPEG-XL bare codestream. The last
483    /// frame is automatically marked with `is_last = true`.
484    ///
485    /// # Errors
486    ///
487    /// Returns error if no frames have been added.
488    pub fn finish(self) -> CodecResult<Vec<u8>> {
489        if self.frames.is_empty() {
490            return Err(CodecError::InvalidParameter(
491                "Animated JPEG-XL requires at least one frame".into(),
492            ));
493        }
494
495        let mut header = JxlHeader::srgb(self.width, self.height, self.channels)?;
496        header.bits_per_sample = self.bit_depth;
497        header.animation = Some(self.animation);
498
499        // Estimate total capacity
500        let total_modular: usize = self.frames.iter().map(|f| f.modular_data.len()).sum();
501        let estimated_capacity = 64 + total_modular + self.frames.len() * 8;
502        let mut writer = BitWriter::with_capacity(estimated_capacity);
503
504        // Use a temporary encoder for header writing methods
505        let helper = JxlEncoder::lossless();
506
507        // Write signature
508        helper.write_signature(&mut writer);
509
510        // Write size header
511        helper.write_size_header(&mut writer, self.width, self.height);
512
513        // Write image metadata (includes animation header)
514        helper.write_image_metadata(&mut writer, &header);
515
516        // Write each frame
517        let frame_count = self.frames.len();
518        for (i, frame) in self.frames.into_iter().enumerate() {
519            let is_last = i == frame_count - 1;
520
521            // Write frame header
522            JxlEncoder::write_frame_header(&mut writer, frame.duration_ticks, is_last);
523
524            // Align to byte boundary before modular data
525            writer.align_to_byte();
526
527            // Write frame data length (so decoder knows where each frame ends)
528            let data_len = frame.modular_data.len() as u32;
529            writer.write_bits(data_len, 32);
530
531            // Write modular-encoded frame data
532            for &byte in &frame.modular_data {
533                writer.write_bits(byte as u32, 8);
534            }
535        }
536
537        Ok(writer.finish())
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::super::decoder::JxlDecoder;
544    use super::*;
545
546    #[test]
547    #[ignore]
548    fn test_lossless_roundtrip_rgb8() {
549        let width = 8u32;
550        let height = 8u32;
551        let channels = 3u8;
552        let pixel_count = (width * height) as usize;
553
554        // Generate test pattern
555        let mut data = Vec::with_capacity(pixel_count * channels as usize);
556        for i in 0..pixel_count {
557            data.push(((i * 3) % 256) as u8); // R
558            data.push(((i * 5 + 50) % 256) as u8); // G
559            data.push(((i * 7 + 100) % 256) as u8); // B
560        }
561
562        let encoder = JxlEncoder::lossless();
563        let encoded = encoder
564            .encode(&data, width, height, channels, 8)
565            .expect("encode ok");
566
567        // Verify signature
568        assert!(JxlDecoder::is_codestream(&encoded));
569
570        let decoder = JxlDecoder::new();
571        let decoded = decoder.decode(&encoded).expect("decode ok");
572
573        assert_eq!(decoded.width, width);
574        assert_eq!(decoded.height, height);
575        assert_eq!(decoded.channels, channels);
576        assert_eq!(decoded.bit_depth, 8);
577        assert_eq!(decoded.data, data, "Lossless roundtrip failed for RGB8");
578    }
579
580    #[test]
581    #[ignore]
582    fn test_lossless_roundtrip_grayscale() {
583        let width = 16u32;
584        let height = 16u32;
585        let channels = 1u8;
586        let pixel_count = (width * height) as usize;
587
588        let mut data = Vec::with_capacity(pixel_count);
589        for i in 0..pixel_count {
590            data.push((i % 256) as u8);
591        }
592
593        let encoder = JxlEncoder::lossless();
594        let encoded = encoder
595            .encode(&data, width, height, channels, 8)
596            .expect("encode ok");
597
598        let decoder = JxlDecoder::new();
599        let decoded = decoder.decode(&encoded).expect("decode ok");
600
601        assert_eq!(decoded.width, width);
602        assert_eq!(decoded.height, height);
603        assert_eq!(decoded.channels, 1);
604        assert_eq!(
605            decoded.data, data,
606            "Lossless roundtrip failed for grayscale"
607        );
608    }
609
610    #[test]
611    #[ignore]
612    fn test_lossless_roundtrip_16bit() {
613        let width = 4u32;
614        let height = 4u32;
615        let channels = 3u8;
616        let pixel_count = (width * height) as usize;
617
618        // Generate 16-bit test data (little-endian)
619        let mut data = Vec::with_capacity(pixel_count * channels as usize * 2);
620        for i in 0..pixel_count {
621            for ch in 0..channels as usize {
622                let val = ((i * 1000 + ch * 3000) % 65536) as u16;
623                data.push(val as u8); // low byte
624                data.push((val >> 8) as u8); // high byte
625            }
626        }
627
628        let encoder = JxlEncoder::lossless();
629        let encoded = encoder
630            .encode(&data, width, height, channels, 16)
631            .expect("encode ok");
632
633        let decoder = JxlDecoder::new();
634        let decoded = decoder.decode(&encoded).expect("decode ok");
635
636        assert_eq!(decoded.width, width);
637        assert_eq!(decoded.height, height);
638        assert_eq!(decoded.channels, channels);
639        assert_eq!(decoded.bit_depth, 16);
640        assert_eq!(decoded.data, data, "Lossless roundtrip failed for 16-bit");
641    }
642
643    #[test]
644    #[ignore]
645    fn test_lossless_roundtrip_rgba() {
646        let width = 4u32;
647        let height = 4u32;
648        let channels = 4u8;
649        let pixel_count = (width * height) as usize;
650
651        let mut data = Vec::with_capacity(pixel_count * channels as usize);
652        for i in 0..pixel_count {
653            data.push(((i * 13) % 256) as u8); // R
654            data.push(((i * 17 + 30) % 256) as u8); // G
655            data.push(((i * 23 + 60) % 256) as u8); // B
656            data.push(((i * 31 + 90) % 256) as u8); // A
657        }
658
659        let encoder = JxlEncoder::lossless();
660        let encoded = encoder
661            .encode(&data, width, height, channels, 8)
662            .expect("encode ok");
663
664        let decoder = JxlDecoder::new();
665        let decoded = decoder.decode(&encoded).expect("decode ok");
666
667        assert_eq!(decoded.width, width);
668        assert_eq!(decoded.height, height);
669        assert_eq!(decoded.channels, channels);
670        assert_eq!(decoded.data, data, "Lossless roundtrip failed for RGBA");
671    }
672
673    #[test]
674    #[ignore]
675    fn test_lossless_roundtrip_flat_image() {
676        // All-zero image (worst case for some compressors)
677        let width = 32u32;
678        let height = 32u32;
679        let channels = 3u8;
680        let data = vec![128u8; (width * height) as usize * channels as usize];
681
682        let encoder = JxlEncoder::lossless();
683        let encoded = encoder
684            .encode(&data, width, height, channels, 8)
685            .expect("encode ok");
686
687        let decoder = JxlDecoder::new();
688        let decoded = decoder.decode(&encoded).expect("decode ok");
689
690        assert_eq!(
691            decoded.data, data,
692            "Lossless roundtrip failed for flat image"
693        );
694    }
695
696    #[test]
697    #[ignore]
698    fn test_encode_invalid_buffer() {
699        let encoder = JxlEncoder::lossless();
700        let result = encoder.encode(&[0u8; 10], 100, 100, 3, 8);
701        assert!(result.is_err());
702    }
703
704    #[test]
705    #[ignore]
706    fn test_encode_zero_dimensions() {
707        let encoder = JxlEncoder::lossless();
708        assert!(encoder.encode(&[], 0, 100, 3, 8).is_err());
709        assert!(encoder.encode(&[], 100, 0, 3, 8).is_err());
710    }
711
712    #[test]
713    #[ignore]
714    fn test_signature_written() {
715        let encoder = JxlEncoder::lossless();
716        let data = vec![0u8; 64 * 64 * 3];
717        let encoded = encoder.encode(&data, 64, 64, 3, 8).expect("ok");
718        assert_eq!(encoded[0], 0xFF);
719        assert_eq!(encoded[1], 0x0A);
720    }
721
722    #[test]
723    #[ignore]
724    fn test_size_header_small() {
725        // 64x64 is divisible by 8 and fits in small encoding
726        let encoder = JxlEncoder::new(JxlConfig::new_lossless());
727        let mut writer = BitWriter::new();
728        encoder.write_size_header(&mut writer, 64, 64);
729        let data = writer.finish();
730
731        // First bit should be 1 (small=true)
732        assert_eq!(data[0] & 1, 1);
733    }
734
735    #[test]
736    #[ignore]
737    fn test_size_header_large() {
738        // Non-power-of-8 dimensions require full encoding
739        let encoder = JxlEncoder::new(JxlConfig::new_lossless());
740        let mut writer = BitWriter::new();
741        encoder.write_size_header(&mut writer, 1920, 1080);
742        let data = writer.finish();
743
744        // First bit should be 0 (small=false)
745        assert_eq!(data[0] & 1, 0);
746    }
747
748    #[test]
749    #[ignore]
750    fn test_effort_levels() {
751        let e1 = JxlEncoder::lossless_with_effort(1);
752        let e9 = JxlEncoder::lossless_with_effort(9);
753        assert_eq!(e1.config.effort, 1);
754        assert_eq!(e9.config.effort, 9);
755    }
756
757    #[test]
758    #[ignore]
759    fn test_deinterleave_rgb() {
760        let encoder = JxlEncoder::lossless();
761        let data = [10u8, 20, 30, 40, 50, 60];
762        let channels = encoder.deinterleave(&data, 2, 1, 3, 8).expect("ok");
763        assert_eq!(channels.len(), 3);
764        assert_eq!(channels[0], vec![10, 40]); // R
765        assert_eq!(channels[1], vec![20, 50]); // G
766        assert_eq!(channels[2], vec![30, 60]); // B
767    }
768
769    #[test]
770    #[ignore]
771    fn test_deinterleave_16bit() {
772        let encoder = JxlEncoder::lossless();
773        // Two 16-bit grayscale pixels: 0x0100 (256) and 0x0200 (512)
774        let data = [0x00u8, 0x01, 0x00, 0x02];
775        let channels = encoder.deinterleave(&data, 2, 1, 1, 16).expect("ok");
776        assert_eq!(channels.len(), 1);
777        assert_eq!(channels[0], vec![256, 512]);
778    }
779
780    // --- Animation encoder tests ---
781
782    /// Generate a simple test frame with a deterministic pattern seeded by `seed`.
783    fn make_test_frame(width: u32, height: u32, channels: u8, seed: u8) -> Vec<u8> {
784        let pixel_count = (width * height) as usize;
785        let mut data = Vec::with_capacity(pixel_count * channels as usize);
786        for i in 0..pixel_count {
787            for ch in 0..channels as usize {
788                let val = ((i.wrapping_mul(3 + ch) + seed as usize * 37 + ch * 50) % 256) as u8;
789                data.push(val);
790            }
791        }
792        data
793    }
794
795    #[test]
796    fn test_animated_encoder_three_frames_rgb() {
797        let anim = JxlAnimation::millisecond();
798        let width = 4u32;
799        let height = 4u32;
800        let channels = 3u8;
801
802        let mut encoder =
803            AnimatedJxlEncoder::new(anim, width, height, channels, 8).expect("create ok");
804
805        let f0 = make_test_frame(width, height, channels, 0);
806        let f1 = make_test_frame(width, height, channels, 1);
807        let f2 = make_test_frame(width, height, channels, 2);
808
809        encoder.add_frame(&f0, 100).expect("frame 0");
810        encoder.add_frame(&f1, 200).expect("frame 1");
811        encoder.add_frame(&f2, 150).expect("frame 2");
812
813        assert_eq!(encoder.frame_count(), 3);
814
815        let codestream = encoder.finish().expect("finish ok");
816
817        // Verify signature
818        assert!(JxlDecoder::is_codestream(&codestream));
819
820        // Decode animated
821        let decoder = JxlDecoder::new();
822        let frames = decoder.decode_animated(&codestream).expect("decode ok");
823
824        assert_eq!(frames.len(), 3);
825
826        // Verify frame 0
827        assert_eq!(frames[0].width, width);
828        assert_eq!(frames[0].height, height);
829        assert_eq!(frames[0].channels, channels);
830        assert_eq!(frames[0].duration_ticks, 100);
831        assert!(!frames[0].is_last);
832        assert_eq!(frames[0].data, f0, "Frame 0 pixel data mismatch");
833
834        // Verify frame 1
835        assert_eq!(frames[1].duration_ticks, 200);
836        assert!(!frames[1].is_last);
837        assert_eq!(frames[1].data, f1, "Frame 1 pixel data mismatch");
838
839        // Verify frame 2 (last)
840        assert_eq!(frames[2].duration_ticks, 150);
841        assert!(frames[2].is_last);
842        assert_eq!(frames[2].data, f2, "Frame 2 pixel data mismatch");
843    }
844
845    #[test]
846    fn test_animated_encoder_single_frame_with_animation_header() {
847        let anim = JxlAnimation::millisecond().with_num_loops(1);
848        let width = 2u32;
849        let height = 2u32;
850        let channels = 3u8;
851
852        let mut encoder =
853            AnimatedJxlEncoder::new(anim, width, height, channels, 8).expect("create ok");
854
855        let frame_data = make_test_frame(width, height, channels, 42);
856        encoder.add_frame(&frame_data, 500).expect("frame ok");
857
858        let codestream = encoder.finish().expect("finish ok");
859
860        let decoder = JxlDecoder::new();
861        let frames = decoder.decode_animated(&codestream).expect("decode ok");
862
863        assert_eq!(frames.len(), 1);
864        assert_eq!(frames[0].duration_ticks, 500);
865        assert!(frames[0].is_last);
866        assert_eq!(frames[0].data, frame_data);
867    }
868
869    #[test]
870    fn test_animated_encoder_zero_duration() {
871        let anim = JxlAnimation::millisecond();
872        let width = 2u32;
873        let height = 2u32;
874        let channels = 1u8;
875
876        let mut encoder =
877            AnimatedJxlEncoder::new(anim, width, height, channels, 8).expect("create ok");
878
879        let f0 = vec![128u8; 4];
880        encoder.add_frame(&f0, 0).expect("frame ok");
881
882        let codestream = encoder.finish().expect("finish ok");
883
884        let decoder = JxlDecoder::new();
885        let frames = decoder.decode_animated(&codestream).expect("decode ok");
886
887        assert_eq!(frames.len(), 1);
888        assert_eq!(frames[0].duration_ticks, 0);
889        assert!(frames[0].is_last);
890    }
891
892    #[test]
893    fn test_animated_encoder_infinite_loop() {
894        let anim = JxlAnimation::millisecond().with_num_loops(0);
895        let width = 2u32;
896        let height = 2u32;
897        let channels = 3u8;
898
899        let mut encoder =
900            AnimatedJxlEncoder::new(anim, width, height, channels, 8).expect("create ok");
901        encoder
902            .add_frame(&make_test_frame(width, height, channels, 0), 100)
903            .expect("ok");
904
905        let codestream = encoder.finish().expect("finish ok");
906
907        let decoder = JxlDecoder::new();
908        let anim_header = decoder
909            .read_animation_header(&codestream)
910            .expect("header ok");
911        let anim_header = anim_header.expect("should have animation");
912        assert_eq!(anim_header.num_loops, 0);
913    }
914
915    #[test]
916    fn test_animated_encoder_no_frames_error() {
917        let anim = JxlAnimation::millisecond();
918        let encoder = AnimatedJxlEncoder::new(anim, 4, 4, 3, 8).expect("create ok");
919        assert!(encoder.finish().is_err());
920    }
921
922    #[test]
923    fn test_animated_encoder_invalid_buffer_size() {
924        let anim = JxlAnimation::millisecond();
925        let mut encoder = AnimatedJxlEncoder::new(anim, 4, 4, 3, 8).expect("create ok");
926        // Buffer too small (4*4*3 = 48 bytes needed, only 10 provided)
927        assert!(encoder.add_frame(&[0u8; 10], 100).is_err());
928    }
929
930    #[test]
931    fn test_animated_encoder_grayscale() {
932        let anim = JxlAnimation::new(24, 1).expect("valid");
933        let width = 4u32;
934        let height = 4u32;
935        let channels = 1u8;
936
937        let mut encoder =
938            AnimatedJxlEncoder::new(anim, width, height, channels, 8).expect("create ok");
939
940        let f0 = make_test_frame(width, height, channels, 10);
941        let f1 = make_test_frame(width, height, channels, 20);
942
943        encoder.add_frame(&f0, 1).expect("frame 0");
944        encoder.add_frame(&f1, 1).expect("frame 1");
945
946        let codestream = encoder.finish().expect("finish ok");
947
948        let decoder = JxlDecoder::new();
949        let frames = decoder.decode_animated(&codestream).expect("decode ok");
950
951        assert_eq!(frames.len(), 2);
952        assert_eq!(frames[0].channels, 1);
953        assert_eq!(frames[0].data, f0);
954        assert_eq!(frames[1].data, f1);
955        assert!(frames[1].is_last);
956    }
957
958    #[test]
959    fn test_animated_encoder_rgba() {
960        let anim = JxlAnimation::millisecond();
961        let width = 4u32;
962        let height = 4u32;
963        let channels = 4u8;
964
965        let mut encoder =
966            AnimatedJxlEncoder::new(anim, width, height, channels, 8).expect("create ok");
967
968        let f0 = make_test_frame(width, height, channels, 0);
969        let f1 = make_test_frame(width, height, channels, 1);
970
971        encoder.add_frame(&f0, 50).expect("frame 0");
972        encoder.add_frame(&f1, 50).expect("frame 1");
973
974        let codestream = encoder.finish().expect("finish ok");
975
976        let decoder = JxlDecoder::new();
977        let frames = decoder.decode_animated(&codestream).expect("decode ok");
978
979        assert_eq!(frames.len(), 2);
980        assert_eq!(frames[0].channels, 4);
981        assert_eq!(frames[0].data, f0, "RGBA frame 0 mismatch");
982        assert_eq!(frames[1].data, f1, "RGBA frame 1 mismatch");
983    }
984
985    #[test]
986    fn test_animated_encoder_with_effort() {
987        let anim = JxlAnimation::millisecond();
988        let width = 4u32;
989        let height = 4u32;
990        let channels = 3u8;
991
992        let mut encoder = AnimatedJxlEncoder::new(anim, width, height, channels, 8)
993            .expect("create ok")
994            .with_effort(3);
995
996        let f0 = make_test_frame(width, height, channels, 0);
997        encoder.add_frame(&f0, 100).expect("frame ok");
998
999        let codestream = encoder.finish().expect("finish ok");
1000
1001        let decoder = JxlDecoder::new();
1002        let frames = decoder.decode_animated(&codestream).expect("decode ok");
1003        assert_eq!(frames.len(), 1);
1004        assert_eq!(frames[0].data, f0);
1005    }
1006
1007    #[test]
1008    fn test_animated_encoder_animation_header_roundtrip() {
1009        let anim = JxlAnimation::new(30, 1)
1010            .expect("valid")
1011            .with_num_loops(5)
1012            .with_timecodes(true);
1013
1014        let width = 2u32;
1015        let height = 2u32;
1016        let channels = 3u8;
1017
1018        let mut encoder =
1019            AnimatedJxlEncoder::new(anim.clone(), width, height, channels, 8).expect("create ok");
1020        encoder
1021            .add_frame(&make_test_frame(width, height, channels, 0), 1)
1022            .expect("ok");
1023
1024        let codestream = encoder.finish().expect("finish ok");
1025
1026        let decoder = JxlDecoder::new();
1027        let header_anim = decoder
1028            .read_animation_header(&codestream)
1029            .expect("read ok")
1030            .expect("should be animated");
1031
1032        assert_eq!(header_anim.tps_numerator, 30);
1033        assert_eq!(header_anim.tps_denominator, 1);
1034        assert_eq!(header_anim.num_loops, 5);
1035        assert!(header_anim.have_timecodes);
1036    }
1037
1038    #[test]
1039    fn test_animated_encoder_is_animated_check() {
1040        // Animated codestream
1041        let anim = JxlAnimation::millisecond();
1042        let mut anim_enc = AnimatedJxlEncoder::new(anim, 2, 2, 3, 8).expect("create ok");
1043        anim_enc
1044            .add_frame(&make_test_frame(2, 2, 3, 0), 100)
1045            .expect("ok");
1046        let animated_cs = anim_enc.finish().expect("ok");
1047
1048        let decoder = JxlDecoder::new();
1049        assert!(decoder.is_animated(&animated_cs).expect("check ok"));
1050
1051        // Non-animated codestream
1052        let encoder = JxlEncoder::lossless();
1053        let data = vec![0u8; 8 * 8 * 3];
1054        let still_cs = encoder.encode(&data, 8, 8, 3, 8).expect("ok");
1055        assert!(!decoder.is_animated(&still_cs).expect("check ok"));
1056    }
1057
1058    #[test]
1059    fn test_still_image_backwards_compat() {
1060        // Ensure the existing single-frame encode/decode pipeline is unchanged
1061        let width = 4u32;
1062        let height = 4u32;
1063        let channels = 3u8;
1064
1065        let data = make_test_frame(width, height, channels, 99);
1066
1067        let encoder = JxlEncoder::lossless();
1068        let encoded = encoder
1069            .encode(&data, width, height, channels, 8)
1070            .expect("encode ok");
1071
1072        // Decode with the standard decoder
1073        let decoder = JxlDecoder::new();
1074        let decoded = decoder.decode(&encoded).expect("decode ok");
1075        assert_eq!(decoded.data, data);
1076
1077        // Also decode with decode_animated: should give 1 frame
1078        let frames = decoder
1079            .decode_animated(&encoded)
1080            .expect("animated decode ok");
1081        assert_eq!(frames.len(), 1);
1082        assert_eq!(frames[0].duration_ticks, 0);
1083        assert!(frames[0].is_last);
1084        assert_eq!(frames[0].data, data);
1085    }
1086
1087    #[test]
1088    fn test_animated_encoder_many_frames() {
1089        let anim = JxlAnimation::millisecond();
1090        let width = 2u32;
1091        let height = 2u32;
1092        let channels = 3u8;
1093
1094        let mut encoder =
1095            AnimatedJxlEncoder::new(anim, width, height, channels, 8).expect("create ok");
1096
1097        let frame_count = 10;
1098        let mut test_frames = Vec::with_capacity(frame_count);
1099        for i in 0..frame_count {
1100            let f = make_test_frame(width, height, channels, i as u8);
1101            encoder.add_frame(&f, (i as u32 + 1) * 50).expect("ok");
1102            test_frames.push(f);
1103        }
1104
1105        let codestream = encoder.finish().expect("finish ok");
1106
1107        let decoder = JxlDecoder::new();
1108        let frames = decoder.decode_animated(&codestream).expect("decode ok");
1109
1110        assert_eq!(frames.len(), frame_count);
1111        for (i, frame) in frames.iter().enumerate() {
1112            assert_eq!(frame.duration_ticks, (i as u32 + 1) * 50);
1113            assert_eq!(frame.data, test_frames[i], "Frame {i} data mismatch");
1114            assert_eq!(frame.is_last, i == frame_count - 1);
1115        }
1116    }
1117}