1 | use 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)] |
7 | struct 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)] |
16 | pub 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 | |
42 | impl 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 | |
56 | fn 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 | |
65 | fn 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 | |
77 | fn 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. |
122 | fn 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. |
136 | pub 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. |
141 | pub 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 | |
163 | trait 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 | |
174 | impl<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)] |
199 | mod 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 | |