1use otf_pixels_core::{PixelsError, Result, Source};
16
17pub const SIGNATURE_87A: [u8; 6] = *b"GIF87a";
19pub const SIGNATURE_89A: [u8; 6] = *b"GIF89a";
21
22pub mod label {
24 pub const EXTENSION: u8 = 0x21;
26 pub const IMAGE: u8 = 0x2C;
28 pub const TRAILER: u8 = 0x3B;
30
31 pub const GRAPHIC_CONTROL: u8 = 0xF9;
33 }
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[non_exhaustive]
45pub struct Screen {
46 pub width: u16,
48 pub height: u16,
50 pub global_table_size: usize,
52 pub background: u8,
54}
55
56impl Screen {
57 pub fn parse(bytes: &[u8; 7]) -> Result<Self> {
63 let width = u16::from_le_bytes([bytes[0], bytes[1]]);
64 let height = u16::from_le_bytes([bytes[2], bytes[3]]);
65 let packed = bytes[4];
66 let background = bytes[5];
67 if width == 0 || height == 0 {
71 return Err(PixelsError::malformed(
72 "gif",
73 format!("logical screen is {width}x{height}"),
74 ));
75 }
76
77 let global_table_size = if packed & 0x80 != 0 {
80 1_usize << ((packed & 0x07) + 1)
81 } else {
82 0
83 };
84
85 Ok(Self {
86 width,
87 height,
88 global_table_size,
89 background,
90 })
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
96pub enum Disposal {
97 #[default]
99 None,
100 Keep,
102 Background,
104 Previous,
106}
107
108impl Disposal {
109 #[must_use]
111 pub const fn from_bits(bits: u8) -> Self {
112 match bits {
113 1 => Self::Keep,
114 2 => Self::Background,
115 3 => Self::Previous,
116 _ => Self::None,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126#[non_exhaustive]
127pub struct GraphicControl {
128 pub disposal: Disposal,
130 pub delay_centiseconds: u16,
132 pub transparent: Option<u8>,
134}
135
136impl GraphicControl {
137 #[must_use]
139 pub fn parse(payload: &[u8]) -> Self {
140 let packed = payload.first().copied().unwrap_or(0);
141 let delay = u16::from_le_bytes([
142 payload.get(1).copied().unwrap_or(0),
143 payload.get(2).copied().unwrap_or(0),
144 ]);
145 let index = payload.get(3).copied().unwrap_or(0);
146 Self {
147 disposal: Disposal::from_bits((packed >> 2) & 0x07),
148 delay_centiseconds: delay,
149 transparent: if packed & 0x01 != 0 {
150 Some(index)
151 } else {
152 None
153 },
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[non_exhaustive]
161pub struct ImageDescriptor {
162 pub left: u16,
164 pub top: u16,
166 pub width: u16,
168 pub height: u16,
170 pub local_table_size: usize,
172 pub interlaced: bool,
174}
175
176impl ImageDescriptor {
177 pub fn parse(bytes: &[u8; 9]) -> Result<Self> {
183 let left = u16::from_le_bytes([bytes[0], bytes[1]]);
184 let top = u16::from_le_bytes([bytes[2], bytes[3]]);
185 let width = u16::from_le_bytes([bytes[4], bytes[5]]);
186 let height = u16::from_le_bytes([bytes[6], bytes[7]]);
187 let packed = bytes[8];
188
189 if width == 0 || height == 0 {
190 return Err(PixelsError::malformed(
191 "gif",
192 format!("frame is {width}x{height}"),
193 ));
194 }
195
196 let local_table_size = if packed & 0x80 != 0 {
197 1_usize << ((packed & 0x07) + 1)
198 } else {
199 0
200 };
201
202 Ok(Self {
203 left,
204 top,
205 width,
206 height,
207 local_table_size,
208 interlaced: packed & 0x40 != 0,
209 })
210 }
211}
212
213#[must_use]
219pub fn interlaced_row(pass: usize, index: u32, height: u32) -> Option<u32> {
220 let (start, step) = match pass {
221 0 => (0, 8),
222 1 => (4, 8),
223 2 => (2, 4),
224 3 => (1, 2),
225 _ => return None,
226 };
227 let row = start + index * step;
228 if row < height { Some(row) } else { None }
229}
230
231#[must_use]
233pub const fn interlaced_pass_rows(pass: usize, height: u32) -> u32 {
234 let (start, step) = match pass {
235 0 => (0_u32, 8_u32),
236 1 => (4, 8),
237 2 => (2, 4),
238 3 => (1, 2),
239 _ => return 0,
240 };
241 if height > start {
242 (height - start).div_ceil(step)
243 } else {
244 0
245 }
246}
247
248pub fn read_sub_blocks<S: Source>(source: &mut S, limit: usize) -> Result<Vec<u8>> {
258 let mut out = Vec::new();
259 loop {
260 let mut length = [0_u8; 1];
261 source.read_exact(&mut length)?;
262 let length = length[0] as usize;
263 if length == 0 {
264 return Ok(out);
265 }
266 if out.len() + length > limit {
267 return Err(PixelsError::malformed(
268 "gif",
269 format!("sub-block chain exceeds the {limit} byte limit"),
270 ));
271 }
272 let at = out.len();
273 out.resize(at + length, 0);
274 let Some(slot) = out.get_mut(at..) else {
275 return Err(PixelsError::malformed("gif", "sub-block buffer is short"));
276 };
277 source.read_exact(slot)?;
278 }
279}
280
281pub fn skip_sub_blocks<S: Source>(source: &mut S) -> Result<()> {
287 let mut scratch = [0_u8; 255];
288 loop {
289 let mut length = [0_u8; 1];
290 source.read_exact(&mut length)?;
291 let length = length[0] as usize;
292 if length == 0 {
293 return Ok(());
294 }
295 let Some(slot) = scratch.get_mut(..length) else {
296 return Err(PixelsError::malformed("gif", "sub-block longer than 255"));
297 };
298 source.read_exact(slot)?;
299 }
300}
301
302pub fn write_sub_blocks(out: &mut Vec<u8>, payload: &[u8]) {
304 for chunk in payload.chunks(255) {
305 out.push(chunk.len() as u8);
306 out.extend_from_slice(chunk);
307 }
308 out.push(0);
309}
310
311#[cfg(test)]
312#[allow(
313 clippy::unwrap_used,
314 clippy::expect_used,
315 clippy::indexing_slicing,
316 clippy::panic,
317 reason = "tests operate on known-good values and assert shapes directly"
318)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn a_screen_descriptor_decodes_its_packed_fields() {
324 let bytes = [4, 0, 3, 0, 0x80 | 0x03, 7, 0];
326 let screen = Screen::parse(&bytes).unwrap();
327 assert_eq!((screen.width, screen.height), (4, 3));
328 assert_eq!(screen.global_table_size, 16);
329 assert_eq!(screen.background, 7);
330 }
331
332 #[test]
333 fn a_screen_without_a_global_table_reports_none() {
334 let bytes = [4, 0, 3, 0, 0x07, 0, 0];
335 let screen = Screen::parse(&bytes).unwrap();
336 assert_eq!(
337 screen.global_table_size, 0,
338 "the size bits must be ignored when the flag is clear"
339 );
340 }
341
342 #[test]
343 fn a_zero_sized_screen_is_malformed() {
344 assert!(Screen::parse(&[0, 0, 3, 0, 0, 0, 0]).is_err());
345 assert!(Screen::parse(&[4, 0, 0, 0, 0, 0, 0]).is_err());
346 }
347
348 #[test]
349 fn an_image_descriptor_decodes_its_packed_fields() {
350 let bytes = [1, 0, 2, 0, 5, 0, 6, 0, 0x80 | 0x40 | 0x02];
352 let image = ImageDescriptor::parse(&bytes).unwrap();
353 assert_eq!((image.left, image.top), (1, 2));
354 assert_eq!((image.width, image.height), (5, 6));
355 assert_eq!(image.local_table_size, 8);
356 assert!(image.interlaced);
357 }
358
359 #[test]
360 fn graphic_control_decodes_disposal_and_transparency() {
361 let control = GraphicControl::parse(&[(2 << 2) | 0x01, 0x02, 0x01, 5]);
363 assert_eq!(control.disposal, Disposal::Background);
364 assert_eq!(control.delay_centiseconds, 0x0102);
365 assert_eq!(control.transparent, Some(5));
366
367 let opaque = GraphicControl::parse(&[0, 0, 0, 5]);
370 assert_eq!(opaque.transparent, None);
371 }
372
373 #[test]
374 fn reserved_disposal_values_are_treated_as_none() {
375 for bits in 4..=7 {
378 assert_eq!(Disposal::from_bits(bits), Disposal::None, "{bits}");
379 }
380 }
381
382 #[test]
383 fn the_four_interlace_passes_cover_every_row_exactly_once() {
384 for height in 1..40_u32 {
387 let mut seen = vec![0_u32; height as usize];
388 let mut total = 0;
389 for pass in 0..4 {
390 let rows = interlaced_pass_rows(pass, height);
391 for index in 0..rows {
392 let row = interlaced_row(pass, index, height)
393 .unwrap_or_else(|| panic!("pass {pass} row {index} of {height}"));
394 seen[row as usize] += 1;
395 total += 1;
396 }
397 }
398 assert_eq!(total, height, "height {height} emitted {total} rows");
399 assert!(
400 seen.iter().all(|&n| n == 1),
401 "height {height} covered rows {seen:?}"
402 );
403 }
404 }
405
406 #[test]
407 fn interlaced_rows_past_a_pass_are_none() {
408 assert_eq!(interlaced_row(0, 0, 1), Some(0));
409 assert_eq!(interlaced_row(1, 0, 1), None, "pass 1 starts at row 4");
410 assert_eq!(
411 interlaced_row(9, 0, 100),
412 None,
413 "there are only four passes"
414 );
415 }
416
417 #[test]
418 fn sub_blocks_round_trip() {
419 for length in [0_usize, 1, 254, 255, 256, 1000] {
420 let payload: Vec<u8> = (0..length).map(|i| (i % 251) as u8).collect();
421 let mut encoded = Vec::new();
422 write_sub_blocks(&mut encoded, &payload);
423 assert_eq!(
424 encoded.last().copied(),
425 Some(0),
426 "a chain must end with a zero length"
427 );
428 let mut source = &encoded[..];
429 let decoded = read_sub_blocks(&mut source, 1 << 20).unwrap();
430 assert_eq!(decoded, payload, "length {length}");
431 }
432 }
433
434 #[test]
435 fn a_sub_block_chain_can_be_skipped_without_reading_it() {
436 let mut encoded = Vec::new();
437 write_sub_blocks(&mut encoded, &vec![7_u8; 600]);
438 encoded.extend_from_slice(b"after");
439 let mut source = &encoded[..];
440 skip_sub_blocks(&mut source).unwrap();
441 assert_eq!(source, b"after", "skipping stopped in the wrong place");
442 }
443
444 #[test]
445 fn a_truncated_chain_is_an_error_not_a_hang() {
446 let mut encoded = Vec::new();
447 write_sub_blocks(&mut encoded, &vec![1_u8; 300]);
448 for cut in 0..encoded.len() {
449 let mut source = &encoded[..cut];
450 let _ = read_sub_blocks(&mut source, 1 << 20);
451 }
452 }
453
454 #[test]
455 fn an_unbounded_chain_is_refused() {
456 let mut encoded = Vec::new();
459 write_sub_blocks(&mut encoded, &vec![0_u8; 100_000]);
460 let mut source = &encoded[..];
461 let error = read_sub_blocks(&mut source, 4096).unwrap_err();
462 assert!(error.to_string().contains("limit"), "{error}");
463 }
464}