image_extras/
xbm.rs

1//! Decoding of X BitMap (.xbm) Images
2//!
3//! XBM (X BitMap) Format is a plain text image format, sometimes used to store
4//! cursor and icon data. XBM images can be valid C code (although a noticeable
5//! fraction of historical images includes a name field which is not a valid C
6//! identifier, and need not even be either of pure-ASCII or UTF-8).
7//!
8//! # Related Links
9//! * <https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#Manipulating_Bitmaps> - The XBM format specification
10//! * <https://en.wikipedia.org/wiki/X_BitMap> - The XBM format on wikipedia
11
12use std::fmt;
13use std::io::{BufRead, Bytes};
14
15use image::error::{DecodingError, ImageFormatHint, ParameterError, ParameterErrorKind};
16use image::{ColorType, ExtendedColorType, ImageDecoder, ImageError, ImageResult};
17
18/// Location of a byte in the input stream.
19///
20/// Includes byte offset (for format debugging with hex editor) and
21/// line:column offset (for format debugging with text editor)
22#[derive(Clone, Copy, Debug)]
23struct TextLocation {
24    byte: u64,
25    line: u64,
26    column: u64,
27}
28
29/// A peekable reader which tracks location information
30struct TextReader<R> {
31    inner: R,
32
33    current: Option<u8>,
34
35    location: TextLocation,
36}
37
38impl<R> TextReader<R>
39where
40    R: Iterator<Item = u8>,
41{
42    /// Initialize a TextReader
43    fn new(mut r: R) -> TextReader<R> {
44        let current = r.next();
45        TextReader {
46            inner: r,
47            current,
48            location: TextLocation {
49                byte: 0,
50                line: 1,
51                column: 0,
52            },
53        }
54    }
55
56    /// Consume the next byte. On EOF, will return None
57    fn next(&mut self) -> Option<u8> {
58        self.current?;
59
60        let mut current = self.inner.next();
61        std::mem::swap(&mut self.current, &mut current);
62
63        self.location.byte += 1;
64        self.location.column += 1;
65        if let Some(b'\n') = current {
66            self.location.line += 1;
67            self.location.column = 0;
68        }
69        current
70    }
71    /// Peek at the next byte. On EOF, will return None
72    fn peek(&self) -> Option<u8> {
73        self.current
74    }
75    /// The location of the last byte returned by [Self::next]
76    fn loc(&self) -> TextLocation {
77        self.location
78    }
79}
80
81/// Properties of an XBM image (excluding the rarely useful `name` field.)
82struct XbmHeaderData {
83    width: u32,
84    height: u32,
85    hotspot: Option<(i32, i32)>,
86}
87
88/// XBM stream decoder (works in no_std, has the natural streaming API for the uncompressed text structure of XBM)
89///
90/// To properly validate the image trailer, invoke `next_byte()` again after reading the last byte of content; if
91/// the trailer is valid it should return Ok(None).
92struct XbmStreamDecoder<R> {
93    r: TextReader<R>,
94    current_position: u64,
95    // Note: technically this includes header metadata that isn't _needed_ when parsing
96    header: XbmHeaderData,
97}
98
99/// Helper struct to project BufRead down to Iterator<Item=u8>. Costs of this simple
100/// lifetime-free abstraction include that the struct requires space to store the
101/// error value, and that code using this must eventually check the error field.
102struct IoAdapter<R> {
103    reader: Bytes<R>,
104    error: Option<std::io::Error>,
105}
106
107impl<R> Iterator for IoAdapter<R>
108where
109    R: BufRead,
110{
111    type Item = u8;
112    #[inline(always)]
113    fn next(&mut self) -> Option<Self::Item> {
114        if self.error.is_some() {
115            return None;
116        }
117        match self.reader.next() {
118            None => None,
119            Some(Ok(v)) => Some(v),
120            Some(Err(e)) => {
121                self.error = Some(e);
122                None
123            }
124        }
125    }
126}
127
128/// XBM decoder (usable wrapper of XbmStreamDecoder that handles IO errors)
129pub struct XbmDecoder<R> {
130    base: XbmStreamDecoder<IoAdapter<R>>,
131}
132
133/// Part of the XBM file in which a parse error occurs
134#[derive(Debug, Clone, Copy)]
135enum XbmPart {
136    Width,
137    Height,
138    HotspotX,
139    HotspotY,
140    Array,
141    Data,
142    ArrayEnd,
143    Trailing,
144}
145
146/// Error that can occur while parsing an XBM file
147#[derive(Debug)]
148enum XbmDecodeError {
149    Parse(XbmPart, TextLocation),
150    DecodeInteger(XbmPart),
151    ZeroWidth,
152    ZeroHeight,
153}
154
155impl fmt::Display for TextLocation {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.write_fmt(format_args!(
158            "byte={},line={}:col={}",
159            self.byte, self.line, self.column
160        ))
161    }
162}
163
164impl fmt::Display for XbmPart {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match self {
167            XbmPart::Width => f.write_str("#define for image width"),
168            XbmPart::Height => f.write_str("#define for image height"),
169            XbmPart::HotspotX => f.write_str("#define for hotspot x coordinate"),
170            XbmPart::HotspotY => f.write_str("#define for hotspot y coordinate"),
171            XbmPart::Array => f.write_str("array definition"),
172            XbmPart::Data => f.write_str("array content"),
173            XbmPart::ArrayEnd => f.write_str("array end"),
174            XbmPart::Trailing => f.write_str("end of file"),
175        }
176    }
177}
178
179impl fmt::Display for XbmDecodeError {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            XbmDecodeError::Parse(part, loc) => f.write_fmt(format_args!(
183                "Failed to parse {}, unexpected character or eof at {}",
184                part, loc
185            )),
186            XbmDecodeError::DecodeInteger(part) => {
187                f.write_fmt(format_args!("Failed to parse integer for {}", part))
188            }
189            XbmDecodeError::ZeroWidth => f.write_str("Invalid image width: should not be zero"),
190            XbmDecodeError::ZeroHeight => f.write_str("Invalid image height: should not be zero"),
191        }
192    }
193}
194
195impl std::error::Error for XbmDecodeError {}
196
197impl From<XbmDecodeError> for ImageError {
198    fn from(e: XbmDecodeError) -> ImageError {
199        ImageError::Decoding(DecodingError::new(ImageFormatHint::Name("XBM".into()), e))
200    }
201}
202
203/// Helper trait for the pattern in which, after calling a function returning a Result,
204/// one wishes to use an error from a different source.
205trait XbmDecoderIoInjectionExt {
206    type Value;
207    fn apply_after(self, err: &mut Option<std::io::Error>) -> Result<Self::Value, ImageError>;
208}
209
210impl<X> XbmDecoderIoInjectionExt for Result<X, XbmDecodeError> {
211    type Value = X;
212    fn apply_after(self, err: &mut Option<std::io::Error>) -> Result<Self::Value, ImageError> {
213        if let Some(err) = err.take() {
214            return Err(ImageError::IoError(err));
215        }
216        match self {
217            Self::Ok(x) => Ok(x),
218            Self::Err(e) => Err(ImageError::Decoding(DecodingError::new(
219                ImageFormatHint::Name("XBM".into()),
220                e,
221            ))),
222        }
223    }
224}
225
226/// A limit on the length of a #define symbol (containing `name` + '_width') in an image.
227/// Names are typically valid C identifiers, and a 255 char limit is common,
228/// so XBM files exceeding this are unlikely to work anyway.
229const MAX_IDENTIFIER_LENGTH: usize = 256;
230
231/// Read precisely the string `s` from `r`, or error.
232fn read_fixed_string<R: Iterator<Item = u8>>(
233    r: &mut TextReader<R>,
234    s: &[u8],
235    part: XbmPart,
236) -> Result<(), XbmDecodeError> {
237    for c in s {
238        if let Some(b) = r.next() {
239            if b != *c {
240                return Err(XbmDecodeError::Parse(part, r.loc()));
241            }
242        } else {
243            return Err(XbmDecodeError::Parse(part, r.loc()));
244        };
245    }
246    Ok(())
247}
248// Read a single byte
249fn read_byte<R: Iterator<Item = u8>>(
250    r: &mut TextReader<R>,
251    part: XbmPart,
252) -> Result<u8, XbmDecodeError> {
253    match r.next() {
254        None => Err(XbmDecodeError::Parse(part, r.loc())),
255        Some(b) => Ok(b),
256    }
257}
258
259/// Read a mixture of ' ' and '\t'. At least one character must be read.
260// Other whitespace characters are not permitted.
261fn read_whitespace_gap<R: Iterator<Item = u8>>(
262    r: &mut TextReader<R>,
263    part: XbmPart,
264) -> Result<(), XbmDecodeError> {
265    let b = read_byte(r, part)?;
266    if !(b == b' ' || b == b'\t') {
267        return Err(XbmDecodeError::Parse(part, r.loc()));
268    }
269    while let Some(b) = r.peek() {
270        if b == b' ' || b == b'\t' {
271            r.next();
272            continue;
273        } else {
274            return Ok(());
275        }
276    }
277    Ok(())
278}
279/// Read a mixture of ' ', '\t', and '\n'. Other whitespace characters are not permitted.
280fn read_optional_whitespace<R: Iterator<Item = u8>>(
281    r: &mut TextReader<R>,
282) -> Result<(), XbmDecodeError> {
283    while let Some(b) = r.peek() {
284        if b == b' ' || b == b'\t' || b == b'\n' {
285            r.next();
286            continue;
287        } else {
288            break;
289        }
290    }
291    Ok(())
292}
293/// Read a mixture of ' ' and '\t', until reading '\n'.
294fn read_to_newline<R: Iterator<Item = u8>>(
295    r: &mut TextReader<R>,
296    part: XbmPart,
297) -> Result<(), XbmDecodeError> {
298    while let Some(b) = r.peek() {
299        if b == b' ' || b == b'\t' {
300            r.next();
301            continue;
302        } else {
303            break;
304        }
305    }
306    if read_byte(r, part)? != b'\n' {
307        Err(XbmDecodeError::Parse(part, r.loc()))
308    } else {
309        Ok(())
310    }
311}
312/// Read token into the buffer until the buffer size is exceeded, or ' ' or '\t' or '\n' is found
313/// Returns the length of the data read.
314fn read_until_whitespace<'a, R: Iterator<Item = u8>>(
315    r: &mut TextReader<R>,
316    buf: &'a mut [u8],
317    part: XbmPart,
318) -> Result<&'a [u8], XbmDecodeError> {
319    let mut len = 0;
320    while let Some(b) = r.peek() {
321        if b == b' ' || b == b'\t' || b == b'\n' {
322            return Ok(&buf[..len]);
323        } else {
324            if len >= buf.len() {
325                // identifier is too long
326                return Err(XbmDecodeError::Parse(part, r.loc()));
327            }
328            buf[len] = b;
329            len += 1;
330            r.next();
331        }
332    }
333    Ok(&buf[..len])
334}
335
336/// Read a single hex digit, either upper or lower case
337fn read_hex_digit<R: Iterator<Item = u8>>(
338    r: &mut TextReader<R>,
339    part: XbmPart,
340) -> Result<u8, XbmDecodeError> {
341    let b = read_byte(r, part)?;
342    match b {
343        b'0'..=b'9' => Ok(b - b'0'),
344        b'A'..=b'F' => Ok(b - b'A' + 10),
345        b'a'..=b'f' => Ok(b - b'a' + 10),
346        _ => Err(XbmDecodeError::Parse(part, r.loc())),
347    }
348}
349
350/// Read a hex-encoded byte (e.g.: 0xA1)
351fn read_hex_byte<R: Iterator<Item = u8>>(
352    r: &mut TextReader<R>,
353    part: XbmPart,
354) -> Result<u8, XbmDecodeError> {
355    if read_byte(r, part)? != b'0' {
356        return Err(XbmDecodeError::Parse(part, r.loc()));
357    }
358    let x = read_byte(r, part)?;
359    if !(x == b'x' || x == b'X') {
360        return Err(XbmDecodeError::Parse(part, r.loc()));
361    }
362    let mut v = read_hex_digit(r, part)? << 4;
363    v += read_hex_digit(r, part)?;
364    Ok(v)
365}
366
367/// Parse string into signed integer, rejecting leading + and leading zeros
368/// (i32::from_str_radix accepts '014' as 14, but in C is it octal and has value 12)
369fn parse_i32(data: &[u8]) -> Option<i32> {
370    if data.starts_with(b"-") {
371        (-(parse_u32(&data[1..])? as i64)).try_into().ok()
372    } else {
373        parse_u32(data)?.try_into().ok()
374    }
375}
376
377/// Parse string into unsigned integer, rejecting leading + and leading zeros
378/// (u32::from_str_radix accepts '014' as 14, but in C is it octal and has value 12)
379fn parse_u32(data: &[u8]) -> Option<u32> {
380    let Some(c1) = data.first() else {
381        // Reject empty string
382        return None;
383    };
384    if *c1 == b'0' && data.len() > 1 {
385        // Nonzero integers may not have leading zeros
386        return None;
387    }
388    let mut x: u32 = 0;
389    for c in data {
390        if b'0' <= *c && *c <= b'9' {
391            x = x.checked_mul(10)?.checked_add((*c - b'0') as u32)?;
392        } else {
393            return None;
394        }
395    }
396    Some(x)
397}
398
399/// Read the XBM file header up to and including the first opening brace
400fn read_xbm_header<'a, R: Iterator<Item = u8>>(
401    r: &mut TextReader<R>,
402    name_width_buf: &'a mut [u8],
403) -> Result<(&'a [u8], XbmHeaderData), XbmDecodeError> {
404    // The header consists of three to five lines. Lines 3-4 may be skipped
405    // In practice, the name may be empty or UTF-8.
406    //
407    //  #define <name>_width <width>
408    //  #define <name>_height <height>
409    //  #define <name>_x_hot <x>
410    //  #define <name>_y_hot <y>
411    //  static <type> <name>_bits[] = { ...
412    let mut int_buf = [0u8; 11]; // -2^31 and 2^32 fit in 11 bytes
413
414    // Read width field and acquire name.
415    read_fixed_string(r, b"#define", XbmPart::Width)?;
416    read_whitespace_gap(r, XbmPart::Width)?;
417    let name_width = read_until_whitespace(r, name_width_buf, XbmPart::Width)?;
418    if !name_width.ends_with(b"_width") {
419        return Err(XbmDecodeError::Parse(XbmPart::Width, r.loc()));
420    }
421    let name = &name_width[..name_width.len() - b"_width".len()];
422    read_whitespace_gap(r, XbmPart::Width)?;
423    let int = read_until_whitespace(r, &mut int_buf, XbmPart::Width)?;
424    read_to_newline(r, XbmPart::Width)?;
425
426    let width = parse_u32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::Width))?;
427    if width == 0 {
428        return Err(XbmDecodeError::ZeroWidth);
429    }
430
431    // Read height field, checking that the name matches
432    read_fixed_string(r, b"#define", XbmPart::Height)?;
433    read_whitespace_gap(r, XbmPart::Height)?;
434    read_fixed_string(r, name, XbmPart::Height)?;
435    read_fixed_string(r, b"_height", XbmPart::Height)?;
436    read_whitespace_gap(r, XbmPart::Height)?;
437    let int = read_until_whitespace(r, &mut int_buf, XbmPart::Height)?;
438    read_to_newline(r, XbmPart::Height)?;
439
440    let height = parse_u32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::Height))?;
441    if height == 0 {
442        return Err(XbmDecodeError::ZeroHeight);
443    }
444
445    let hotspot = match r.peek() {
446        Some(b'#') => {
447            // Parse hotspot lines
448            read_fixed_string(r, b"#define", XbmPart::HotspotX)?;
449            read_whitespace_gap(r, XbmPart::HotspotX)?;
450            read_fixed_string(r, name, XbmPart::HotspotX)?;
451            read_fixed_string(r, b"_x_hot", XbmPart::HotspotX)?;
452            read_whitespace_gap(r, XbmPart::HotspotX)?;
453            let int = read_until_whitespace(r, &mut int_buf, XbmPart::HotspotX)?;
454            read_to_newline(r, XbmPart::HotspotX)?;
455
456            let hotspot_x =
457                parse_i32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::HotspotX))?;
458
459            read_fixed_string(r, b"#define", XbmPart::HotspotY)?;
460            read_whitespace_gap(r, XbmPart::HotspotY)?;
461            read_fixed_string(r, name, XbmPart::HotspotY)?;
462            read_fixed_string(r, b"_y_hot", XbmPart::HotspotY)?;
463            read_whitespace_gap(r, XbmPart::HotspotY)?;
464            let int = read_until_whitespace(r, &mut int_buf, XbmPart::HotspotY)?;
465            read_to_newline(r, XbmPart::HotspotY)?;
466
467            let hotspot_y =
468                parse_i32(int).ok_or(XbmDecodeError::DecodeInteger(XbmPart::HotspotY))?;
469
470            Some((hotspot_x, hotspot_y))
471        }
472        Some(b's') => None,
473        _ => {
474            r.next();
475            return Err(XbmDecodeError::Parse(XbmPart::Array, r.loc()));
476        }
477    };
478
479    read_fixed_string(r, b"static", XbmPart::Array)?;
480    read_whitespace_gap(r, XbmPart::Array)?;
481    match r.peek() {
482        Some(b'c') => {
483            read_fixed_string(r, b"char", XbmPart::Array)?;
484        }
485        Some(b'u') => {
486            read_fixed_string(r, b"unsigned", XbmPart::Array)?;
487            read_whitespace_gap(r, XbmPart::Array)?;
488            read_fixed_string(r, b"char", XbmPart::Array)?;
489        }
490        _ => {
491            r.next();
492            return Err(XbmDecodeError::Parse(XbmPart::Array, r.loc()));
493        }
494    }
495    read_whitespace_gap(r, XbmPart::Array)?;
496    read_fixed_string(r, name, XbmPart::Array)?;
497    read_fixed_string(r, b"_bits[]", XbmPart::Array)?;
498    read_whitespace_gap(r, XbmPart::Array)?;
499    read_fixed_string(r, b"=", XbmPart::Array)?;
500    read_whitespace_gap(r, XbmPart::Array)?;
501    read_fixed_string(r, b"{", XbmPart::Array)?;
502
503    Ok((
504        name,
505        XbmHeaderData {
506            width,
507            height,
508            hotspot,
509        },
510    ))
511}
512
513impl<R> XbmStreamDecoder<R>
514where
515    R: Iterator<Item = u8>,
516{
517    /// Create a new `XbmStreamDecoder` or error if the header failed to parse.
518    pub fn new(reader: R) -> Result<XbmStreamDecoder<R>, (R, XbmDecodeError)> {
519        let mut r = TextReader::new(reader);
520
521        let mut name_width_buf = [0u8; MAX_IDENTIFIER_LENGTH];
522        match read_xbm_header(&mut r, &mut name_width_buf) {
523            Err(e) => Err((r.inner, e)),
524            Ok((_name, header)) => Ok(XbmStreamDecoder {
525                r,
526                current_position: 0,
527                header,
528            }),
529        }
530    }
531
532    /// Read the next byte of the raw image data. The XBM image is organized
533    /// in row major order with rows containing ceil(width / 8) bytes, so that
534    /// the `i`th pixel in a row is the `(i%8)`th least significant
535    /// bit of the `(i/8)`th byte in the row. Bit value 1 = black, 0 = white.
536    pub fn next_byte(&mut self) -> Result<Option<u8>, XbmDecodeError> {
537        let data_size = (self.header.width.div_ceil(8) as u64) * (self.header.height as u64);
538        if self.current_position < data_size {
539            let first = self.current_position == 0;
540            self.current_position += 1;
541
542            if !first {
543                read_optional_whitespace(&mut self.r)?;
544                read_fixed_string(&mut self.r, b",", XbmPart::Data)?;
545            }
546            read_optional_whitespace(&mut self.r)?;
547            Ok(Some(read_hex_byte(&mut self.r, XbmPart::Data)?))
548        } else {
549            // Read optional comma, followed by final };
550            read_optional_whitespace(&mut self.r)?;
551            match self.r.peek() {
552                Some(b',') => {
553                    read_fixed_string(&mut self.r, b",", XbmPart::Data)?;
554                    read_optional_whitespace(&mut self.r)?;
555                }
556                Some(b'}') => (),
557                _ => {
558                    self.r.next();
559                    return Err(XbmDecodeError::Parse(XbmPart::ArrayEnd, self.r.loc()));
560                }
561            }
562            read_fixed_string(&mut self.r, b"}", XbmPart::ArrayEnd)?;
563            read_optional_whitespace(&mut self.r)?;
564            read_fixed_string(&mut self.r, b";", XbmPart::ArrayEnd)?;
565            read_optional_whitespace(&mut self.r)?;
566
567            if self.r.next().is_some() {
568                // File has unexpected trailing contents
569                return Err(XbmDecodeError::Parse(XbmPart::Trailing, self.r.loc()));
570            };
571
572            Ok(None)
573        }
574    }
575}
576
577impl<R> XbmDecoder<R>
578where
579    R: BufRead,
580{
581    /// Create a new `XBMDecoder`.
582    pub fn new(reader: R) -> Result<XbmDecoder<R>, ImageError> {
583        match XbmStreamDecoder::new(IoAdapter {
584            reader: reader.bytes(),
585            error: None,
586        }) {
587            Err((mut r, e)) => Err(e).apply_after(&mut r.error),
588            Ok(x) => Ok(XbmDecoder { base: x }),
589        }
590    }
591
592    /// Returns the (x,y) hotspot coordinates of the image, if the image provides them.
593    pub fn hotspot(&self) -> Option<(i32, i32)> {
594        self.base.header.hotspot
595    }
596}
597
598impl<R: BufRead> ImageDecoder for XbmDecoder<R> {
599    fn dimensions(&self) -> (u32, u32) {
600        (self.base.header.width, self.base.header.height)
601    }
602    fn color_type(&self) -> ColorType {
603        ColorType::L8
604    }
605    fn original_color_type(&self) -> ExtendedColorType {
606        ExtendedColorType::L1
607    }
608    fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()>
609    where
610        Self: Sized,
611    {
612        for row in buf.chunks_exact_mut(self.base.header.width as usize) {
613            // The XBM format discards the last `8 * ceil(self.width / 8) - self.width` bits in each row
614            for chunk in row.chunks_mut(8) {
615                let nxt = self
616                    .base
617                    .next_byte()
618                    .apply_after(&mut self.base.r.inner.error)?;
619                let val = nxt.ok_or_else(|| {
620                    ImageError::Parameter(ParameterError::from_kind(
621                        ParameterErrorKind::DimensionMismatch,
622                    ))
623                })?;
624                for (i, p) in chunk.iter_mut().enumerate() {
625                    // Set bits correspond to black, unset bits to white
626                    *p = if val & (1 << i) == 0 { 0xff } else { 0 };
627                }
628            }
629        }
630
631        let val = self
632            .base
633            .next_byte()
634            .apply_after(&mut self.base.r.inner.error)?;
635        if val.is_some() {
636            return Err(ImageError::Parameter(ParameterError::from_kind(
637                ParameterErrorKind::DimensionMismatch,
638            )));
639        }
640
641        Ok(())
642    }
643    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
644        (*self).read_image(buf)
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use std::fs::File;
652    use std::io::BufReader;
653
654    #[test]
655    fn image_without_hotspot() {
656        let decoder = XbmDecoder::new(BufReader::new(
657            File::open("tests/images/xbm/1x1.xbm").unwrap(),
658        ))
659        .expect("Unable to read XBM file");
660
661        assert_eq!((1, 1), decoder.dimensions());
662        assert_eq!(None, decoder.hotspot());
663    }
664
665    #[test]
666    fn image_with_hotspot() {
667        let decoder = XbmDecoder::new(BufReader::new(
668            File::open("tests/images/xbm/hotspot.xbm").unwrap(),
669        ))
670        .expect("Unable to read XBM file");
671
672        assert_eq!((5, 5), decoder.dimensions());
673        assert_eq!(Some((-1, 2)), decoder.hotspot());
674    }
675}