1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate::error::{DecodeError, DecodeErrorKind};
use crate::limits::check_dimensions;
use crate::types::{Channels, DecodedImage, Format};
use image::ImageReader;
use std::io::Cursor;
/// Returns true if the GIF's first frame has a Graphic Control Extension
/// with the transparency flag set — matching PIL's img.info['transparency'] logic.
fn gif_has_transparency(bytes: &[u8]) -> bool {
if bytes.len() < 13 {
return false;
}
// Skip 6-byte header + 7-byte logical screen descriptor
let packed = bytes[10];
let gct_flag = (packed >> 7) & 1;
let gct_size = packed & 7;
let mut i: usize = 13;
// Skip global color table
if gct_flag != 0 {
let n_colors = 1usize << (gct_size + 1);
i += n_colors * 3;
}
// Walk blocks until we hit the first image descriptor
while i < bytes.len() {
let block_type = bytes[i];
match block_type {
0x3B => return false, // GIF trailer
0x21 => {
// Extension block
if i + 1 >= bytes.len() {
return false;
}
let ext_label = bytes[i + 1];
if ext_label == 0xF9 {
// Graphic Control Extension
// Format: 0x21 0xF9 <block-size=4> <flags> <delay-lo> <delay-hi> <transparent-idx> 0x00
if i + 3 < bytes.len() {
let flags = bytes[i + 3];
if flags & 1 != 0 {
return true;
}
}
// This GCE belongs to the first image — if it has no transparency, stop.
// Skip extension sub-blocks
i += 2;
while i < bytes.len() {
let sub_len = bytes[i] as usize;
i += 1;
if sub_len == 0 {
break;
}
i += sub_len;
}
// After the GCE we expect the image descriptor (0x2C), don't keep scanning
return false;
} else {
// Other extension — skip it
i += 2;
while i < bytes.len() {
let sub_len = bytes[i] as usize;
i += 1;
if sub_len == 0 {
break;
}
i += sub_len;
}
}
}
0x2C => {
// Image descriptor — first frame reached, no GCE transparency found
return false;
}
_ => {
// Unknown block, stop
return false;
}
}
}
false
}
pub(crate) fn decode_gif(bytes: &[u8]) -> Result<DecodedImage, DecodeError> {
// Sniff the canvas dimensions from the Logical Screen Descriptor (LSD)
// at offsets 6..10 BEFORE invoking the image crate's decoder, so the
// MAX_PIXELS guard fires before the underlying decoder allocates the
// raster. Spec §3.1 requires this ordering.
if bytes.len() >= 10 {
let gif_width = u16::from_le_bytes([bytes[6], bytes[7]]) as usize;
let gif_height = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
check_dimensions(gif_width, gif_height, Format::Gif)?;
}
let reader = ImageReader::with_format(Cursor::new(bytes), image::ImageFormat::Gif);
let img = match reader.decode() {
Ok(i) => i,
Err(e) => {
return Err(DecodeError::new(
DecodeErrorKind::CorruptInput,
Some(Format::Gif),
format!("image::decode failed: {}", e),
))
}
};
let (width, height) = (img.width() as usize, img.height() as usize);
// Defense in depth: the image crate composes individual frames onto the
// canvas, so its output dimensions match the LSD. We've already checked
// those above; re-checking here is cheap and protects against any
// future divergence between LSD and decoded dimensions.
check_dimensions(width, height, Format::Gif)?;
// The image crate always returns RGBA8 for GIF palette images.
// We replicate PIL's behaviour: use RGBA only if the first frame has a
// Graphic Control Extension with the transparency flag set.
if gif_has_transparency(bytes) {
let data: Vec<u8> = img.to_rgba8().into_raw();
Ok(DecodedImage {
width,
height,
data,
channels: Channels::Rgba,
format: Format::Gif,
})
} else {
let data: Vec<u8> = img.to_rgb8().into_raw();
Ok(DecodedImage {
width,
height,
data,
channels: Channels::Rgb,
format: Format::Gif,
})
}
}