diff --git a/Cargo.toml b/Cargo.toml index acfbcef5f8..db55fe8c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ kamadak-exif = "0.5.5" default = ["rayon", "default-formats"] # Format features -default-formats = ["avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp"] +default-formats = ["avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp", "xbm"] avif = ["dep:ravif", "dep:rgb"] bmp = [] dds = [] @@ -84,6 +84,7 @@ qoi = ["dep:qoi"] tga = [] tiff = ["dep:tiff"] webp = ["dep:image-webp"] +xbm = [] # Other features rayon = ["dep:rayon"] # Enables multi-threading diff --git a/README.md b/README.md index e3eb168058..c156c30678 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ image format encoders and decoders. | TGA | Yes | Yes | | TIFF | Yes | Yes | | WebP | Yes | Yes (lossless only) | +| XBM | --- | Yes | - \* Requires the `avif-native` feature, uses the libdav1d C library. diff --git a/src/codecs/xbm.rs b/src/codecs/xbm.rs new file mode 100644 index 0000000000..6624203763 --- /dev/null +++ b/src/codecs/xbm.rs @@ -0,0 +1,244 @@ +//! Encoding of XBM images +//! +//! # Related Links +//! +//! - + +use std::io::Write; + +use crate::{ + color::ExtendedColorType, + error::{ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind}, + image::{ImageEncoder, ImageFormat}, +}; + +/// XBM encoder +pub struct XbmEncoder { + writer: W, +} + +impl XbmEncoder { + /// Creates a new encoder that writes its output to ```writer```. + pub fn new(writer: W) -> XbmEncoder { + Self { writer } + } + + /// Encodes the image `buf` that has dimensions `width` and `height`. + /// + /// # Panics + /// + /// Panics if `width * height != buf.len()` or `buf` contains pixels other + /// than `0` or `1`. + #[track_caller] + pub fn encode(mut self, name: &str, buf: &[u8], width: u32, height: u32) -> ImageResult<()> { + let expected_buffer_len = u64::from(width) * u64::from(height); + assert_eq!( + expected_buffer_len, + buf.len() as u64, + "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image", + buf.len(), + ); + assert!( + !buf.iter().any(|&p| p > 1), + "image contained pixels other than `0` or `1`" + ); + self.writer + .write_all(format!("#define {name}_width {width}\n").as_bytes())?; + self.writer + .write_all(format!("#define {name}_height {height}\n").as_bytes())?; + self.writer + .write_all(format!("static unsigned char {name}_bits[] = {{\n").as_bytes())?; + let mut pixels = Vec::with_capacity(12); + for bits_per_line in buf.chunks(width as usize) { + for bits in bits_per_line.chunks(8) { + let mut byte = 0; + for (i, bit) in bits.iter().enumerate() { + byte |= bit << (7 - i); + } + byte = byte.reverse_bits(); + pixels.push(byte); + // line_width <= 80 + if pixels.len() == 12 { + let line = pixels + .iter() + .map(|p| format!("{p:#04x}")) + .collect::>() + .join(", "); + self.writer.write_all(format!(" {line},\n").as_bytes())?; + pixels.clear(); + } + } + } + if !pixels.is_empty() { + let line = pixels + .iter() + .map(|p| format!("{p:#04x}")) + .collect::>() + .join(", "); + self.writer.write_all(format!(" {line},\n").as_bytes())?; + } + self.writer.write_all(b"};\n")?; + Ok(()) + } +} + +impl ImageEncoder for XbmEncoder { + #[track_caller] + fn write_image( + self, + buf: &[u8], + width: u32, + height: u32, + color_type: ExtendedColorType, + ) -> ImageResult<()> { + if color_type != ExtendedColorType::L1 { + return Err(ImageError::Unsupported( + UnsupportedError::from_format_and_kind( + ImageFormat::Xbm.into(), + UnsupportedErrorKind::Color(color_type), + ), + )); + } + self.encode("image", buf, width, height) + } +} + +#[cfg(test)] +mod tests { + use std::str; + + use super::*; + + #[test] + fn write() { + let mut writer = [0; 132]; + // "R" (8x7) + let buf = b"\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x01\x01\x01\x00\x00\x00\ +\x00\x00\x01\x00\x00\x01\x00\x00\ +\x00\x00\x01\x01\x01\x00\x00\x00\ +\x00\x00\x01\x00\x00\x01\x00\x00\ +\x00\x00\x01\x01\x01\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00"; + let expected = "#define image_width 8 +#define image_height 7 +static unsigned char image_bits[] = { + 0x00, 0x1c, 0x24, 0x1c, 0x24, 0x1c, 0x00, +}; +"; + XbmEncoder::new(&mut writer[..]) + .write_image(buf, 8, 7, ExtendedColorType::L1) + .unwrap(); + assert_eq!(str::from_utf8(&writer).unwrap(), expected); + } + + #[test] + fn write_16x14() { + let mut writer = [0; 268]; + // "R" (16x14) + let buf = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + let expected = "#define image_width 16 +#define image_height 14 +static unsigned char image_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0xf0, 0x03, 0xf0, 0x03, 0x30, 0x0c, 0x30, 0x0c, + 0xf0, 0x03, 0xf0, 0x03, 0x30, 0x0c, 0x30, 0x0c, 0xf0, 0x03, 0xf0, 0x03, + 0x00, 0x00, 0x00, 0x00, +}; +"; + XbmEncoder::new(&mut writer[..]) + .write_image(buf, 16, 14, ExtendedColorType::L1) + .unwrap(); + assert_eq!(str::from_utf8(&writer).unwrap(), expected); + } + + #[test] + fn write_width_7() { + let mut writer = [0; 126]; + // "I" (7x6) + let buf = b"\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x01\x01\x01\x00\x00\ +\x00\x00\x00\x01\x00\x00\x00\ +\x00\x00\x00\x01\x00\x00\x00\ +\x00\x00\x01\x01\x01\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00"; + let expected = "#define image_width 7 +#define image_height 6 +static unsigned char image_bits[] = { + 0x00, 0x1c, 0x08, 0x08, 0x1c, 0x00, +}; +"; + XbmEncoder::new(&mut writer[..]) + .write_image(buf, 7, 6, ExtendedColorType::L1) + .unwrap(); + assert_eq!(str::from_utf8(&writer).unwrap(), expected); + } + + #[test] + fn write_width_14() { + let mut writer = [0; 240]; + // "I" (14x12) + let buf = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; + let expected = "#define image_width 14 +#define image_height 12 +static unsigned char image_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0xf0, 0x03, 0xf0, 0x03, 0xc0, 0x00, 0xc0, 0x00, + 0xc0, 0x00, 0xc0, 0x00, 0xf0, 0x03, 0xf0, 0x03, 0x00, 0x00, 0x00, 0x00, +}; +"; + XbmEncoder::new(&mut writer[..]) + .write_image(buf, 14, 12, ExtendedColorType::L1) + .unwrap(); + assert_eq!(str::from_utf8(&writer).unwrap(), expected); + } + + #[test] + fn write_invalid_extended_color_type() { + let mut writer = []; + let buf = [0; 8]; + assert!(XbmEncoder::new(&mut writer[..]) + .write_image(&buf, 8, 1, ExtendedColorType::Rgba8) + .is_err()); + } + + #[test] + #[should_panic(expected = "Invalid buffer length: expected 32 got 8 for 16x2 image")] + fn write_invalid_dimensions() { + let mut writer = []; + let buf = [0; 8]; + let _: ImageResult<()> = + XbmEncoder::new(&mut writer[..]).write_image(&buf, 16, 2, ExtendedColorType::L1); + } + + #[test] + #[should_panic(expected = "image contained pixels other than `0` or `1`")] + fn write_invalid_pixels() { + let mut writer = []; + let buf = b"\x00\x01\x02\x03\x03\x02\x01\x00"; + let _: ImageResult<()> = + XbmEncoder::new(&mut writer[..]).write_image(buf, 8, 1, ExtendedColorType::L1); + } +} diff --git a/src/image.rs b/src/image.rs index 74822d4173..a707db604c 100644 --- a/src/image.rs +++ b/src/image.rs @@ -65,6 +65,9 @@ pub enum ImageFormat { /// An Image in QOI Format Qoi, + + /// An Image in XBM Format + Xbm, } impl ImageFormat { @@ -103,6 +106,7 @@ impl ImageFormat { "pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm, "ff" => ImageFormat::Farbfeld, "qoi" => ImageFormat::Qoi, + "xbm" => ImageFormat::Xbm, _ => return None, }) } @@ -178,6 +182,7 @@ impl ImageFormat { // Qoi's MIME type is being worked on. // See: https://github.com/phoboslab/qoi/issues/167 "image/x-qoi" => Some(ImageFormat::Qoi), + "image/x-xbitmap" => Some(ImageFormat::Xbm), _ => None, } } @@ -225,6 +230,7 @@ impl ImageFormat { ImageFormat::Qoi => "image/x-qoi", // farbfeld's MIME type taken from https://www.wikidata.org/wiki/Q28206109 ImageFormat::Farbfeld => "application/octet-stream", + ImageFormat::Xbm => "image/x-xbitmap", } } @@ -249,6 +255,7 @@ impl ImageFormat { ImageFormat::Farbfeld => true, ImageFormat::Avif => true, ImageFormat::Qoi => true, + ImageFormat::Xbm => false, } } @@ -273,6 +280,7 @@ impl ImageFormat { ImageFormat::OpenExr => true, ImageFormat::Dds => false, ImageFormat::Qoi => true, + ImageFormat::Xbm => true, } } @@ -304,6 +312,7 @@ impl ImageFormat { // According to: https://aomediacodec.github.io/av1-avif/#mime-registration ImageFormat::Avif => &["avif"], ImageFormat::Qoi => &["qoi"], + ImageFormat::Xbm => &["xbm"], } } @@ -326,6 +335,7 @@ impl ImageFormat { ImageFormat::Farbfeld => cfg!(feature = "ff"), ImageFormat::Avif => cfg!(feature = "avif"), ImageFormat::Qoi => cfg!(feature = "qoi"), + ImageFormat::Xbm => false, ImageFormat::Dds => false, } } @@ -349,6 +359,7 @@ impl ImageFormat { ImageFormat::OpenExr => cfg!(feature = "exr"), ImageFormat::Qoi => cfg!(feature = "qoi"), ImageFormat::Hdr => cfg!(feature = "hdr"), + ImageFormat::Xbm => cfg!(feature = "xbm"), ImageFormat::Dds => false, } } @@ -371,6 +382,7 @@ impl ImageFormat { ImageFormat::Qoi, ImageFormat::Dds, ImageFormat::Hdr, + ImageFormat::Xbm, ] .iter() .copied() diff --git a/src/image_reader/free_functions.rs b/src/image_reader/free_functions.rs index 7a9bb34400..e72c009e5e 100644 --- a/src/image_reader/free_functions.rs +++ b/src/image_reader/free_functions.rs @@ -115,6 +115,10 @@ pub(crate) fn write_buffer_impl( ImageFormat::Hdr => { hdr::HdrEncoder::new(buffered_write).write_image(buf, width, height, color) } + #[cfg(feature = "xbm")] + ImageFormat::Xbm => { + xbm::XbmEncoder::new(buffered_write).write_image(buf, width, height, color) + } _ => Err(ImageError::Unsupported( UnsupportedError::from_format_and_kind( ImageFormatHint::Unknown, diff --git a/src/lib.rs b/src/lib.rs index e34d754709..6b4f4c37c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,6 +221,7 @@ pub mod flat; /// | TGA | Yes | Yes | /// | TIFF | Yes | Yes | /// | WebP | Yes | Yes (lossless only) | +/// | XBM | --- | Yes | /// /// - \* Requires the `avif-native` feature, uses the libdav1d C library. /// @@ -276,6 +277,8 @@ pub mod codecs { pub mod tiff; #[cfg(feature = "webp")] pub mod webp; + #[cfg(feature = "xbm")] + pub mod xbm; #[cfg(feature = "dds")] mod dxt;