1use crate::error::{CodecError, CodecResult};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Ffv1Version {
14 V0,
16 V1,
18 V2,
20 V3,
22}
23
24impl Ffv1Version {
25 pub fn from_u8(v: u8) -> CodecResult<Self> {
27 match v {
28 0 => Ok(Self::V0),
29 1 => Ok(Self::V1),
30 2 => Ok(Self::V2),
31 3 => Ok(Self::V3),
32 _ => Err(CodecError::InvalidParameter(format!(
33 "unsupported FFV1 version: {v}"
34 ))),
35 }
36 }
37
38 #[must_use]
40 pub const fn as_u8(self) -> u8 {
41 match self {
42 Self::V0 => 0,
43 Self::V1 => 1,
44 Self::V2 => 2,
45 Self::V3 => 3,
46 }
47 }
48
49 #[must_use]
51 pub const fn uses_range_coder(self) -> bool {
52 matches!(self, Self::V3 | Self::V2)
53 }
54
55 #[must_use]
57 pub const fn supports_ec(self) -> bool {
58 matches!(self, Self::V3)
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum Ffv1Colorspace {
65 YCbCr,
67 Rgb,
69}
70
71impl Ffv1Colorspace {
72 pub fn from_u8(v: u8) -> CodecResult<Self> {
74 match v {
75 0 => Ok(Self::YCbCr),
76 1 => Ok(Self::Rgb),
77 _ => Err(CodecError::InvalidParameter(format!(
78 "unsupported FFV1 colorspace: {v}"
79 ))),
80 }
81 }
82
83 #[must_use]
85 pub const fn as_u8(self) -> u8 {
86 match self {
87 Self::YCbCr => 0,
88 Self::Rgb => 1,
89 }
90 }
91
92 #[must_use]
94 pub const fn plane_count(self) -> usize {
95 match self {
96 Self::YCbCr => 3,
97 Self::Rgb => 3,
99 }
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum Ffv1ChromaType {
106 Chroma420,
108 Chroma422,
110 Chroma444,
112}
113
114impl Ffv1ChromaType {
115 #[must_use]
117 pub const fn h_shift(self) -> u32 {
118 match self {
119 Self::Chroma420 | Self::Chroma422 => 1,
120 Self::Chroma444 => 0,
121 }
122 }
123
124 #[must_use]
126 pub const fn v_shift(self) -> u32 {
127 match self {
128 Self::Chroma420 => 1,
129 Self::Chroma422 | Self::Chroma444 => 0,
130 }
131 }
132
133 pub fn from_shifts(h_shift: u32, v_shift: u32) -> CodecResult<Self> {
135 match (h_shift, v_shift) {
136 (1, 1) => Ok(Self::Chroma420),
137 (1, 0) => Ok(Self::Chroma422),
138 (0, 0) => Ok(Self::Chroma444),
139 _ => Err(CodecError::InvalidParameter(format!(
140 "unsupported chroma subsampling: h_shift={h_shift}, v_shift={v_shift}"
141 ))),
142 }
143 }
144}
145
146#[derive(Clone, Debug)]
151pub struct Ffv1Config {
152 pub version: Ffv1Version,
154 pub width: u32,
156 pub height: u32,
158 pub colorspace: Ffv1Colorspace,
160 pub chroma_type: Ffv1ChromaType,
162 pub bits_per_raw_sample: u8,
164 pub num_h_slices: u32,
166 pub num_v_slices: u32,
168 pub ec: bool,
170 pub state_transition_delta: Vec<i16>,
172 pub range_coder_mode: bool,
175}
176
177impl Default for Ffv1Config {
178 fn default() -> Self {
179 Self {
180 version: Ffv1Version::V3,
181 width: 0,
182 height: 0,
183 colorspace: Ffv1Colorspace::YCbCr,
184 chroma_type: Ffv1ChromaType::Chroma420,
185 bits_per_raw_sample: 8,
186 num_h_slices: 1,
187 num_v_slices: 1,
188 ec: true,
189 state_transition_delta: Vec::new(),
190 range_coder_mode: true,
191 }
192 }
193}
194
195impl Ffv1Config {
196 #[must_use]
198 pub fn num_slices(&self) -> u32 {
199 self.num_h_slices * self.num_v_slices
200 }
201
202 #[must_use]
204 pub fn max_sample_value(&self) -> i32 {
205 (1i32 << self.bits_per_raw_sample) - 1
206 }
207
208 #[must_use]
210 pub fn plane_count(&self) -> usize {
211 self.colorspace.plane_count()
212 }
213
214 #[must_use]
216 pub fn plane_dimensions(&self, plane_index: usize) -> (u32, u32) {
217 if plane_index == 0 || self.colorspace == Ffv1Colorspace::Rgb {
218 (self.width, self.height)
219 } else {
220 let w =
221 (self.width + (1 << self.chroma_type.h_shift()) - 1) >> self.chroma_type.h_shift();
222 let h =
223 (self.height + (1 << self.chroma_type.v_shift()) - 1) >> self.chroma_type.v_shift();
224 (w, h)
225 }
226 }
227
228 pub fn validate(&self) -> CodecResult<()> {
230 if self.width == 0 || self.height == 0 {
231 return Err(CodecError::InvalidParameter(
232 "frame dimensions must be nonzero".to_string(),
233 ));
234 }
235 if !matches!(self.bits_per_raw_sample, 8 | 10 | 12 | 16) {
236 return Err(CodecError::InvalidParameter(format!(
237 "unsupported bits_per_raw_sample: {}",
238 self.bits_per_raw_sample
239 )));
240 }
241 if self.num_h_slices == 0 || self.num_v_slices == 0 {
242 return Err(CodecError::InvalidParameter(
243 "slice counts must be nonzero".to_string(),
244 ));
245 }
246 Ok(())
247 }
248}
249
250#[derive(Clone, Debug, Default)]
252pub struct SliceHeader {
253 pub slice_x: u32,
255 pub slice_y: u32,
257 pub slice_width: u32,
259 pub slice_height: u32,
261}
262
263pub const CONTEXT_COUNT: usize = 32;
266
267pub const INITIAL_STATE: u8 = 128;
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 #[ignore]
276 fn test_version_roundtrip() {
277 for v in [
278 Ffv1Version::V0,
279 Ffv1Version::V1,
280 Ffv1Version::V2,
281 Ffv1Version::V3,
282 ] {
283 let n = v.as_u8();
284 let parsed = Ffv1Version::from_u8(n).expect("valid version");
285 assert_eq!(parsed, v);
286 }
287 }
288
289 #[test]
290 #[ignore]
291 fn test_version_invalid() {
292 assert!(Ffv1Version::from_u8(4).is_err());
293 }
294
295 #[test]
296 #[ignore]
297 fn test_colorspace() {
298 assert_eq!(Ffv1Colorspace::YCbCr.plane_count(), 3);
299 assert_eq!(Ffv1Colorspace::Rgb.plane_count(), 3);
300 assert_eq!(Ffv1Colorspace::YCbCr.as_u8(), 0);
301 assert_eq!(Ffv1Colorspace::Rgb.as_u8(), 1);
302 }
303
304 #[test]
305 #[ignore]
306 fn test_chroma_type_shifts() {
307 assert_eq!(Ffv1ChromaType::Chroma420.h_shift(), 1);
308 assert_eq!(Ffv1ChromaType::Chroma420.v_shift(), 1);
309 assert_eq!(Ffv1ChromaType::Chroma422.h_shift(), 1);
310 assert_eq!(Ffv1ChromaType::Chroma422.v_shift(), 0);
311 assert_eq!(Ffv1ChromaType::Chroma444.h_shift(), 0);
312 assert_eq!(Ffv1ChromaType::Chroma444.v_shift(), 0);
313 }
314
315 #[test]
316 #[ignore]
317 fn test_chroma_type_from_shifts() {
318 assert_eq!(
319 Ffv1ChromaType::from_shifts(1, 1).expect("valid"),
320 Ffv1ChromaType::Chroma420
321 );
322 assert_eq!(
323 Ffv1ChromaType::from_shifts(1, 0).expect("valid"),
324 Ffv1ChromaType::Chroma422
325 );
326 assert_eq!(
327 Ffv1ChromaType::from_shifts(0, 0).expect("valid"),
328 Ffv1ChromaType::Chroma444
329 );
330 assert!(Ffv1ChromaType::from_shifts(2, 0).is_err());
331 }
332
333 #[test]
334 #[ignore]
335 fn test_config_plane_dimensions() {
336 let config = Ffv1Config {
337 width: 1920,
338 height: 1080,
339 chroma_type: Ffv1ChromaType::Chroma420,
340 ..Default::default()
341 };
342 assert_eq!(config.plane_dimensions(0), (1920, 1080));
343 assert_eq!(config.plane_dimensions(1), (960, 540));
344 assert_eq!(config.plane_dimensions(2), (960, 540));
345 }
346
347 #[test]
348 #[ignore]
349 fn test_config_validation() {
350 let mut config = Ffv1Config {
351 width: 1920,
352 height: 1080,
353 ..Default::default()
354 };
355 assert!(config.validate().is_ok());
356
357 config.width = 0;
358 assert!(config.validate().is_err());
359
360 config.width = 1920;
361 config.bits_per_raw_sample = 9;
362 assert!(config.validate().is_err());
363 }
364
365 #[test]
366 #[ignore]
367 fn test_max_sample_value() {
368 let config = Ffv1Config {
369 bits_per_raw_sample: 8,
370 ..Default::default()
371 };
372 assert_eq!(config.max_sample_value(), 255);
373
374 let config10 = Ffv1Config {
375 bits_per_raw_sample: 10,
376 ..Default::default()
377 };
378 assert_eq!(config10.max_sample_value(), 1023);
379 }
380}