| 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 | |