Skip to content

Commit

Permalink
feat: Add XBM encoder
Browse files Browse the repository at this point in the history
  • Loading branch information
sorairolake committed Oct 8, 2024
1 parent a373218 commit cd4dc2a
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -84,6 +84,7 @@ qoi = ["dep:qoi"]
tga = []
tiff = ["dep:tiff"]
webp = ["dep:image-webp"]
xbm = []

# Other features
rayon = ["dep:rayon"] # Enables multi-threading
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
244 changes: 244 additions & 0 deletions src/codecs/xbm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//! Encoding of XBM images
//!
//! # Related Links
//!
//! - <https://www.x.org/releases/X11R7.7/doc/libX11/libX11/libX11.html#Manipulating_Bitmaps>

use std::io::Write;

use crate::{
color::ExtendedColorType,
error::{ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind},
image::{ImageEncoder, ImageFormat},
};

/// XBM encoder
pub struct XbmEncoder<W: Write> {
writer: W,
}

impl<W: Write> XbmEncoder<W> {
/// Creates a new encoder that writes its output to ```writer```.
pub fn new(writer: W) -> XbmEncoder<W> {
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::<Vec<_>>()
.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::<Vec<_>>()
.join(", ");
self.writer.write_all(format!(" {line},\n").as_bytes())?;
}
self.writer.write_all(b"};\n")?;
Ok(())
}
}

impl<W: Write> ImageEncoder for XbmEncoder<W> {
#[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);
}
}
12 changes: 12 additions & 0 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ pub enum ImageFormat {

/// An Image in QOI Format
Qoi,

/// An Image in XBM Format
Xbm,
}

impl ImageFormat {
Expand Down Expand Up @@ -103,6 +106,7 @@ impl ImageFormat {
"pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm,
"ff" => ImageFormat::Farbfeld,
"qoi" => ImageFormat::Qoi,
"xbm" => ImageFormat::Xbm,
_ => return None,
})
}
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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",
}
}

Expand All @@ -249,6 +255,7 @@ impl ImageFormat {
ImageFormat::Farbfeld => true,
ImageFormat::Avif => true,
ImageFormat::Qoi => true,
ImageFormat::Xbm => false,
}
}

Expand All @@ -273,6 +280,7 @@ impl ImageFormat {
ImageFormat::OpenExr => true,
ImageFormat::Dds => false,
ImageFormat::Qoi => true,
ImageFormat::Xbm => true,
}
}

Expand Down Expand Up @@ -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"],
}
}

Expand All @@ -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,
}
}
Expand All @@ -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,
}
}
Expand All @@ -371,6 +382,7 @@ impl ImageFormat {
ImageFormat::Qoi,
ImageFormat::Dds,
ImageFormat::Hdr,
ImageFormat::Xbm,
]
.iter()
.copied()
Expand Down
4 changes: 4 additions & 0 deletions src/image_reader/free_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ pub(crate) fn write_buffer_impl<W: std::io::Write + Seek>(
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,
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit cd4dc2a

Please sign in to comment.