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 flag: enable noise synthesis.
90pub const ENABLE_NOISE: u64 = 0x01;
91/// Frame flag: enable patches (dictionary-based repeated patterns).
92pub const PATCHES_FLAG: u64 = 0x02;
93/// Frame flag: enable splines.
94pub const SPLINES_FLAG: u64 = 0x10;
95/// Frame flag: use a separate LF frame for DC coefficients.
96pub const USE_LF_FRAME: u64 = 0x20;
97/// Frame flag: skip adaptive LF smoothing.
98pub const SKIP_ADAPTIVE_LF_SMOOTHING: u64 = 0x80;
99
100/// Frame header structure.
101///
102/// Used by both VarDCT and Modular encoding paths. Fields are parameterized
103/// to cover both modes. Use `lossy()` or `lossless()` constructors for defaults.
104#[derive(Debug, Clone)]
105pub struct FrameHeader {
106    /// Frame type.
107    pub frame_type: FrameType,
108    /// Encoding method.
109    pub encoding: Encoding,
110    /// Whether the image metadata has xyb_encoded=true.
111    /// Controls whether do_ycbcr is written (only when false).
112    pub xyb_encoded: bool,
113    /// Frame flags (bitfield: ENABLE_NOISE=0x01, PATCHES_FLAG=0x02,
114    /// SKIP_ADAPTIVE_LF_SMOOTHING=0x80).
115    pub flags: u64,
116    /// Whether the frame uses YCbCr color transform (only written when !xyb_encoded).
117    pub do_ycbcr: bool,
118    /// JPEG upsampling mode for chroma (only for VarDCT + YCbCr).
119    pub jpeg_upsampling: [u8; 3],
120    /// Upsampling factor (1, 2, 4, or 8).
121    pub upsampling: u32,
122    /// Extra channel upsampling factors.
123    pub ec_upsampling: Vec<u32>,
124    /// Group size shift (Modular only: 0=128, 1=256, 2=512, 3=1024).
125    pub group_size_shift: u32,
126    /// X channel quant matrix scale (VarDCT only, 3 bits, range 0-7).
127    pub x_qm_scale: u32,
128    /// B channel quant matrix scale (VarDCT only, 3 bits, range 0-7).
129    pub b_qm_scale: u32,
130    /// Number of passes (1-11).
131    pub num_passes: u32,
132    /// Per-pass shift values (num_passes - 1 elements). Last pass implicitly has shift=0.
133    /// Each shift is 0-3 bits: coefficients are right-shifted before encoding,
134    /// left-shifted by the decoder before accumulation.
135    pub pass_shifts: Vec<u32>,
136    /// Number of downsampling brackets (0-4).
137    pub num_ds: u32,
138    /// Downsample factors per bracket (1, 2, 4, or 8).
139    pub ds_downsample: Vec<u32>,
140    /// Last pass index per downsampling bracket.
141    pub ds_last_pass: Vec<u32>,
142    /// X offset for cropped frames.
143    pub x0: i32,
144    /// Y offset for cropped frames.
145    pub y0: i32,
146    /// Frame width (0 = full image width).
147    pub width: u32,
148    /// Frame height (0 = full image height).
149    pub height: u32,
150    /// Blending information for the main frame.
151    pub blend_mode: BlendMode,
152    /// Per-extra-channel blending modes.
153    pub ec_blend_modes: Vec<BlendMode>,
154    /// Source reference frame for blending (0-3).
155    pub blend_source: u32,
156    /// Alpha channel to use for blending.
157    pub alpha_blend_channel: u32,
158    /// Whether frame is saved for reference.
159    pub save_as_reference: u32,
160    /// Whether to save before color transform.
161    pub save_before_ct: bool,
162    /// Frame name.
163    pub name: String,
164    /// Whether the file header signals animation (have_animation=true).
165    /// When true, duration/timecode fields are written for normal frames.
166    pub have_animation: bool,
167    /// Whether the file header signals have_timecodes.
168    pub have_timecodes: bool,
169    /// Duration in ticks (for animation).
170    pub duration: u32,
171    /// Timecode (if have_timecodes).
172    pub timecode: u32,
173    /// Whether this is the last frame.
174    pub is_last: bool,
175    /// LF level for LfFrame (frame_type=1). Written as u2S(1,2,3,4).
176    /// Only meaningful when frame_type == LfFrame. Typically 1 for DC frames.
177    pub lf_level: u32,
178    /// Enable gaborish (Gabor-like blur in decoder loop filter).
179    pub gaborish: bool,
180    /// Number of EPF (Edge-Preserving Filter) iterations (0-3).
181    pub epf_iters: u32,
182}
183
184impl Default for FrameHeader {
185    fn default() -> Self {
186        Self {
187            frame_type: FrameType::Regular,
188            encoding: Encoding::VarDct,
189            xyb_encoded: true,
190            flags: 0,
191            do_ycbcr: false,
192            jpeg_upsampling: [0; 3],
193            upsampling: 1,
194            ec_upsampling: Vec::new(),
195            group_size_shift: 1,
196            x_qm_scale: 2,
197            b_qm_scale: 2,
198            num_passes: 1,
199            pass_shifts: Vec::new(),
200            num_ds: 0,
201            ds_downsample: Vec::new(),
202            ds_last_pass: Vec::new(),
203            x0: 0,
204            y0: 0,
205            width: 0,
206            height: 0,
207            blend_mode: BlendMode::Replace,
208            blend_source: 0,
209            ec_blend_modes: Vec::new(),
210            alpha_blend_channel: 0,
211            save_as_reference: 0,
212            save_before_ct: false,
213            name: String::new(),
214            have_animation: false,
215            have_timecodes: false,
216            duration: 0,
217            timecode: 0,
218            is_last: true,
219            lf_level: 0,
220            gaborish: true,
221            epf_iters: 2,
222        }
223    }
224}
225
226impl FrameHeader {
227    /// Creates a frame header for a lossy VarDCT frame with default parameters.
228    ///
229    /// Defaults: xyb_encoded=true, flags=SKIP_ADAPTIVE_LF_SMOOTHING (0x80),
230    /// gaborish=true, epf_iters=2.
231    pub fn lossy() -> Self {
232        Self {
233            encoding: Encoding::VarDct,
234            xyb_encoded: true,
235            flags: 0x80, // SKIP_ADAPTIVE_LF_SMOOTHING
236            gaborish: true,
237            epf_iters: 2,
238            ..Default::default()
239        }
240    }
241
242    /// Creates a frame header for a lossless Modular frame.
243    ///
244    /// Defaults: xyb_encoded=false, do_ycbcr=false, flags=0,
245    /// group_size_shift=1 (256), gaborish=false, epf_iters=0.
246    pub fn lossless() -> Self {
247        Self {
248            encoding: Encoding::Modular,
249            xyb_encoded: false,
250            do_ycbcr: false,
251            flags: 0,
252            group_size_shift: 1,
253            gaborish: false,
254            epf_iters: 0,
255            ..Default::default()
256        }
257    }
258
259    /// Creates a frame header for an LF (DC) frame.
260    ///
261    /// LfFrames contain DC coefficients at 1/8 resolution, encoded as modular.
262    /// Uses xyb_encoded=true, group_size_shift=1 (256), no loop filter.
263    /// The `width` and `height` are the DC frame dimensions (xsize_blocks × ysize_blocks).
264    pub fn lf_frame(width: u32, height: u32, lf_level: u32) -> Self {
265        Self {
266            frame_type: FrameType::LfFrame,
267            encoding: Encoding::Modular,
268            xyb_encoded: true,
269            flags: SKIP_ADAPTIVE_LF_SMOOTHING,
270            gaborish: false,
271            epf_iters: 0,
272            is_last: false,
273            save_before_ct: false,
274            width,
275            height,
276            lf_level,
277            group_size_shift: 1, // 256
278            ..Default::default()
279        }
280    }
281
282    /// Writes the frame header to the bitstream.
283    ///
284    /// Follows the JXL codestream specification (ISO 18181-1) Table A.2.
285    pub fn write(&self, writer: &mut BitWriter) -> Result<()> {
286        // all_default: true only when all fields match the decoder's default
287        // VarDCT default frame: Regular, VarDCT, no flags, do_ycbcr=true,
288        // upsampling=1, group_size_shift=1, x/b_qm_scale=2, 1 pass,
289        // no crop, Replace blend, is_last=true, no name, gab+epf2
290        let all_default = self.is_all_default();
291        writer.write_bit(all_default)?;
292        if all_default {
293            return Ok(());
294        }
295
296        // frame_type
297        writer.write(2, self.frame_type as u64)?;
298
299        // encoding
300        writer.write(1, self.encoding as u64)?;
301
302        // flags (U64)
303        writer.write_u64_coder(self.flags)?;
304
305        // do_ycbcr: only present when xyb_encoded is false
306        if !self.xyb_encoded {
307            writer.write_bit(self.do_ycbcr)?;
308        }
309
310        // jpeg_upsampling: only for VarDCT with YCbCr (when do_ycbcr and !xyb_encoded)
311        if self.encoding == Encoding::VarDct && self.do_ycbcr && !self.xyb_encoded {
312            for &up in &self.jpeg_upsampling {
313                writer.write(2, up as u64)?;
314            }
315        }
316
317        // upsampling, ec_upsampling: not present when USE_LF_FRAME flag is set
318        // (jxl-rs frame_header.rs:288-300)
319        if self.flags & USE_LF_FRAME == 0 {
320            // upsampling (U32: 1, 2, 4, 8)
321            writer.write_u32_coder(self.upsampling, 1, 2, 4, 8, 0)?;
322
323            // ec_upsampling per extra channel
324            for &ecu in &self.ec_upsampling {
325                writer.write_u32_coder(ecu, 1, 2, 4, 8, 0)?;
326            }
327        }
328
329        // group_size_shift: Modular only (VarDCT uses fixed 256x256 groups)
330        if self.encoding == Encoding::Modular {
331            writer.write(2, self.group_size_shift as u64)?;
332        }
333
334        // x_qm_scale, b_qm_scale: VarDCT + xyb_encoded only
335        if self.encoding == Encoding::VarDct && self.xyb_encoded {
336            writer.write(3, self.x_qm_scale as u64)?;
337            writer.write(3, self.b_qm_scale as u64)?;
338        }
339
340        // num_passes: not present for ReferenceOnly frames (libjxl spec)
341        if self.frame_type != FrameType::ReferenceOnly {
342            // num_passes (U32: 1, 2, 3, 4+u(3))
343            writer.write_u32_coder(self.num_passes, 1, 2, 3, 4, 3)?;
344            if self.num_passes != 1 {
345                self.write_passes(writer)?;
346            }
347        }
348
349        // lf_level: only for LfFrame, written as u2S(1, 2, 3, 4)
350        // (jxl-rs frame_header.rs:321-324, after passes, before have_crop)
351        if self.frame_type == FrameType::LfFrame {
352            writer.write_u32_coder(self.lf_level, 1, 2, 3, 4, 0)?;
353        }
354
355        // have_crop: present for all frame types except LfFrame
356        if self.frame_type != FrameType::LfFrame {
357            let have_crop = self.x0 != 0 || self.y0 != 0 || self.width != 0 || self.height != 0;
358            writer.write_bit(have_crop)?;
359            if have_crop {
360                // x0, y0: only for Regular/SkipProgressive frames (not ReferenceOnly)
361                if self.frame_type != FrameType::ReferenceOnly {
362                    self.write_crop_origin(writer)?;
363                }
364                // width, height: always present when have_crop
365                Self::write_crop_u32(writer, self.width)?;
366                Self::write_crop_u32(writer, self.height)?;
367            }
368        }
369
370        // blending_info (for Regular or SkipProgressive frames)
371        let normal_frame =
372            self.frame_type == FrameType::Regular || self.frame_type == FrameType::SkipProgressive;
373        if normal_frame {
374            self.write_blending_info(writer)?;
375        }
376
377        // ec_blending_info per extra channel
378        for &mode in &self.ec_blend_modes {
379            self.write_ec_blending_info(mode, writer)?;
380        }
381
382        // duration and timecode (for animated normal frames)
383        if normal_frame && self.have_animation {
384            // duration: U32(Val(0), Val(1), Bits(8), Bits(32))
385            match self.duration {
386                0 => writer.write(2, 0)?,
387                1 => writer.write(2, 1)?,
388                d if d <= 255 => {
389                    writer.write(2, 2)?;
390                    writer.write(8, d as u64)?;
391                }
392                d => {
393                    writer.write(2, 3)?;
394                    writer.write(32, d as u64)?;
395                }
396            }
397            if self.have_timecodes {
398                writer.write(32, self.timecode as u64)?;
399            }
400        }
401
402        // is_last (for Regular or SkipProgressive)
403        if normal_frame {
404            writer.write_bit(self.is_last)?;
405        }
406
407        // save_as_reference (only when !is_last and not LfFrame)
408        if !self.is_last && self.frame_type != FrameType::LfFrame {
409            writer.write(2, self.save_as_reference as u64)?;
410
411            // save_before_ct has two independent conditions (libjxl spec):
412            // 1. ReferenceOnly frames: ALWAYS present (default true)
413            // 2. Normal frames that reset canvas and can be referenced: present (default false)
414            if self.frame_type == FrameType::ReferenceOnly {
415                writer.write_bit(self.save_before_ct)?;
416            } else {
417                let full_frame =
418                    self.x0 == 0 && self.y0 == 0 && self.width == 0 && self.height == 0;
419                let resets_canvas = self.blend_mode == BlendMode::Replace && full_frame;
420                let can_be_referenced = self.duration == 0 || self.save_as_reference != 0;
421                if resets_canvas && can_be_referenced && normal_frame {
422                    writer.write_bit(self.save_before_ct)?;
423                }
424            }
425        }
426
427        // name
428        self.write_name(writer)?;
429
430        // restoration_filter (loop filter)
431        self.write_loop_filter(writer)?;
432
433        // frame header extensions (U64, always 0 for now)
434        writer.write_u64_coder(0)?;
435
436        Ok(())
437    }
438
439    /// Writes crop information.
440    ///
441    /// Crop dimensions use U32(Bits(8), Bits(11)+256, Bits(14)+2048, Bits(30)+18432).
442    /// x0/y0 are packed-signed first, then encoded with the same distribution.
443    /// Writes crop origin (x0, y0) as UnpackSigned values.
444    fn write_crop_origin(&self, writer: &mut BitWriter) -> Result<()> {
445        let x0u = if self.x0 >= 0 {
446            (self.x0 as u32) << 1
447        } else {
448            (((-self.x0 - 1) as u32) << 1) | 1
449        };
450        let y0u = if self.y0 >= 0 {
451            (self.y0 as u32) << 1
452        } else {
453            (((-self.y0 - 1) as u32) << 1) | 1
454        };
455        Self::write_crop_u32(writer, x0u)?;
456        Self::write_crop_u32(writer, y0u)?;
457        Ok(())
458    }
459
460    /// Encodes a single crop dimension value using U32(Bits(8), Bits(11)+256, Bits(14)+2304, Bits(30)+18688).
461    fn write_crop_u32(writer: &mut BitWriter, value: u32) -> Result<()> {
462        if value < 256 {
463            writer.write(2, 0)?; // selector 0: Bits(8)
464            writer.write(8, value as u64)?;
465        } else if value < 2304 {
466            writer.write(2, 1)?; // selector 1: Bits(11)+256
467            writer.write(11, (value - 256) as u64)?;
468        } else if value < 18688 {
469            writer.write(2, 2)?; // selector 2: Bits(14)+2304
470            writer.write(14, (value - 2304) as u64)?;
471        } else {
472            writer.write(2, 3)?; // selector 3: Bits(30)+18688
473            writer.write(30, (value - 18688) as u64)?;
474        }
475        Ok(())
476    }
477
478    /// Writes blending information for the main frame.
479    fn write_blending_info(&self, writer: &mut BitWriter) -> Result<()> {
480        writer.write_u32_coder(self.blend_mode as u32, 0, 1, 2, 3, 2)?;
481
482        // source: only when not (full_frame && Replace)
483        // Full frame is the default (no crop), so source is written for non-Replace modes.
484        let full_frame = self.x0 == 0 && self.y0 == 0 && self.width == 0 && self.height == 0;
485        if !(full_frame && self.blend_mode == BlendMode::Replace) {
486            writer.write(2, self.blend_source as u64)?;
487        }
488
489        if self.blend_mode == BlendMode::Blend || self.blend_mode == BlendMode::AlphaWeightedAdd {
490            writer.write_u32_coder(self.alpha_blend_channel, 0, 1, 2, 3, 3)?;
491            writer.write_bit(false)?; // clamp = false
492        }
493
494        Ok(())
495    }
496
497    /// Writes blending information for an extra channel.
498    fn write_ec_blending_info(&self, mode: BlendMode, writer: &mut BitWriter) -> Result<()> {
499        writer.write_u32_coder(mode as u32, 0, 1, 2, 3, 2)?;
500
501        let full_frame = self.x0 == 0 && self.y0 == 0 && self.width == 0 && self.height == 0;
502        if !(full_frame && mode == BlendMode::Replace) {
503            writer.write(2, 0)?; // source = 0
504        }
505
506        if mode == BlendMode::Blend || mode == BlendMode::AlphaWeightedAdd {
507            writer.write_u32_coder(0, 0, 1, 2, 3, 3)?; // alpha channel = 0
508            writer.write_bit(false)?; // clamp = false
509        }
510
511        Ok(())
512    }
513
514    /// Writes the frame name.
515    fn write_name(&self, writer: &mut BitWriter) -> Result<()> {
516        let name_len = self.name.len() as u32;
517        if name_len == 0 {
518            writer.write(2, 0)?; // selector 0 = length 0
519        } else if name_len < 4 {
520            writer.write(2, 0)?; // selector 0 (length encoded as 0, but name bytes follow)
521        } else if name_len < 20 {
522            writer.write(2, 2)?;
523            writer.write(4, (name_len - 4) as u64)?;
524        } else {
525            writer.write(2, 3)?;
526            writer.write(10, (name_len - 20) as u64)?;
527        }
528        for byte in self.name.bytes() {
529            writer.write(8, byte as u64)?;
530        }
531        Ok(())
532    }
533
534    /// Writes the loop filter (restoration_filter) section.
535    fn write_loop_filter(&self, writer: &mut BitWriter) -> Result<()> {
536        // all_default means gab=true, epf_iters=2 (decoder defaults)
537        let lf_all_default = self.gaborish && self.epf_iters == 2;
538
539        writer.write_bit(lf_all_default)?;
540        if lf_all_default {
541            return Ok(());
542        }
543
544        // gab
545        writer.write_bit(self.gaborish)?;
546        if self.gaborish {
547            writer.write_bit(false)?; // gab_custom = false (use default weights)
548        }
549
550        // epf_iters
551        writer.write(2, self.epf_iters as u64)?;
552
553        // EPF custom parameters (only when epf_iters > 0)
554        if self.epf_iters > 0 {
555            writer.write_bit(false)?; // epf_sharp_custom = false
556            writer.write_bit(false)?; // epf_weight_custom = false
557            writer.write_bit(false)?; // epf_sigma_custom = false
558        }
559
560        // loop filter extensions (U64)
561        writer.write_u64_coder(0)?;
562
563        Ok(())
564    }
565
566    /// Writes the Passes struct when num_passes > 1.
567    ///
568    /// Format (from jxl-rs decoder):
569    /// - num_ds: u2S(0, 1, 2, Bits(1)+3)
570    /// - shift[0..num_passes-1]: Bits(2) each
571    /// - downsample[0..num_ds]: u2S(1, 2, 4, 8)
572    /// - last_pass[0..num_ds]: u2S(0, 1, 2, Bits(3))
573    fn write_passes(&self, writer: &mut BitWriter) -> Result<()> {
574        // num_ds: u2S(0, 1, 2, Bits(1)+3)
575        writer.write_u32_coder(self.num_ds, 0, 1, 2, 3, 1)?;
576
577        // shift[0..num_passes-1]: Bits(2) each
578        for i in 0..self.num_passes.saturating_sub(1) as usize {
579            let shift = self.pass_shifts.get(i).copied().unwrap_or(0);
580            writer.write(2, shift as u64)?;
581        }
582
583        // downsample[0..num_ds]: u2S(1, 2, 4, 8)
584        for i in 0..self.num_ds as usize {
585            let ds = self.ds_downsample.get(i).copied().unwrap_or(1);
586            writer.write_u32_coder(ds, 1, 2, 4, 8, 0)?;
587        }
588
589        // last_pass[0..num_ds]: u2S(0, 1, 2, Bits(3))
590        for i in 0..self.num_ds as usize {
591            let lp = self.ds_last_pass.get(i).copied().unwrap_or(0);
592            writer.write_u32_coder(lp, 0, 1, 2, 0, 3)?;
593        }
594
595        Ok(())
596    }
597
598    /// Returns true if all fields match the decoder's "all_default" frame header.
599    ///
600    /// The all_default frame header is: Regular VarDCT, no flags, do_ycbcr=true,
601    /// upsampling=1, group_size_shift=1, x/b_qm_scale=2, 1 pass, no crop,
602    /// Replace blend, is_last=true, no name, default loop filter (gab+epf2).
603    fn is_all_default(&self) -> bool {
604        self.frame_type == FrameType::Regular
605            && self.encoding == Encoding::VarDct
606            && self.xyb_encoded
607            && self.flags == 0
608            && self.do_ycbcr
609            && self.upsampling == 1
610            && self.ec_upsampling.is_empty()
611            && self.ec_blend_modes.is_empty()
612            && self.group_size_shift == 1
613            && self.x_qm_scale == 2
614            && self.b_qm_scale == 2
615            && self.num_passes == 1
616            && self.pass_shifts.is_empty()
617            && self.x0 == 0
618            && self.y0 == 0
619            && self.width == 0
620            && self.height == 0
621            && self.blend_mode == BlendMode::Replace
622            && self.blend_source == 0
623            && self.save_as_reference == 0
624            && !self.save_before_ct
625            && self.name.is_empty()
626            && !self.have_animation
627            && self.is_last
628            && self.gaborish
629            && self.epf_iters == 2
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn test_default_frame() {
639        let frame = FrameHeader::lossy();
640        let mut writer = BitWriter::new();
641        frame.write(&mut writer).unwrap();
642    }
643
644    #[test]
645    fn test_lossless_frame() {
646        let frame = FrameHeader::lossless();
647        assert_eq!(frame.encoding, Encoding::Modular);
648        assert!(!frame.do_ycbcr);
649        assert!(!frame.gaborish);
650        assert_eq!(frame.epf_iters, 0);
651
652        let mut writer = BitWriter::new();
653        frame.write(&mut writer).unwrap();
654        assert!(writer.bits_written() > 0);
655    }
656
657    #[test]
658    fn test_frame_type_values() {
659        assert_eq!(FrameType::Regular as u8, 0);
660        assert_eq!(FrameType::LfFrame as u8, 1);
661        assert_eq!(FrameType::ReferenceOnly as u8, 2);
662        assert_eq!(FrameType::SkipProgressive as u8, 3);
663    }
664
665    #[test]
666    fn test_encoding_values() {
667        assert_eq!(Encoding::VarDct as u8, 0);
668        assert_eq!(Encoding::Modular as u8, 1);
669    }
670
671    #[test]
672    fn test_blend_mode_values() {
673        assert_eq!(BlendMode::Replace as u8, 0);
674        assert_eq!(BlendMode::Add as u8, 1);
675        assert_eq!(BlendMode::Blend as u8, 2);
676        assert_eq!(BlendMode::AlphaWeightedAdd as u8, 3);
677        assert_eq!(BlendMode::Mul as u8, 4);
678    }
679
680    #[test]
681    fn test_frame_with_crop() {
682        let mut frame = FrameHeader::lossy();
683        frame.x0 = 0;
684        frame.y0 = 0;
685        frame.width = 20000;
686        frame.height = 20000;
687
688        let mut writer = BitWriter::new();
689        frame.write(&mut writer).unwrap();
690        assert!(writer.bits_written() > 10);
691    }
692
693    #[test]
694    fn test_frame_with_large_crop_offset() {
695        let mut frame = FrameHeader::lossy();
696        frame.x0 = 128;
697        frame.y0 = 128;
698        frame.width = 20000;
699        frame.height = 20000;
700
701        let mut writer = BitWriter::new();
702        frame.write(&mut writer).unwrap();
703        assert!(writer.bits_written() > 10);
704    }
705
706    #[test]
707    fn test_frame_with_name() {
708        let mut frame = FrameHeader::lossy();
709        frame.name = "TestFrame".to_string();
710
711        let mut writer = BitWriter::new();
712        frame.write(&mut writer).unwrap();
713        assert!(writer.bits_written() > 80);
714    }
715
716    #[test]
717    fn test_frame_with_long_name() {
718        let mut frame = FrameHeader::lossy();
719        frame.name = "ThisIsAVeryLongFrameName".to_string();
720
721        let mut writer = BitWriter::new();
722        frame.write(&mut writer).unwrap();
723        assert!(writer.bits_written() > 200);
724    }
725
726    #[test]
727    fn test_lf_frame_type() {
728        // Use the dedicated constructor which sets lf_level correctly
729        let frame = FrameHeader::lf_frame(32, 32, 1);
730
731        let mut writer = BitWriter::new();
732        frame.write(&mut writer).unwrap();
733        assert!(writer.bits_written() > 0);
734    }
735
736    #[test]
737    fn test_reference_only_frame() {
738        let mut frame = FrameHeader::lossy();
739        frame.frame_type = FrameType::ReferenceOnly;
740
741        let mut writer = BitWriter::new();
742        frame.write(&mut writer).unwrap();
743        assert!(writer.bits_written() > 0);
744    }
745
746    #[test]
747    fn test_skip_progressive_frame() {
748        let mut frame = FrameHeader::lossy();
749        frame.frame_type = FrameType::SkipProgressive;
750
751        let mut writer = BitWriter::new();
752        frame.write(&mut writer).unwrap();
753        assert!(writer.bits_written() > 0);
754    }
755
756    #[test]
757    fn test_blend_mode_add() {
758        let mut frame = FrameHeader::lossy();
759        frame.blend_mode = BlendMode::Add;
760
761        let mut writer = BitWriter::new();
762        frame.write(&mut writer).unwrap();
763        assert!(writer.bits_written() > 0);
764    }
765
766    #[test]
767    fn test_blend_mode_blend_with_alpha() {
768        let mut frame = FrameHeader::lossy();
769        frame.blend_mode = BlendMode::Blend;
770        frame.alpha_blend_channel = 1;
771
772        let mut writer = BitWriter::new();
773        frame.write(&mut writer).unwrap();
774        assert!(writer.bits_written() > 0);
775    }
776
777    #[test]
778    fn test_blend_mode_alpha_weighted_add() {
779        let mut frame = FrameHeader::lossy();
780        frame.blend_mode = BlendMode::AlphaWeightedAdd;
781        frame.alpha_blend_channel = 2;
782
783        let mut writer = BitWriter::new();
784        frame.write(&mut writer).unwrap();
785        assert!(writer.bits_written() > 0);
786    }
787
788    #[test]
789    fn test_blend_mode_mul() {
790        let mut frame = FrameHeader::lossy();
791        frame.blend_mode = BlendMode::Mul;
792
793        let mut writer = BitWriter::new();
794        frame.write(&mut writer).unwrap();
795        assert!(writer.bits_written() > 0);
796    }
797
798    #[test]
799    fn test_upsampling_factors() {
800        for upsampling in [1, 2, 4, 8] {
801            let mut frame = FrameHeader::lossy();
802            frame.upsampling = upsampling;
803
804            let mut writer = BitWriter::new();
805            frame.write(&mut writer).unwrap();
806            assert!(writer.bits_written() > 0);
807        }
808    }
809
810    #[test]
811    fn test_ec_upsampling() {
812        let mut frame = FrameHeader::lossy();
813        frame.ec_upsampling = vec![2, 4, 8];
814
815        let mut writer = BitWriter::new();
816        frame.write(&mut writer).unwrap();
817        assert!(writer.bits_written() > 0);
818    }
819
820    #[test]
821    fn test_group_size_shift() {
822        for shift in 0..4 {
823            let mut frame = FrameHeader::lossless();
824            frame.group_size_shift = shift;
825
826            let mut writer = BitWriter::new();
827            frame.write(&mut writer).unwrap();
828            assert!(writer.bits_written() > 0);
829        }
830    }
831
832    #[test]
833    fn test_save_as_reference() {
834        let mut frame = FrameHeader::lossy();
835        frame.save_as_reference = 2;
836        frame.is_last = false; // save_as_reference only written when !is_last
837
838        let mut writer = BitWriter::new();
839        frame.write(&mut writer).unwrap();
840        assert!(writer.bits_written() > 0);
841    }
842
843    #[test]
844    fn test_not_last_frame() {
845        let mut frame = FrameHeader::lossy();
846        frame.is_last = false;
847
848        let mut writer = BitWriter::new();
849        frame.write(&mut writer).unwrap();
850        assert!(writer.bits_written() > 0);
851    }
852
853    #[test]
854    fn test_vardct_loop_filter_all_default() {
855        // gab=true, epf=2 → all_default for loop filter
856        let frame = FrameHeader::lossy();
857        assert!(frame.gaborish && frame.epf_iters == 2);
858
859        let mut writer = BitWriter::new();
860        frame.write(&mut writer).unwrap();
861    }
862
863    #[test]
864    fn test_vardct_no_gaborish() {
865        let mut frame = FrameHeader::lossy();
866        frame.gaborish = false;
867        frame.epf_iters = 1;
868
869        let mut writer = BitWriter::new();
870        frame.write(&mut writer).unwrap();
871        assert!(writer.bits_written() > 0);
872    }
873
874    #[test]
875    fn test_vardct_no_epf() {
876        let mut frame = FrameHeader::lossy();
877        frame.gaborish = true;
878        frame.epf_iters = 0;
879
880        let mut writer = BitWriter::new();
881        frame.write(&mut writer).unwrap();
882        assert!(writer.bits_written() > 0);
883    }
884
885    #[test]
886    fn test_vardct_with_noise() {
887        let mut frame = FrameHeader::lossy();
888        frame.flags = 0x80 | 0x01; // SKIP_LF_SMOOTHING + ENABLE_NOISE
889
890        let mut writer = BitWriter::new();
891        frame.write(&mut writer).unwrap();
892        assert!(writer.bits_written() > 0);
893    }
894
895    #[test]
896    fn test_vardct_custom_qm_scale() {
897        let mut frame = FrameHeader::lossy();
898        frame.x_qm_scale = 5;
899        frame.b_qm_scale = 4;
900
901        let mut writer = BitWriter::new();
902        frame.write(&mut writer).unwrap();
903        assert!(writer.bits_written() > 0);
904    }
905
906    #[test]
907    fn test_vardct_with_extra_channels() {
908        let mut frame = FrameHeader::lossy();
909        frame.ec_upsampling = vec![1]; // one extra channel, no upsampling
910        frame.ec_blend_modes = vec![BlendMode::Replace];
911
912        let mut writer = BitWriter::new();
913        frame.write(&mut writer).unwrap();
914        assert!(writer.bits_written() > 0);
915    }
916
917    #[test]
918    fn test_lossless_with_extra_channels() {
919        let mut frame = FrameHeader::lossless();
920        frame.ec_upsampling = vec![1]; // one extra channel, no upsampling
921        frame.ec_blend_modes = vec![BlendMode::Replace];
922
923        let mut writer = BitWriter::new();
924        frame.write(&mut writer).unwrap();
925        assert!(writer.bits_written() > 0);
926    }
927
928    /// Verify that our VarDCT frame header matches the old hand-written write_frame_header()
929    /// bit for bit. Parameters: x_qm=3, b_qm=2, epf=1, noise=false, gab=true, 0 extra channels.
930    #[test]
931    fn test_vardct_bit_exact_vs_old() {
932        // Old path equivalent:
933        // flags = 128 (0x80), x_qm=3, b_qm=2, epf=1, gab=true, 0 extra channels
934        let mut old_writer = BitWriter::new();
935        // Manually replicate the old write_frame_header():
936        old_writer.write(1, 0).unwrap(); // not all_default
937        old_writer.write(2, 0).unwrap(); // RegularFrame
938        old_writer.write(1, 0).unwrap(); // VarDCT
939        old_writer.write(2, 2).unwrap(); // flags U64 selector 2
940        old_writer.write(8, 128 - 17).unwrap(); // flags = 128
941        old_writer.write(2, 0).unwrap(); // upsampling = 1
942        old_writer.write(3, 3).unwrap(); // x_qm_scale
943        old_writer.write(3, 2).unwrap(); // b_qm_scale
944        old_writer.write(2, 0).unwrap(); // num_passes = 1
945        old_writer.write(1, 0).unwrap(); // have_crop = false
946        old_writer.write(2, 0).unwrap(); // blend = Replace
947        old_writer.write(1, 1).unwrap(); // is_last
948        old_writer.write(2, 0).unwrap(); // name = ""
949        // Loop filter: not all_default (gab=true but epf=1, not 2)
950        old_writer.write(1, 0).unwrap(); // lf not all_default
951        old_writer.write(1, 1).unwrap(); // gab = true
952        old_writer.write(1, 0).unwrap(); // gab_custom = false
953        old_writer.write(2, 1).unwrap(); // epf_iters = 1
954        old_writer.write(1, 0).unwrap(); // epf_sharp_custom = false
955        old_writer.write(1, 0).unwrap(); // epf_weight_custom = false
956        old_writer.write(1, 0).unwrap(); // epf_sigma_custom = false
957        old_writer.write(2, 0).unwrap(); // lf_extensions = 0
958        old_writer.write(2, 0).unwrap(); // frame_extensions = 0
959
960        let mut new_writer = BitWriter::new();
961        let mut frame = FrameHeader::lossy();
962        frame.x_qm_scale = 3;
963        frame.b_qm_scale = 2;
964        frame.epf_iters = 1;
965        frame.write(&mut new_writer).unwrap();
966
967        // Compare bit counts (writers may not be byte-aligned)
968        assert_eq!(
969            old_writer.bits_written(),
970            new_writer.bits_written(),
971            "VarDCT frame header bit count should match"
972        );
973        // Pad and compare bytes
974        old_writer.zero_pad_to_byte();
975        new_writer.zero_pad_to_byte();
976        assert_eq!(
977            old_writer.finish(),
978            new_writer.finish(),
979            "VarDCT frame header should be bit-exact"
980        );
981    }
982
983    /// Verify VarDCT with gab=true, epf=2 (loop filter all_default).
984    #[test]
985    fn test_vardct_lf_all_default_bit_exact() {
986        let mut old_writer = BitWriter::new();
987        old_writer.write(1, 0).unwrap(); // not all_default
988        old_writer.write(2, 0).unwrap(); // RegularFrame
989        old_writer.write(1, 0).unwrap(); // VarDCT
990        old_writer.write(2, 2).unwrap(); // flags U64 selector 2
991        old_writer.write(8, 128 - 17).unwrap(); // flags = 128
992        old_writer.write(2, 0).unwrap(); // upsampling = 1
993        old_writer.write(3, 3).unwrap(); // x_qm_scale
994        old_writer.write(3, 2).unwrap(); // b_qm_scale
995        old_writer.write(2, 0).unwrap(); // num_passes = 1
996        old_writer.write(1, 0).unwrap(); // have_crop = false
997        old_writer.write(2, 0).unwrap(); // blend = Replace
998        old_writer.write(1, 1).unwrap(); // is_last
999        old_writer.write(2, 0).unwrap(); // name = ""
1000        old_writer.write(1, 1).unwrap(); // lf all_default
1001        old_writer.write(2, 0).unwrap(); // frame_extensions = 0
1002
1003        let mut new_writer = BitWriter::new();
1004        let mut frame = FrameHeader::lossy();
1005        frame.x_qm_scale = 3;
1006        frame.b_qm_scale = 2;
1007        frame.gaborish = true;
1008        frame.epf_iters = 2;
1009        frame.write(&mut new_writer).unwrap();
1010
1011        assert_eq!(
1012            old_writer.bits_written(),
1013            new_writer.bits_written(),
1014            "VarDCT lf all_default bit count should match"
1015        );
1016        old_writer.zero_pad_to_byte();
1017        new_writer.zero_pad_to_byte();
1018        assert_eq!(
1019            old_writer.finish(),
1020            new_writer.finish(),
1021            "VarDCT with lf all_default should be bit-exact"
1022        );
1023    }
1024}