1use std::{
2 fmt::{self, Debug, Formatter},
3 io::{Cursor, Error, ErrorKind, Read, Result as IoResult, Seek, SeekFrom},
4};
5
6#[derive(Debug, Clone, Eq, PartialEq)]
7struct Toc {
8 toctype: u32,
9 subtype: u32,
10 pos: u32,
11}
12
13/// A struct representing an image.
14/// Pixels are in ARGB format, with each byte representing a single channel.
15#[derive(Clone, Eq, PartialEq, Debug)]
16pub struct Image {
17 /// The nominal size of the image.
18 pub size: u32,
19
20 /// The actual width of the image. Doesn't need to match `size`.
21 pub width: u32,
22
23 /// The actual height of the image. Doesn't need to match `size`.
24 pub height: u32,
25
26 /// The X coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated)
27 pub xhot: u32,
28
29 /// The Y coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated)
30 pub yhot: u32,
31
32 /// The amount of time (in milliseconds) that this image should be shown for, before switching to the next.
33 pub delay: u32,
34
35 /// A slice containing the pixels' bytes, in RGBA format (or, in the order of the file).
36 pub pixels_rgba: Vec<u8>,
37
38 /// A slice containing the pixels' bytes, in ARGB format.
39 pub pixels_argb: Vec<u8>,
40}
41
42impl std::fmt::Display for Image {
43 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44 f&mut DebugStruct<'_, '_>.debug_struct("Image")
45 .field("size", &self.size)
46 .field("width", &self.width)
47 .field("height", &self.height)
48 .field("xhot", &self.xhot)
49 .field("yhot", &self.yhot)
50 .field("delay", &self.delay)
51 .field(name:"pixels", &"/* omitted */")
52 .finish()
53 }
54}
55
56fn parse_header(i: &mut impl Read) -> IoResult<(u32, u32)> {
57 i.tag(*b"Xcur")?;
58 let header: u32 = i.u32_le()?;
59 let _version: u32 = i.u32_le()?;
60 let ntoc: u32 = i.u32_le()?;
61
62 Ok((header, ntoc))
63}
64
65fn parse_toc(i: &mut impl Read) -> IoResult<Toc> {
66 let toctype: u32 = i.u32_le()?; // Type
67 let subtype: u32 = i.u32_le()?; // Subtype
68 let pos: u32 = i.u32_le()?; // Position
69
70 Ok(Toc {
71 toctype,
72 subtype,
73 pos,
74 })
75}
76
77fn parse_img(i: &mut impl Read) -> IoResult<Image> {
78 i.tag([0x24, 0x00, 0x00, 0x00])?; // Header size
79 i.tag([0x02, 0x00, 0xfd, 0xff])?; // Type
80 let size = i.u32_le()?;
81 i.tag([0x01, 0x00, 0x00, 0x00])?; // Image version (1)
82 let width = i.u32_le()?;
83 let height = i.u32_le()?;
84 let xhot = i.u32_le()?;
85 let yhot = i.u32_le()?;
86 let delay = i.u32_le()?;
87
88 // Check image is well-formed. Taken from https://gitlab.freedesktop.org/xorg/lib/libxcursor/-/blob/09617bcc9a0f1b5072212da5f8fede92ab85d157/src/file.c#L456-463
89 if width > 0x7fff || height > 0x7fff {
90 return Err(Error::new(ErrorKind::Other, "Image too large"));
91 }
92 if width == 0 || height == 0 {
93 return Err(Error::new(
94 ErrorKind::Other,
95 "Image with zero width or height",
96 ));
97 }
98 if xhot > width || yhot > height {
99 return Err(Error::new(ErrorKind::Other, "Hotspot outside image"));
100 }
101
102 let img_length: usize = (4 * width * height) as usize;
103 let pixels_rgba = i.take_bytes(img_length)?;
104 let pixels_argb = rgba_to_argb(&pixels_rgba);
105
106 Ok(Image {
107 size,
108 width,
109 height,
110 xhot,
111 yhot,
112 delay,
113 pixels_argb,
114 pixels_rgba,
115 })
116}
117
118/// Converts a RGBA slice into an ARGB vec
119///
120/// Note that, if the input length is not
121/// a multiple of 4, the extra elements are ignored.
122fn rgba_to_argb(i: &[u8]) -> Vec<u8> {
123 let mut res: Vec = Vec::with_capacity(i.len());
124
125 for rgba: &[u8] in i.chunks_exact(chunk_size:4) {
126 res.push(rgba[3]);
127 res.push(rgba[0]);
128 res.push(rgba[1]);
129 res.push(rgba[2]);
130 }
131
132 res
133}
134
135/// Parse an XCursor file into its images.
136pub fn parse_xcursor(content: &[u8]) -> Option<Vec<Image>> {
137 parse_xcursor_stream(&mut Cursor::new(inner:content)).ok()
138}
139
140/// Parse an XCursor file into its images.
141pub fn parse_xcursor_stream<R: Read + Seek>(input: &mut R) -> IoResult<Vec<Image>> {
142 let (header: u32, ntoc: u32) = parse_header(input)?;
143 input.seek(pos:SeekFrom::Start(header as u64))?;
144
145 let mut img_indices: Vec = Vec::new();
146 for _ in 0..ntoc {
147 let toc: Toc = parse_toc(input)?;
148
149 if toc.toctype == 0xfffd_0002 {
150 img_indices.push(toc.pos);
151 }
152 }
153
154 let mut imgs: Vec = Vec::with_capacity(ntoc as usize);
155 for index: u32 in img_indices {
156 input.seek(pos:SeekFrom::Start(index.into()))?;
157 imgs.push(parse_img(input)?);
158 }
159
160 Ok(imgs)
161}
162
163trait StreamExt {
164 /// Parse a series of bytes, returning `None` if it doesn't exist.
165 fn tag(&mut self, tag: [u8; 4]) -> IoResult<()>;
166
167 /// Take a slice of bytes.
168 fn take_bytes(&mut self, len: usize) -> IoResult<Vec<u8>>;
169
170 /// Parse a 32-bit little endian number.
171 fn u32_le(&mut self) -> IoResult<u32>;
172}
173
174impl<R: Read> StreamExt for R {
175 fn tag(&mut self, tag: [u8; 4]) -> IoResult<()> {
176 let mut data: [u8; 4] = [0u8; 4];
177 self.read_exact(&mut data)?;
178 if data != tag {
179 Err(Error::new(kind:ErrorKind::Other, error:"Tag mismatch"))
180 } else {
181 Ok(())
182 }
183 }
184
185 fn take_bytes(&mut self, len: usize) -> IoResult<Vec<u8>> {
186 let mut data: Vec = vec![0; len];
187 self.read_exact(&mut data)?;
188 Ok(data)
189 }
190
191 fn u32_le(&mut self) -> IoResult<u32> {
192 let mut data: [u8; 4] = [0u8; 4];
193 self.read_exact(&mut data)?;
194 Ok(u32::from_le_bytes(data))
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::{parse_header, parse_toc, parse_xcursor, rgba_to_argb, Image, Toc};
201 use std::io::Cursor;
202
203 // A sample (and simple) XCursor file generated with xcursorgen.
204 // Contains a single 4x4 image.
205 const FILE_CONTENTS: [u8; 128] = [
206 0x58, 0x63, 0x75, 0x72, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
207 0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00,
208 0x00, 0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04,
209 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
210 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
211 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
212 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00,
213 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
214 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
215 ];
216
217 #[test]
218 fn test_parse_header() {
219 let mut cursor = Cursor::new(&FILE_CONTENTS[..]);
220 assert_eq!(parse_header(&mut cursor).unwrap(), (16, 1));
221 assert_eq!(cursor.position(), 16);
222 }
223
224 #[test]
225 fn test_parse_toc() {
226 let toc = Toc {
227 toctype: 0xfffd0002,
228 subtype: 4,
229 pos: 0x1c,
230 };
231 let mut cursor = Cursor::new(&FILE_CONTENTS[16..]);
232 assert_eq!(parse_toc(&mut cursor).unwrap(), toc);
233 assert_eq!(cursor.position(), 28 - 16);
234 }
235
236 #[test]
237 fn test_parse_image() {
238 // The image always repeats the same pixels across its 4 x 4 pixels
239 let make_pixels = |pixel: [u8; 4]| {
240 // This is just "pixels.repeat(4 * 4)", but working in Rust 1.34
241 std::iter::repeat(pixel)
242 .take(4 * 4)
243 .flat_map(|p| p.iter().cloned().collect::<Vec<_>>())
244 .collect()
245 };
246 let expected = Image {
247 size: 4,
248 width: 4,
249 height: 4,
250 xhot: 1,
251 yhot: 1,
252 delay: 1,
253 pixels_rgba: make_pixels([0, 0, 0, 128]),
254 pixels_argb: make_pixels([128, 0, 0, 0]),
255 };
256 assert_eq!(Some(vec![expected]), parse_xcursor(&FILE_CONTENTS));
257 }
258
259 #[test]
260 fn test_one_image_three_times() {
261 let data = [
262 b'X', b'c', b'u', b'r', // magic
263 0x10, 0x00, 0x00, 0x00, // header file offset (16)
264 0x00, 0x00, 0x00, 0x00, // version
265 0x03, 0x00, 0x00, 0x00, // num TOC entries, 3
266 // TOC
267 0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
268 0x04, 0x00, 0x00, 0x00, // size 4
269 0x34, 0x00, 0x00, 0x00, // image offset (52)
270 0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
271 0x03, 0x00, 0x00, 0x00, // size 3
272 0x34, 0x00, 0x00, 0x00, // image offset (52)
273 0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
274 0x04, 0x00, 0x00, 0x00, // size 4
275 0x34, 0x00, 0x00, 0x00, // image offset (52)
276 // image
277 0x24, 0x00, 0x00, 0x00, // header
278 0x02, 0x00, 0xfd, 0xff, // IMAGE_TYPE
279 0x04, 0x00, 0x00, 0x00, // size 4
280 0x01, 0x00, 0x00, 0x00, // version
281 0x01, 0x00, 0x00, 0x00, // width 1
282 0x01, 0x00, 0x00, 0x00, // height 1
283 0x00, 0x00, 0x00, 0x00, // x_hot 0
284 0x00, 0x00, 0x00, 0x00, // y_hot 0
285 0x00, 0x00, 0x00, 0x00, // delay 0
286 0x12, 0x34, 0x56, 0x78, // pixel
287 ];
288 let expected = Image {
289 size: 4,
290 width: 1,
291 height: 1,
292 xhot: 0,
293 yhot: 0,
294 delay: 0,
295 pixels_rgba: vec![0x12, 0x34, 0x56, 0x78],
296 pixels_argb: vec![0x78, 0x12, 0x34, 0x56],
297 };
298 assert_eq!(
299 Some(vec![expected.clone(), expected.clone(), expected.clone()]),
300 parse_xcursor(&data)
301 );
302 }
303
304 #[test]
305 fn test_rgba_to_argb() {
306 let initial: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
307
308 assert_eq!(rgba_to_argb(&initial), [3u8, 0, 1, 2, 7, 4, 5, 6])
309 }
310
311 #[test]
312 fn test_rgba_to_argb_extra_items() {
313 let initial: [u8; 9] = [0, 1, 2, 3, 4, 5, 6, 7, 8];
314
315 assert_eq!(rgba_to_argb(&initial), &[3u8, 0, 1, 2, 7, 4, 5, 6]);
316 }
317
318 #[test]
319 fn test_rgba_to_argb_no_items() {
320 let initial: &[u8] = &[];
321
322 assert_eq!(initial, &rgba_to_argb(initial)[..]);
323 }
324}
325