1use gamut_av1::{EncodedStill, encode_still_intra, encode_still_lossless_identity};
4use gamut_color::Planar8;
5use gamut_core::{Dimensions, EncodeImage, ImageRef, Result, Rgb8};
6use gamut_isobmff::{Av1cConfig, AvifStillImage, ImageTransform, NclxColr, write_avif_still};
7
8#[derive(Debug, Clone, Copy, Default)]
16pub struct AvifEncoder {
17 qindex: u8,
19 transform: ImageTransform,
21}
22
23impl AvifEncoder {
24 #[must_use]
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 #[must_use]
33 pub fn with_qindex(mut self, qindex: u8) -> Self {
34 self.qindex = qindex;
35 self
36 }
37
38 #[must_use]
43 pub fn with_rotation_ccw(mut self, quarter_turns: u8) -> Self {
44 self.transform.rotation_ccw = quarter_turns % 4;
45 self
46 }
47
48 #[must_use]
52 pub fn with_mirror(mut self, axis: u8) -> Self {
53 self.transform.mirror_axis = Some(axis & 1);
54 self
55 }
56}
57
58fn build_avif(still: &EncodedStill, dims: Dimensions, transform: ImageTransform) -> Vec<u8> {
62 let c = &still.config;
63 let av1c = Av1cConfig {
64 seq_profile: c.seq_profile,
65 seq_level_idx_0: c.seq_level_idx_0,
66 seq_tier_0: c.seq_tier_0,
67 high_bitdepth: c.high_bitdepth,
68 twelve_bit: c.twelve_bit,
69 monochrome: c.monochrome,
70 chroma_subsampling_x: c.chroma_subsampling_x,
71 chroma_subsampling_y: c.chroma_subsampling_y,
72 chroma_sample_position: c.chroma_sample_position,
73 };
74 let nclx = NclxColr {
75 colour_primaries: c.color_primaries,
76 transfer_characteristics: c.transfer_characteristics,
77 matrix_coefficients: c.matrix_coefficients,
78 full_range: c.full_range,
79 };
80 let image = AvifStillImage {
81 width: dims.width,
82 height: dims.height,
83 bit_depth: 8,
84 num_channels: 3,
85 av1c,
86 nclx,
87 transform,
88 item_data: &still.obus,
89 };
90 write_avif_still(&image)
91}
92
93impl EncodeImage<Rgb8> for AvifEncoder {
94 fn encode_image(&self, image: ImageRef<'_, Rgb8>, out: &mut Vec<u8>) -> Result<usize> {
96 let dims = image.dimensions();
97 let planes = Planar8::from_rgb8_identity_view(image);
98 let still = if self.qindex == 0 {
99 encode_still_lossless_identity(&planes)?
100 } else {
101 encode_still_intra(&planes, self.qindex)?.0
102 };
103 let file = build_avif(&still, dims, self.transform);
104 out.extend_from_slice(&file);
105 Ok(file.len())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn encode(w: u32, h: u32) -> Vec<u8> {
114 let mut rgb = vec![0u8; (w * h * 3) as usize];
115 for (i, b) in rgb.iter_mut().enumerate() {
116 *b = (i * 37) as u8;
117 }
118 let mut out = Vec::new();
119 let dims = Dimensions {
120 width: w,
121 height: h,
122 };
123 AvifEncoder::new()
124 .encode_image(ImageRef::<Rgb8>::new(&rgb, dims).unwrap(), &mut out)
125 .unwrap();
126 out
127 }
128
129 #[test]
130 fn produces_valid_avif_container() {
131 let f = encode(40, 24);
132 assert_eq!(&f[4..8], b"ftyp");
133 for fourcc in [
134 b"meta", b"av1C", b"ispe", b"pixi", b"colr", b"mdat", b"av01",
135 ] {
136 assert!(f.windows(4).any(|w| w == fourcc), "missing box {fourcc:?}");
137 }
138 }
139
140 #[test]
141 fn lossy_produces_valid_avif_container() {
142 let mut rgb = vec![0u8; 48 * 32 * 3];
145 for (i, b) in rgb.iter_mut().enumerate() {
146 *b = (i * 29) as u8;
147 }
148 for q in [4u8, 40, 200] {
149 let mut out = Vec::new();
150 let n = AvifEncoder::new()
151 .with_qindex(q)
152 .encode_image(
153 ImageRef::<Rgb8>::new(
154 &rgb,
155 Dimensions {
156 width: 48,
157 height: 32,
158 },
159 )
160 .unwrap(),
161 &mut out,
162 )
163 .unwrap();
164 assert_eq!(n, out.len());
165 assert_eq!(&out[4..8], b"ftyp");
166 for fourcc in [b"meta", b"av1C", b"ispe", b"mdat", b"av01"] {
167 assert!(
168 out.windows(4).any(|w| w == fourcc),
169 "missing box {fourcc:?}"
170 );
171 }
172 }
173 }
174
175 #[test]
176 fn ispe_matches_dimensions() {
177 let (w, h) = (37u32, 19u32);
178 let f = encode(w, h);
179 let pos = f.windows(4).position(|x| x == b"ispe").unwrap();
180 let body = pos + 4 + 4; let rw = u32::from_be_bytes([f[body], f[body + 1], f[body + 2], f[body + 3]]);
182 let rh = u32::from_be_bytes([f[body + 4], f[body + 5], f[body + 6], f[body + 7]]);
183 assert_eq!((rw, rh), (w, h));
184 }
185
186 #[test]
187 fn rejects_wrong_length() {
188 let r = ImageRef::<Rgb8>::new(
190 &[0; 10],
191 Dimensions {
192 width: 4,
193 height: 4,
194 },
195 );
196 assert!(r.is_err());
197 }
198
199 #[test]
200 fn appends_without_clobbering() {
201 let mut out = vec![0xAA, 0xBB];
202 let rgb = vec![128u8; 4 * 4 * 3];
203 let n = AvifEncoder::new()
204 .encode_image(
205 ImageRef::<Rgb8>::new(
206 &rgb,
207 Dimensions {
208 width: 4,
209 height: 4,
210 },
211 )
212 .unwrap(),
213 &mut out,
214 )
215 .unwrap();
216 assert_eq!(out.len(), 2 + n);
217 assert_eq!(&out[0..2], &[0xAA, 0xBB]);
218 }
219
220 fn encode_with(enc: AvifEncoder, w: u32, h: u32) -> Vec<u8> {
221 let mut rgb = vec![0u8; (w * h * 3) as usize];
222 for (i, b) in rgb.iter_mut().enumerate() {
223 *b = (i * 37) as u8;
224 }
225 let mut out = Vec::new();
226 let dims = Dimensions {
227 width: w,
228 height: h,
229 };
230 enc.encode_image(ImageRef::<Rgb8>::new(&rgb, dims).unwrap(), &mut out)
231 .unwrap();
232 out
233 }
234
235 #[test]
236 fn with_rotation_ccw_emits_irot_and_normalizes_mod_four() {
237 let f = encode_with(AvifEncoder::new().with_rotation_ccw(1), 4, 4);
240 let p = f
241 .windows(4)
242 .position(|w| w == b"irot")
243 .expect("irot present");
244 assert_eq!(f[p + 4] & 0x03, 1, "irot angle = 1");
245 let f0 = encode_with(AvifEncoder::new().with_rotation_ccw(4), 4, 4);
247 assert!(
248 !f0.windows(4).any(|w| w == b"irot"),
249 "rotation 4 ≡ 0 ⇒ no irot"
250 );
251 }
252
253 #[test]
254 fn with_mirror_emits_imir_axis() {
255 for axis in [0u8, 1] {
256 let f = encode_with(AvifEncoder::new().with_mirror(axis), 4, 4);
257 let p = f
258 .windows(4)
259 .position(|w| w == b"imir")
260 .expect("imir present");
261 assert_eq!(f[p + 4] & 0x01, axis, "imir axis = {axis}");
262 assert!(!f.windows(4).any(|w| w == b"irot"), "mirror only ⇒ no irot");
263 }
264 }
265}