1 | use std::{ |
2 | convert::TryInto, |
3 | fmt, |
4 | fmt::{Debug, Formatter}, |
5 | mem::size_of, |
6 | }; |
7 | |
8 | #[derive (Debug, Clone, Eq, PartialEq)] |
9 | struct Toc { |
10 | toctype: u32, |
11 | subtype: u32, |
12 | pos: u32, |
13 | } |
14 | |
15 | /// A struct representing an image. |
16 | /// Pixels are in ARGB format, with each byte representing a single channel. |
17 | #[derive (Clone, Eq, PartialEq, Debug)] |
18 | pub struct Image { |
19 | /// The nominal size of the image. |
20 | pub size: u32, |
21 | |
22 | /// The actual width of the image. Doesn't need to match `size`. |
23 | pub width: u32, |
24 | |
25 | /// The actual height of the image. Doesn't need to match `size`. |
26 | pub height: u32, |
27 | |
28 | /// The X coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated) |
29 | pub xhot: u32, |
30 | |
31 | /// The Y coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated) |
32 | pub yhot: u32, |
33 | |
34 | /// The amount of time (in milliseconds) that this image should be shown for, before switching to the next. |
35 | pub delay: u32, |
36 | |
37 | /// A slice containing the pixels' bytes, in RGBA format (or, in the order of the file). |
38 | pub pixels_rgba: Vec<u8>, |
39 | |
40 | /// A slice containing the pixels' bytes, in ARGB format. |
41 | pub pixels_argb: Vec<u8>, |
42 | } |
43 | |
44 | impl std::fmt::Display for Image { |
45 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
46 | f&mut DebugStruct<'_, '_>.debug_struct("Image" ) |
47 | .field("size" , &self.size) |
48 | .field("width" , &self.width) |
49 | .field("height" , &self.height) |
50 | .field("xhot" , &self.xhot) |
51 | .field("yhot" , &self.yhot) |
52 | .field("delay" , &self.delay) |
53 | .field(name:"pixels" , &"/* omitted */" ) |
54 | .finish() |
55 | } |
56 | } |
57 | |
58 | fn parse_header(mut i: Stream<'_>) -> Option<(Stream<'_>, u32)> { |
59 | i.tag(b"Xcur" )?; |
60 | i.u32_le()?; |
61 | i.u32_le()?; |
62 | let ntoc: u32 = i.u32_le()?; |
63 | |
64 | Some((i, ntoc)) |
65 | } |
66 | |
67 | fn parse_toc(mut i: Stream<'_>) -> Option<(Stream<'_>, Toc)> { |
68 | let toctype: u32 = i.u32_le()?; // Type |
69 | let subtype: u32 = i.u32_le()?; // Subtype |
70 | let pos: u32 = i.u32_le()?; // Position |
71 | |
72 | Some(( |
73 | i, |
74 | Toc { |
75 | toctype, |
76 | subtype, |
77 | pos, |
78 | }, |
79 | )) |
80 | } |
81 | |
82 | fn parse_img(mut i: Stream<'_>) -> Option<(Stream<'_>, Image)> { |
83 | i.tag(&[0x24, 0x00, 0x00, 0x00])?; // Header size |
84 | i.tag(&[0x02, 0x00, 0xfd, 0xff])?; // Type |
85 | let size = i.u32_le()?; |
86 | i.tag(&[0x01, 0x00, 0x00, 0x00])?; // Image version (1) |
87 | let width = i.u32_le()?; |
88 | let height = i.u32_le()?; |
89 | let xhot = i.u32_le()?; |
90 | let yhot = i.u32_le()?; |
91 | let delay = i.u32_le()?; |
92 | |
93 | let img_length: usize = (4 * width * height) as usize; |
94 | let pixels_slice = i.take_bytes(img_length)?; |
95 | let pixels_argb = rgba_to_argb(pixels_slice); |
96 | let pixels_rgba = Vec::from(pixels_slice); |
97 | |
98 | Some(( |
99 | i, |
100 | Image { |
101 | size, |
102 | width, |
103 | height, |
104 | xhot, |
105 | yhot, |
106 | delay, |
107 | pixels_argb, |
108 | pixels_rgba, |
109 | }, |
110 | )) |
111 | } |
112 | |
113 | /// Converts a RGBA slice into an ARGB vec |
114 | /// |
115 | /// Note that, if the input length is not |
116 | /// a multiple of 4, the extra elements are ignored. |
117 | fn rgba_to_argb(i: &[u8]) -> Vec<u8> { |
118 | let mut res: Vec = Vec::with_capacity(i.len()); |
119 | |
120 | for rgba: &[u8] in i.chunks(chunk_size:4) { |
121 | if rgba.len() < 4 { |
122 | break; |
123 | } |
124 | |
125 | res.push(rgba[3]); |
126 | res.push(rgba[0]); |
127 | res.push(rgba[1]); |
128 | res.push(rgba[2]); |
129 | } |
130 | |
131 | res |
132 | } |
133 | |
134 | /// Parse an XCursor file into its images. |
135 | pub fn parse_xcursor(content: &[u8]) -> Option<Vec<Image>> { |
136 | let (mut i: &[u8], ntoc: u32) = parse_header(content)?; |
137 | let mut imgs: Vec = Vec::with_capacity(ntoc as usize); |
138 | |
139 | for _ in 0..ntoc { |
140 | let (j: &[u8], toc: Toc) = parse_toc(i)?; |
141 | i = j; |
142 | |
143 | if toc.toctype == 0xfffd_0002 { |
144 | let index: RangeFrom = toc.pos as usize..; |
145 | let (_, img: Image) = parse_img(&content[index])?; |
146 | imgs.push(img); |
147 | } |
148 | } |
149 | |
150 | Some(imgs) |
151 | } |
152 | |
153 | type Stream<'a> = &'a [u8]; |
154 | |
155 | trait StreamExt<'a>: 'a { |
156 | /// Parse a series of bytes, returning `None` if it doesn't exist. |
157 | fn tag(&mut self, tag: &[u8]) -> Option<()>; |
158 | |
159 | /// Take a slice of bytes. |
160 | fn take_bytes(&mut self, len: usize) -> Option<&'a [u8]>; |
161 | |
162 | /// Parse a 32-bit little endian number. |
163 | fn u32_le(&mut self) -> Option<u32>; |
164 | } |
165 | |
166 | impl<'a> StreamExt<'a> for Stream<'a> { |
167 | fn tag(&mut self, tag: &[u8]) -> Option<()> { |
168 | if self.len() < tag.len() || self[..tag.len()] != *tag { |
169 | None |
170 | } else { |
171 | *self = &self[tag.len()..]; |
172 | Some(()) |
173 | } |
174 | } |
175 | |
176 | fn take_bytes(&mut self, len: usize) -> Option<&'a [u8]> { |
177 | if self.len() < len { |
178 | None |
179 | } else { |
180 | let (value, tail) = self.split_at(len); |
181 | *self = tail; |
182 | Some(value) |
183 | } |
184 | } |
185 | |
186 | fn u32_le(&mut self) -> Option<u32> { |
187 | self.take_bytes(size_of::<u32>()) |
188 | .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) |
189 | } |
190 | } |
191 | |
192 | #[cfg (test)] |
193 | mod tests { |
194 | use super::{parse_header, parse_toc, rgba_to_argb, Toc}; |
195 | |
196 | // A sample (and simple) XCursor file generated with xcursorgen. |
197 | // Contains a single 4x4 image. |
198 | const FILE_CONTENTS: [u8; 128] = [ |
199 | 0x58, 0x63, 0x75, 0x72, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, |
200 | 0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, |
201 | 0x00, 0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, |
202 | 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, |
203 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, |
204 | 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, |
205 | 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, |
206 | 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, |
207 | 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, |
208 | ]; |
209 | |
210 | #[test ] |
211 | fn test_parse_header() { |
212 | assert_eq!( |
213 | parse_header(&FILE_CONTENTS).unwrap(), |
214 | (&FILE_CONTENTS[16..], 1) |
215 | ) |
216 | } |
217 | |
218 | #[test ] |
219 | fn test_parse_toc() { |
220 | let toc = Toc { |
221 | toctype: 0xfffd0002, |
222 | subtype: 4, |
223 | pos: 0x1c, |
224 | }; |
225 | assert_eq!( |
226 | parse_toc(&FILE_CONTENTS[16..]).unwrap(), |
227 | (&FILE_CONTENTS[28..], toc) |
228 | ) |
229 | } |
230 | |
231 | #[test ] |
232 | fn test_rgba_to_argb() { |
233 | let initial: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; |
234 | |
235 | assert_eq!(rgba_to_argb(&initial), [3u8, 0, 1, 2, 7, 4, 5, 6]) |
236 | } |
237 | |
238 | #[test ] |
239 | fn test_rgba_to_argb_extra_items() { |
240 | let initial: [u8; 9] = [0, 1, 2, 3, 4, 5, 6, 7, 8]; |
241 | |
242 | assert_eq!(rgba_to_argb(&initial), &[3u8, 0, 1, 2, 7, 4, 5, 6]); |
243 | } |
244 | |
245 | #[test ] |
246 | fn test_rgba_to_argb_no_items() { |
247 | let initial: &[u8] = &[]; |
248 | |
249 | assert_eq!(initial, &rgba_to_argb(initial)[..]); |
250 | } |
251 | } |
252 | |