Files
Dylan M. Taylorandsubha-2025 0ff43c72b3 Add craft-raster-codecs: DDS, DICOM, FITS, IFF/ILBM, PCX, PNM and XPM from PhotoCraft (#880)
* Add craft-raster-codecs: DDS, DICOM, FITS, IFF/ILBM, PCX, PNM and XPM from PhotoCraft

One module per format, moved from photocraft-codecs at 63b641b5 with its tests.
Decoders return one flat Image and take Limits; encoders take an image already in a
layout the format stores. Layout planning, colour and the document mapping stay in the app.

* craft-raster-codecs: PCX reads four 8-bit planes as RGBA and version 4 headers; pcx::dimensions

Four 8-bit planes are red, green, blue and alpha: they decode to RGBA instead of being
refused. Version 4 (PC Paintbrush for Windows) headers are accepted. pcx::dimensions gives the
pixel size from the header without decoding.

Ported from the PCX reader of storytold/designcraft#2011.

---------

Co-authored-by: subha-2025 <218455239+subha-2025@users.noreply.github.com>
2026-11-11 18:37:51 -05:00

128 lines
4.1 KiB
Rust

use craft_raster_codecs::{ChannelLayout, CodecError, Image, Limits, xpm};
fn decode(b: &[u8]) -> Result<Image, CodecError> {
xpm::decode(b, &Limits::default())
}
const SAMPLE: &[u8] = br#"/* XPM */
static char *icon[] = {
"2 2 3 1",
". c #ff0000",
" c None",
"+ c #00ff00",
".+",
" +"
};
"#;
#[test]
fn xpm3_decodes_palette_transparency_and_spaces_in_keys() {
assert!(xpm::looks_like_xpm(SAMPLE));
let img = decode(SAMPLE).unwrap();
assert_eq!(img.layout(), ChannelLayout::Rgba);
assert_eq!(img.dimensions(), (2, 2));
assert_eq!(img.data(), &[255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255]);
}
#[test]
fn rgb_precisions_and_x11_name() {
let xpm = br#"/* XPM */
static char *i[] = {
"3 1 3 1",
"a c #f00",
"b c #ffff00000000",
"c c gray70",
"abc"
};"#;
let img = decode(xpm).unwrap();
assert_eq!(img.data(), &[255, 0, 0, 255, 255, 0, 0, 255, 179, 179, 179, 255]);
}
#[test]
fn rejects_truncation_unsupported_colours_and_unknown_keys() {
for bytes in [
&b"/* XPM */\nstatic char *i[] = { \"1 1 1 1\", \"a c None\" };"[..],
&b"/* XPM */\nstatic char *i[] = { \"1 1 1 1\", \"a c NotAColor\", \"a\" };"[..],
&b"/* XPM */\nstatic char *i[] = { \"1 1 1 1\", \"a c #a\xC3\xA9\", \"a\" };"[..],
&b"/* XPM */\nstatic char *i[] = { \"1 1 1 1\", \"a c #+f0000\", \"a\" };"[..],
&b"/* XPM */\nstatic char *i[] = { \"1 1 1 1\", \"a c black\", \"z\" };"[..],
&b"/* XPM */\nstatic char *i[] = { \"1 1 1 1\", \"a c black\", \"aa\" };"[..],
&b"/* XPM */\nstatic char *i[] = { \"1 1 2 1\", \"a c black\", \"a c white\", \"a\" };"[..],
] {
assert!(decode(bytes).is_err(), "{:?}", bytes);
}
assert!(decode(b"/* XPM */\n\"4294967295 4294967295 1 1\"").is_err());
assert!(decode(b"/* XPM */\n\"2 1 1 1\" \"a c black\" \"aa\" /* bad").is_err());
}
#[test]
fn x11_names_and_field_priority() {
let xpm = br#"/* XPM */
static char *i[] = {
"4 1 4 1",
"a c LightGray",
"b c DarkBlue",
"c m black g white c light blue",
"d m black g4 red g green",
"abcd"
};"#;
let image = decode(xpm).unwrap();
assert_eq!(image.data(), &[211, 211, 211, 255, 0, 0, 139, 255, 173, 216, 230, 255, 0, 255, 0, 255]);
}
#[test]
fn named_x11_colors_use_x11_not_css_values() {
let bytes = br#"/* XPM */
static char *icon[] = {
"6 1 6 1",
"a c gray",
"b c maroon",
"c c purple",
"d c LightGoldenrodYellow",
"e c DarkSlateGray4",
"f c Ghost White",
"abcdef"
};"#;
let image = decode(bytes).unwrap();
assert_eq!(image.data(), &[190, 190, 190, 255, 176, 48, 96, 255, 160, 32, 240, 255, 250, 250, 210, 255, 82, 139, 139, 255, 248, 248, 255, 255,]);
}
#[test]
fn xpm2_decodes_crlf_comments_spaces_and_multichar_keys() {
let bytes = b"! XPM2\r\n! exported by an icon tool\r\n2 2 3 2\r\n.a c #f00\r\n!_ c None\r\nzz c DarkBlue\r\n.a!_\r\nzz.a\r\n";
assert!(xpm::looks_like_xpm(bytes));
let image = decode(bytes).unwrap();
assert_eq!(image.dimensions(), (2, 2));
assert_eq!(image.data(), &[255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 139, 255, 255, 0, 0, 255]);
}
#[test]
fn xpm2_allows_exclamation_as_a_pixel_key_and_preserves_space_keys() {
let bytes = b"! XPM2\n2 1 2 1\n! c red\n c None\n! \n";
let image = decode(bytes).unwrap();
assert_eq!(image.data(), &[255, 0, 0, 255, 0, 0, 0, 0]);
}
#[test]
fn xpm2_malformed_inputs_return_errors() {
for bytes in [
&b"! XPM2\n1 1 1 1\na c black\n"[..],
&b"! XPM2\n1 1 1 1\na c black\nb\n"[..],
&b"! XPM2\n1 1 1 1\na c #a\xc3\xa9\na\n"[..],
&b"! XPM2 invalid\n1 1 1 1\na c black\na\n"[..],
&b"! XPM2\n100000 100000 1 1\na c red\na\n"[..],
] {
assert!(decode(bytes).is_err(), "{bytes:?}");
}
assert!(!xpm::looks_like_xpm(b"! XPM23\n1 1 1 1\na c black\na\n"));
}
#[test]
fn xpm_rows_shorter_than_the_header_declares_are_refused_before_allocating() {
let b = b"/* XPM */\nstatic char *x[] = {\n\"30000 30000 1 1\",\n\". c #000000\",\n\".\",\n};\n".to_vec();
match decode(&b) {
Err(CodecError::Malformed { .. }) => {}
other => panic!("expected a malformed-row error, got {:?}", other.map(|i| (i.width(), i.height()))),
}
}