Skip to main content

jxl_encoder/headers/
frame_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//! Frame header for JPEG XL.
6
7use crate::bit_writer::BitWriter;
8use crate::error::Result;
9
10/// Crop rectangle for a frame within the canvas.
11///
12/// When set on a frame, the frame contains only the specified rectangular region.
13/// The decoder composites this region onto the persistent canvas using the frame's
14/// blend mode. For `Replace` blending, only the crop rectangle is replaced; the
15/// rest of the canvas is unchanged.
16#[derive(Debug, Clone, Copy)]
17pub struct FrameCrop {
18    /// X offset of the crop region within the canvas.
19    pub x0: i32,
20    /// Y offset of the crop region within the canvas.
21    pub y0: i32,
22    /// Width of the crop region.
23    pub width: u32,
24    /// Height of the crop region.
25    pub height: u32,
26}
27
28/// Overrides for frame header fields in animation encoding.
29///
30/// Used by `encode_animation()` to set per-frame duration, is_last, and animation flags
31/// without exposing the full FrameHeader construction to callers.
32#[derive(Debug, Clone, Default)]
33pub struct FrameOptions {
34    /// Whether the file header has animation enabled.
35    pub have_animation: bool,
36    /// Whether the file header has have_timecodes enabled.
37    pub have_timecodes: bool,
38    /// Duration in ticks for this frame (only used if have_animation=true).
39    pub duration: u32,
40    /// Whether this is the last frame in the file.
41    pub is_last: bool,
42    /// Optional crop rectangle for this frame (None = full frame).
43    pub crop: Option<FrameCrop>,
44}
45
46/// Frame type.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48#[repr(u8)]
49pub enum FrameType {
50    /// Regular frame.
51    #[default]
52    Regular = 0,
53    /// LF (low-frequency) frame.
54    LfFrame = 1,
55    /// Reference-only frame (not displayed).
56    ReferenceOnly = 2,
57    /// Skip progressive rendering.
58    SkipProgressive = 3,
59}
60
61/// Encoding method for the frame.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
63#[repr(u8)]
64pub enum Encoding {
65    /// VarDCT encoding (lossy).
66    #[default]
67    VarDct = 0,
68    /// Modular encoding (lossless or lossy).
69    Modular = 1,
70}
71
72/// Blending mode for combining frames.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74#[repr(u8)]
75pub enum BlendMode {
76    /// Replace (no blending).
77    #[default]
78    Replace = 0,
79    /// Add to previous frame.
80    Add = 1,
81    /// Blend using alpha.
82    Blend = 2,
83    /// Alpha-weighted add.
84    AlphaWeightedAdd = 3,
85    /// Multiply.
86    Mul = 4,
87}
88
89/// Frame header structure.
90///
91/// Used by both VarDCT and Modular encoding paths. Fields are parameterized
92/// to cover both modes. Use `lossy()` or `lossless()` constructors for defaults.
93#[derive(Debug, Clone)]
94pub struct FrameHeader {
95    /// Frame type.
96    pub frame_type: FrameType,
97    /// Encoding method.
98    pub encoding: Encoding,
99    /// Whether the image metadata has xyb_encoded=true.
100    /// Controls whether do_ycbcr is written (only when false).
101    pub xyb_encoded: bool,
102    /// Frame flags (e.g., SKIP_ADAPTIVE_LF_SMOOTHING=0x80, ENABLE_NOISE=0x01).
103    pub flags: u64,
104    /// Whether the frame uses YCbCr color transform (only written when !xyb_encoded).
105    pub do_ycbcr: bool,
106    /// JPEG upsampling mode for chroma (only for VarDCT + YCbCr).
107    pub jpeg_upsampling: [u8; 3],
108    /// Upsampling factor (1, 2, 4, or 8).
109    pub upsampling: u32,
110    /// Extra channel upsampling factors.
111    pub ec_upsampling: Vec<u32>,
112    /// Group size shift (Modular only: 0=128, 1=256, 2=512, 3=1024).
113    pub group_size_shift: u32,
114    /// X channel quant matrix scale (VarDCT only, 3 bits, range 0-7).
115    pub x_qm_scale: u32,
116    /// B channel quant matrix scale (VarDCT only, 3 bits, range 0-7).
117    pub b_qm_scale: u32,
118    /// Number of passes (1-10).
119    pub num_passes: u32,
120    /// X offset for cropped frames.
121    pub x0: i32,
122    /// Y offset for cropped frames.
123    pub y0: i32,
124    /// Frame width (0 = full image width).
125    pub width: u32,
126    /// Frame height (0 = full image height).
127    pub height: u32,
128    /// Blending information for the main frame.
129    pub blend_mode: BlendMode,
130    /// Per-extra-channel blending modes.
131    pub ec_blend_modes: Vec<BlendMode>,
132    /// Source reference frame for blending (0-3).
133    pub blend_source: u32,
134    /// Alpha channel to use for blending.
135    pub alpha_blend_channel: u32,
136    /// Whether frame is saved for reference.
137    pub save_as_reference: u32,
138    /// Whether to save before color transform.
139    pub save_before_ct: bool,
140    /// Frame name.
141    pub name: String,
142    /// Whether the file header signals animation (have_animation=true).
143    /// When true, duration/timecode fields are written for normal frames.
144    pub have_animation: bool,
145    /// Whether the file header signals have_timecodes.
146    pub have_timecodes: bool,
147    /// Duration in ticks (for animation).
148    pub duration: u32,
149    /// Timecode (if have_timecodes).
150    pub timecode: u32,
151    /// Whether this is the last frame.
152    pub is_last: bool,
153    /// Enable gaborish (Gabor-like blur in decoder loop filter).
154    pub gaborish: bool,
155    /// Number of EPF (Edge-Preserving Filter) iterations (0-3).
156    pub epf_iters: u32,
157}
158
159impl Default for FrameHeader {
160    fn default() -> Self {
161        Self {
162            frame_type: FrameType::Regular,
163            encoding: Encoding::VarDct,
164            xyb_encoded: true,
165            flags: 0,
166            do_ycbcr: false,
167            jpeg_upsampling: [0; 3],
168            upsampling: 1,
169            ec_upsampling: Vec::new(),
170            group_size_shift: 1,
171            x_qm_scale: 2,
172            b_qm_scale: 2,
173            num_passes: 1,
174            x0: 0,
175            y0: 0,
176            width: 0,
177            height: 0,
178            blend_mode: BlendMode::Replace,
179            blend_source: 0,
180            ec_blend_modes: Vec::new(),
181            alpha_blend_channel: 0,
182            save_as_reference: 0,
183            save_before_ct: false,
184            name: String::new(),
185            have_animation: false,
186            have_timecodes: false,
187            duration: 0,
188            timecode: 0,
189            is_last: true,
190            gaborish: true,
191            epf_iters: 2,
192        }
193    }
194}
195
196impl FrameHeader {
197    /// Creates a frame header for a lossy VarDCT frame with default parameters.
198    ///
199    /// Defaults: xyb_encoded=true, flags=SKIP_ADAPTIVE_LF_SMOOTHING (0x80),
200    /// gaborish=true, epf_iters=2.
201    pub fn lossy() -> Self {
202        Self {
203            encoding: Encoding::VarDct,
204            xyb_encoded: true,
205            flags: 0x80, // SKIP_ADAPTIVE_LF_SMOOTHING
206            gaborish: true,
207            epf_iters: 2,
208            ..Default::default()
209        }
210    }
211
212    /// Creates a frame header for a lossless Modular frame.
213    ///
214    /// Defaults: xyb_encoded=false, do_ycbcr=false, flags=0,
215    /// group_size_shift=1 (256), gaborish=false, epf_iters=0.
216    pub fn lossless() -> Self {
217        Self {
218            encoding: Encoding::Modular,
219            xyb_encoded: false,
220            do_ycbcr: false,
221            flags: 0,
222            group_size_shift: 1,
223            gaborish: false,
224            epf_iters: 0,
225            ..Default::default()
226        }
227    }
228
229    /// Writes the frame header to the bitstream.
230    ///
231    /// Follows the JXL codestream specification (ISO 18181-1) Table A.2.
232    pub fn write(&self, writer: &mut BitWriter) -> Result<()> {
233        // all_default: true only when all fields match the decoder's default
234        // VarDCT default frame: Regular, VarDCT, no flags, do_ycbcr=true,
235        // upsampling=1, group_size_shift=1, x/b_qm_scale=2, 1 pass,
236        // no crop, Replace blend, is_last=true, no name, gab+epf2
237        let all_default = self.is_all_default();
238        writer.write_bit(all_default)?;
239        if all_default {
240            return Ok(());
241        }
242
243        // frame_type
244        writer.write(2, self.frame_type as u64)?;
245
246        // encoding
247        writer.write(1, self.encoding as u64)?;
248
249        // flags (U64)
250        writer.write_u64_coder(self.flags)?;
251
252        // do_ycbcr: only present when xyb_encoded is false
253        if !self.xyb_encoded {
254            writer.write_bit(self.do_ycbcr)?;
255        }
256
257        // jpeg_upsampling: only for VarDCT with YCbCr (when do_ycbcr and !xyb_encoded)
258        if self.encoding == Encoding::VarDct && self.do_ycbcr && !self.xyb_encoded {
259            for &up in &self.jpeg_upsampling {
260                writer.write(2, up as u64)?;
261            }
262        }
263
264        // upsampling (U32: 1, 2, 4, 8)
265        writer.write_u32_coder(self.upsampling, 1, 2, 4, 8, 0)?;
266
267        // ec_upsampling per extra channel
268        for &ecu in &self.ec_upsampling {
269            writer.write_u32_coder(ecu, 1, 2, 4, 8, 0)?;
270        }
271
272        // group_size_shift: Modular only (VarDCT uses fixed 256x256 groups)
273        if self.encoding == Encoding::Modular {
274            writer.write(2, self.group_size_shift as u64)?;
275        }
276
277        // x_qm_scale, b_qm_scale: VarDCT + xyb_encoded only
278        if self.encoding == Encoding::VarDct && self.xyb_encoded {
279            writer.write(3, self.x_qm_scale as u64)?;
280            writer.write(3, self.b_qm_scale as u64)?;
281        }
282
283        // num_passes (U32: 1, 2, 3, 4+u(3))
284        writer.write_u32_coder(self.num_passes, 1, 2, 3, 4, 3)?;
285        // TODO: if num_passes > 1, write pass-specific data
286
287        // have_crop (only for non-LfFrame, non-ReferenceOnly)
288        if self.frame_type != FrameType::ReferenceOnly {
289            let have_crop = self.x0 != 0 || self.y0 != 0 || self.width != 0 || self.height != 0;
290            writer.write_bit(have_crop)?;
291            if have_crop {
292                self.write_crop(writer)?;
293            }
294        }
295
296        // blending_info (for Regular or SkipProgressive frames)
297        let normal_frame =
298            self.frame_type == FrameType::Regular || self.frame_type == FrameType::SkipProgressive;
299        if normal_frame {
300            self.write_blending_info(writer)?;
301        }
302
303        // ec_blending_info per extra channel
304        for &mode in &self.ec_blend_modes {
305            self.write_ec_blending_info(mode, writer)?;
306        }
307
308        // duration and timecode (for animated normal frames)
309        if normal_frame && self.have_animation {
310            // duration: U32(Val(0), Val(1), Bits(8), Bits(32))
311            match self.duration {
312                0 => writer.write(2, 0)?,
313                1 => writer.write(2, 1)?,
314                d if d <= 255 => {
315                    writer.write(2, 2)?;
316                    writer.write(8, d as u64)?;
317                }
318                d => {
319                    writer.write(2, 3)?;
320                    writer.write(32, d as u64)?;
321                }
322            }
323            if self.have_timecodes {
324                writer.write(32, self.timecode as u64)?;
325            }
326        }
327
328        // is_last (for Regular or SkipProgressive)
329        if normal_frame {
330            writer.write_bit(self.is_last)?;
331        }
332
333        // save_as_reference (only when !is_last and not LfFrame)
334        if !self.is_last && self.frame_type != FrameType::LfFrame {
335            writer.write(2, self.save_as_reference as u64)?;
336            // save_before_ct: written when frame resets canvas and can be referenced.
337            // Condition matches decoder: resets_canvas && (duration==0 || save_as_reference!=0)
338            let full_frame = self.x0 == 0 && self.y0 == 0 && self.width == 0 && self.height == 0;
339            let resets_canvas = self.blend_mode == BlendMode::Replace && full_frame;
340            if resets_canvas && (self.duration == 0 || self.save_as_reference != 0) {
341                writer.write_bit(self.save_before_ct)?;
342            }
343        }
344
345        // name
346        self.write_name(writer)?;
347
348        // restoration_filter (loop filter)
349        self.write_loop_filter(writer)?;
350
351        // frame header extensions (U64, always 0 for now)
352        writer.write_u64_coder(0)?;
353
354        Ok(())
355    }
356
357    /// Writes crop information.
358    ///
359    /// Crop dimensions use U32(Bits(8), Bits(11)+256, Bits(14)+2048, Bits(30)+18432).
360    /// x0/y0 are packed-signed first, then encoded with the same distribution.
361    fn write_crop(&self, writer: &mut BitWriter) -> Result<()> {
362        // x0, y0 as UnpackSigned
363        let x0u = if self.x0 >= 0 {
364            (self.x0 as u32) << 1
365        } else {
366            (((-self.x0 - 1) as u32) << 1) | 1
367        };
368        let y0u = if self.y0 >= 0 {
369            (self.y0 as u32) << 1
370        } else {
371            (((-self.y0 - 1) as u32) << 1) | 1
372        };
373
374        Self::write_crop_u32(writer, x0u)?;
375        Self::write_crop_u32(writer, y0u)?;
376        Self::write_crop_u32(writer, self.width)?;
377        Self::write_crop_u32(writer, self.height)?;
378
379        Ok(())
380    }
381
382    /// Encodes a single crop dimension value using U32(Bits(8), Bits(11)+256, Bits(14)+2304, Bits(30)+18688).
383    fn write_crop_u32(writer: &mut BitWriter, value: u32) -> Result<()> {
384        if value < 256 {
385            writer.write(2, 0)?; // selector 0: Bits(8)
386            writer.write(8, value as u64)?;
387        } else if value < 2304 {
388            writer.write(2, 1)?; // selector 1: Bits(11)+256
389            writer.write(11, (value - 256) as u64)?;
390        } else if value < 18688 {
391            writer.write(2, 2)?; // selector 2: Bits(14)+2304
392            writer.write(14, (value - 2304) as u64)?;
393        } else {
394            writer.write(2, 3)?; // selector 3: Bits(30)+18688
395            writer.write(30, (value - 18688) as u64)?;
396        }
397        Ok(())
398    }
399
400    /// Writes blending information for the main frame.
401    fn write_blending_info(&self, writer: &mut BitWriter) -> Result<()> {
402        writer.write_u32_coder(self.blend_mode as u32, 0, 1, 2, 3, 2)?;
403
404        // source: only when not (full_frame && Replace)
405        // Full frame is the default (no crop), so source is written for non-Replace modes.
406        let full_frame = self.x0 == 0 && self.y0 == 0 && self.width == 0 && self.height == 0;
407        if !(full_frame && self.blend_mode == BlendMode::Replace) {
408            writer.write(2, self.blend_source as u64)?;
409        }
410
411        if self.blend_mode == BlendMode::Blend || self.blend_mode == BlendMode::AlphaWeightedAdd {
412            writer.write_u32_coder(self.alpha_blend_channel, 0, 1, 2, 3, 3)?;
413            writer.write_bit(false)?; // clamp = false
414        }
415
416        Ok(())
417    }
418
419    /// Writes blending information for an extra channel.
420    fn write_ec_blending_info(&self, mode: BlendMode, writer: &mut BitWriter) -> Result<()> {
421        writer.write_u32_coder(mode as u32, 0, 1, 2, 3, 2)?;
422
423        let full_frame = self.x0 == 0 && self.y0 == 0 && self.width == 0 && self.height == 0;
424        if !(full_frame && mode == BlendMode::Replace) {
425            writer.write(2, 0)?; // source = 0
426        }
427
428        if mode == BlendMode::Blend || mode == BlendMode::AlphaWeightedAdd {
429            writer.write_u32_coder(0, 0, 1, 2, 3, 3)?; // alpha channel = 0
430            writer.write_bit(false)?; // clamp = false
431        }
432
433        Ok(())
434    }
435
436    /// Writes the frame name.
437    fn write_name(&self, writer: &mut BitWriter) -> Result<()> {
438        let name_len = self.name.len() as u32;
439        if name_len == 0 {
440            writer.write(2, 0)?; // selector 0 = length 0
441        } else if name_len < 4 {
442            writer.write(2, 0)?; // selector 0 (length encoded as 0, but name bytes follow)
443        } else if name_len < 20 {
444            writer.write(2, 2)?;
445            writer.write(4, (name_len - 4) as u64)?;
446        } else {
447            writer.write(2, 3)?;
448            writer.write(10, (name_len - 20) as u64)?;
449        }
450        for byte in self.name.bytes() {
451            writer.write(8, byte as u64)?;
452        }
453        Ok(())
454    }
455
456    /// Writes the loop filter (restoration_filter) section.
457    fn write_loop_filter(&self, writer: &mut BitWriter) -> Result<()> {
458        // all_default means gab=true, epf_iters=2 (decoder defaults)
459        let lf_all_default = self.gaborish && self.epf_iters == 2;
460
461        writer.write_bit(lf_all_default)?;
462        if lf_all_default {
463            return Ok(());
464        }
465
466        // gab
467        writer.write_bit(self.gaborish)?;
468        if self.gaborish {
469            writer.write_bit(false)?; // gab_custom = false (use default weights)
470        }
471
472        // epf_iters
473        writer.write(2, self.epf_iters as u64)?;
474
475        // EPF custom parameters (only when epf_iters > 0)
476        if self.epf_iters > 0 {
477            writer.write_bit(false)?; // epf_sharp_custom = false
478            writer.write_bit(false)?; // epf_weight_custom = false
479            writer.write_bit(false)?; // epf_sigma_custom = false
480        }
481
482        // loop filter extensions (U64)
483        writer.write_u64_coder(0)?;
484
485        Ok(())
486    }
487
488    /// Returns true if all fields match the decoder's "all_default" frame header.
489    ///
490    /// The all_default frame header is: Regular VarDCT, no flags, do_ycbcr=true,
491    /// upsampling=1, group_size_shift=1, x/b_qm_scale=2, 1 pass, no crop,
492    /// Replace blend, is_last=true, no name, default loop filter (gab+epf2).
493    fn is_all_default(&self) -> bool {
494        self.frame_type == FrameType::Regular
495            && self.encoding == Encoding::VarDct
496            && self.xyb_encoded
497            && self.flags == 0
498            && self.do_ycbcr
499            && self.upsampling == 1
500            && self.ec_upsampling.is_empty()
501            && self.ec_blend_modes.is_empty()
502            && self.group_size_shift == 1
503            && self.x_qm_scale == 2
504            && self.b_qm_scale == 2
505            && self.num_passes == 1
506            && self.x0 == 0
507            && self.y0 == 0
508            && self.width == 0
509            && self.height == 0
510            && self.blend_mode == BlendMode::Replace
511            && self.blend_source == 0
512            && self.save_as_reference == 0
513            && !self.save_before_ct
514            && self.name.is_empty()
515            && !self.have_animation
516            && self.is_last
517            && self.gaborish
518            && self.epf_iters == 2
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    #[test]
527    fn test_default_frame() {
528        let frame = FrameHeader::lossy();
529        let mut writer = BitWriter::new();
530        frame.write(&mut writer).unwrap();
531    }
532
533    #[test]
534    fn test_lossless_frame() {
535        let frame = FrameHeader::lossless();
536        assert_eq!(frame.encoding, Encoding::Modular);
537        assert!(!frame.do_ycbcr);
538        assert!(!frame.gaborish);
539        assert_eq!(frame.epf_iters, 0);
540
541        let mut writer = BitWriter::new();
542        frame.write(&mut writer).unwrap();
543        assert!(writer.bits_written() > 0);
544    }
545
546    #[test]
547    fn test_frame_type_values() {
548        assert_eq!(FrameType::Regular as u8, 0);
549        assert_eq!(FrameType::LfFrame as u8, 1);
550        assert_eq!(FrameType::ReferenceOnly as u8, 2);
551        assert_eq!(FrameType::SkipProgressive as u8, 3);
552    }
553
554    #[test]
555    fn test_encoding_values() {
556        assert_eq!(Encoding::VarDct as u8, 0);
557        assert_eq!(Encoding::Modular as u8, 1);
558    }
559
560    #[test]
561    fn test_blend_mode_values() {
562        assert_eq!(BlendMode::Replace as u8, 0);
563        assert_eq!(BlendMode::Add as u8, 1);
564        assert_eq!(BlendMode::Blend as u8, 2);
565        assert_eq!(BlendMode::AlphaWeightedAdd as u8, 3);
566        assert_eq!(BlendMode::Mul as u8, 4);
567    }
568
569    #[test]
570    fn test_frame_with_crop() {
571        let mut frame = FrameHeader::lossy();
572        frame.x0 = 0;
573        frame.y0 = 0;
574        frame.width = 20000;
575        frame.height = 20000;
576
577        let mut writer = BitWriter::new();
578        frame.write(&mut writer).unwrap();
579        assert!(writer.bits_written() > 10);
580    }
581
582    #[test]
583    fn test_frame_with_large_crop_offset() {
584        let mut frame = FrameHeader::lossy();
585        frame.x0 = 128;
586        frame.y0 = 128;
587        frame.width = 20000;
588        frame.height = 20000;
589
590        let mut writer = BitWriter::new();
591        frame.write(&mut writer).unwrap();
592        assert!(writer.bits_written() > 10);
593    }
594
595    #[test]
596    fn test_frame_with_name() {
597        let mut frame = FrameHeader::lossy();
598        frame.name = "TestFrame".to_string();
599
600        let mut writer = BitWriter::new();
601        frame.write(&mut writer).unwrap();
602        assert!(writer.bits_written() > 80);
603    }
604
605    #[test]
606    fn test_frame_with_long_name() {
607        let mut frame = FrameHeader::lossy();
608        frame.name = "ThisIsAVeryLongFrameName".to_string();
609
610        let mut writer = BitWriter::new();
611        frame.write(&mut writer).unwrap();
612        assert!(writer.bits_written() > 200);
613    }
614
615    #[test]
616    fn test_lf_frame_type() {
617        let mut frame = FrameHeader::lossy();
618        frame.frame_type = FrameType::LfFrame;
619
620        let mut writer = BitWriter::new();
621        frame.write(&mut writer).unwrap();
622        assert!(writer.bits_written() > 0);
623    }
624
625    #[test]
626    fn test_reference_only_frame() {
627        let mut frame = FrameHeader::lossy();
628        frame.frame_type = FrameType::ReferenceOnly;
629
630        let mut writer = BitWriter::new();
631        frame.write(&mut writer).unwrap();
632        assert!(writer.bits_written() > 0);
633    }
634
635    #[test]
636    fn test_skip_progressive_frame() {
637        let mut frame = FrameHeader::lossy();
638        frame.frame_type = FrameType::SkipProgressive;
639
640        let mut writer = BitWriter::new();
641        frame.write(&mut writer).unwrap();
642        assert!(writer.bits_written() > 0);
643    }
644
645    #[test]
646    fn test_blend_mode_add() {
647        let mut frame = FrameHeader::lossy();
648        frame.blend_mode = BlendMode::Add;
649
650        let mut writer = BitWriter::new();
651        frame.write(&mut writer).unwrap();
652        assert!(writer.bits_written() > 0);
653    }
654
655    #[test]
656    fn test_blend_mode_blend_with_alpha() {
657        let mut frame = FrameHeader::lossy();
658        frame.blend_mode = BlendMode::Blend;
659        frame.alpha_blend_channel = 1;
660
661        let mut writer = BitWriter::new();
662        frame.write(&mut writer).unwrap();
663        assert!(writer.bits_written() > 0);
664    }
665
666    #[test]
667    fn test_blend_mode_alpha_weighted_add() {
668        let mut frame = FrameHeader::lossy();
669        frame.blend_mode = BlendMode::AlphaWeightedAdd;
670        frame.alpha_blend_channel = 2;
671
672        let mut writer = BitWriter::new();
673        frame.write(&mut writer).unwrap();
674        assert!(writer.bits_written() > 0);
675    }
676
677    #[test]
678    fn test_blend_mode_mul() {
679        let mut frame = FrameHeader::lossy();
680        frame.blend_mode = BlendMode::Mul;
681
682        let mut writer = BitWriter::new();
683        frame.write(&mut writer).unwrap();
684        assert!(writer.bits_written() > 0);
685    }
686
687    #[test]
688    fn test_upsampling_factors() {
689        for upsampling in [1, 2, 4, 8] {
690            let mut frame = FrameHeader::lossy();
691            frame.upsampling = upsampling;
692
693            let mut writer = BitWriter::new();
694            frame.write(&mut writer).unwrap();
695            assert!(writer.bits_written() > 0);
696        }
697    }
698
699    #[test]
700    fn test_ec_upsampling() {
701        let mut frame = FrameHeader::lossy();
702        frame.ec_upsampling = vec![2, 4, 8];
703
704        let mut writer = BitWriter::new();
705        frame.write(&mut writer).unwrap();
706        assert!(writer.bits_written() > 0);
707    }
708
709    #[test]
710    fn test_group_size_shift() {
711        for shift in 0..4 {
712            let mut frame = FrameHeader::lossless();
713            frame.group_size_shift = shift;
714
715            let mut writer = BitWriter::new();
716            frame.write(&mut writer).unwrap();
717            assert!(writer.bits_written() > 0);
718        }
719    }
720
721    #[test]
722    fn test_save_as_reference() {
723        let mut frame = FrameHeader::lossy();
724        frame.save_as_reference = 2;
725        frame.is_last = false; // save_as_reference only written when !is_last
726
727        let mut writer = BitWriter::new();
728        frame.write(&mut writer).unwrap();
729        assert!(writer.bits_written() > 0);
730    }
731
732    #[test]
733    fn test_not_last_frame() {
734        let mut frame = FrameHeader::lossy();
735        frame.is_last = false;
736
737        let mut writer = BitWriter::new();
738        frame.write(&mut writer).unwrap();
739        assert!(writer.bits_written() > 0);
740    }
741
742    #[test]
743    fn test_vardct_loop_filter_all_default() {
744        // gab=true, epf=2 → all_default for loop filter
745        let frame = FrameHeader::lossy();
746        assert!(frame.gaborish && frame.epf_iters == 2);
747
748        let mut writer = BitWriter::new();
749        frame.write(&mut writer).unwrap();
750    }
751
752    #[test]
753    fn test_vardct_no_gaborish() {
754        let mut frame = FrameHeader::lossy();
755        frame.gaborish = false;
756        frame.epf_iters = 1;
757
758        let mut writer = BitWriter::new();
759        frame.write(&mut writer).unwrap();
760        assert!(writer.bits_written() > 0);
761    }
762
763    #[test]
764    fn test_vardct_no_epf() {
765        let mut frame = FrameHeader::lossy();
766        frame.gaborish = true;
767        frame.epf_iters = 0;
768
769        let mut writer = BitWriter::new();
770        frame.write(&mut writer).unwrap();
771        assert!(writer.bits_written() > 0);
772    }
773
774    #[test]
775    fn test_vardct_with_noise() {
776        let mut frame = FrameHeader::lossy();
777        frame.flags = 0x80 | 0x01; // SKIP_LF_SMOOTHING + ENABLE_NOISE
778
779        let mut writer = BitWriter::new();
780        frame.write(&mut writer).unwrap();
781        assert!(writer.bits_written() > 0);
782    }
783
784    #[test]
785    fn test_vardct_custom_qm_scale() {
786        let mut frame = FrameHeader::lossy();
787        frame.x_qm_scale = 5;
788        frame.b_qm_scale = 4;
789
790        let mut writer = BitWriter::new();
791        frame.write(&mut writer).unwrap();
792        assert!(writer.bits_written() > 0);
793    }
794
795    #[test]
796    fn test_vardct_with_extra_channels() {
797        let mut frame = FrameHeader::lossy();
798        frame.ec_upsampling = vec![1]; // one extra channel, no upsampling
799        frame.ec_blend_modes = vec![BlendMode::Replace];
800
801        let mut writer = BitWriter::new();
802        frame.write(&mut writer).unwrap();
803        assert!(writer.bits_written() > 0);
804    }
805
806    #[test]
807    fn test_lossless_with_extra_channels() {
808        let mut frame = FrameHeader::lossless();
809        frame.ec_upsampling = vec![1]; // one extra channel, no upsampling
810        frame.ec_blend_modes = vec![BlendMode::Replace];
811
812        let mut writer = BitWriter::new();
813        frame.write(&mut writer).unwrap();
814        assert!(writer.bits_written() > 0);
815    }
816
817    /// Verify that our VarDCT frame header matches the old hand-written write_frame_header()
818    /// bit for bit. Parameters: x_qm=3, b_qm=2, epf=1, noise=false, gab=true, 0 extra channels.
819    #[test]
820    fn test_vardct_bit_exact_vs_old() {
821        // Old path equivalent:
822        // flags = 128 (0x80), x_qm=3, b_qm=2, epf=1, gab=true, 0 extra channels
823        let mut old_writer = BitWriter::new();
824        // Manually replicate the old write_frame_header():
825        old_writer.write(1, 0).unwrap(); // not all_default
826        old_writer.write(2, 0).unwrap(); // RegularFrame
827        old_writer.write(1, 0).unwrap(); // VarDCT
828        old_writer.write(2, 2).unwrap(); // flags U64 selector 2
829        old_writer.write(8, 128 - 17).unwrap(); // flags = 128
830        old_writer.write(2, 0).unwrap(); // upsampling = 1
831        old_writer.write(3, 3).unwrap(); // x_qm_scale
832        old_writer.write(3, 2).unwrap(); // b_qm_scale
833        old_writer.write(2, 0).unwrap(); // num_passes = 1
834        old_writer.write(1, 0).unwrap(); // have_crop = false
835        old_writer.write(2, 0).unwrap(); // blend = Replace
836        old_writer.write(1, 1).unwrap(); // is_last
837        old_writer.write(2, 0).unwrap(); // name = ""
838        // Loop filter: not all_default (gab=true but epf=1, not 2)
839        old_writer.write(1, 0).unwrap(); // lf not all_default
840        old_writer.write(1, 1).unwrap(); // gab = true
841        old_writer.write(1, 0).unwrap(); // gab_custom = false
842        old_writer.write(2, 1).unwrap(); // epf_iters = 1
843        old_writer.write(1, 0).unwrap(); // epf_sharp_custom = false
844        old_writer.write(1, 0).unwrap(); // epf_weight_custom = false
845        old_writer.write(1, 0).unwrap(); // epf_sigma_custom = false
846        old_writer.write(2, 0).unwrap(); // lf_extensions = 0
847        old_writer.write(2, 0).unwrap(); // frame_extensions = 0
848
849        let mut new_writer = BitWriter::new();
850        let mut frame = FrameHeader::lossy();
851        frame.x_qm_scale = 3;
852        frame.b_qm_scale = 2;
853        frame.epf_iters = 1;
854        frame.write(&mut new_writer).unwrap();
855
856        // Compare bit counts (writers may not be byte-aligned)
857        assert_eq!(
858            old_writer.bits_written(),
859            new_writer.bits_written(),
860            "VarDCT frame header bit count should match"
861        );
862        // Pad and compare bytes
863        old_writer.zero_pad_to_byte();
864        new_writer.zero_pad_to_byte();
865        assert_eq!(
866            old_writer.finish(),
867            new_writer.finish(),
868            "VarDCT frame header should be bit-exact"
869        );
870    }
871
872    /// Verify VarDCT with gab=true, epf=2 (loop filter all_default).
873    #[test]
874    fn test_vardct_lf_all_default_bit_exact() {
875        let mut old_writer = BitWriter::new();
876        old_writer.write(1, 0).unwrap(); // not all_default
877        old_writer.write(2, 0).unwrap(); // RegularFrame
878        old_writer.write(1, 0).unwrap(); // VarDCT
879        old_writer.write(2, 2).unwrap(); // flags U64 selector 2
880        old_writer.write(8, 128 - 17).unwrap(); // flags = 128
881        old_writer.write(2, 0).unwrap(); // upsampling = 1
882        old_writer.write(3, 3).unwrap(); // x_qm_scale
883        old_writer.write(3, 2).unwrap(); // b_qm_scale
884        old_writer.write(2, 0).unwrap(); // num_passes = 1
885        old_writer.write(1, 0).unwrap(); // have_crop = false
886        old_writer.write(2, 0).unwrap(); // blend = Replace
887        old_writer.write(1, 1).unwrap(); // is_last
888        old_writer.write(2, 0).unwrap(); // name = ""
889        old_writer.write(1, 1).unwrap(); // lf all_default
890        old_writer.write(2, 0).unwrap(); // frame_extensions = 0
891
892        let mut new_writer = BitWriter::new();
893        let mut frame = FrameHeader::lossy();
894        frame.x_qm_scale = 3;
895        frame.b_qm_scale = 2;
896        frame.gaborish = true;
897        frame.epf_iters = 2;
898        frame.write(&mut new_writer).unwrap();
899
900        assert_eq!(
901            old_writer.bits_written(),
902            new_writer.bits_written(),
903            "VarDCT lf all_default bit count should match"
904        );
905        old_writer.zero_pad_to_byte();
906        new_writer.zero_pad_to_byte();
907        assert_eq!(
908            old_writer.finish(),
909            new_writer.finish(),
910            "VarDCT with lf all_default should be bit-exact"
911        );
912    }
913}