mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 11:13:55 +00:00
* 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>
128 lines
4.1 KiB
Rust
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()))),
|
|
}
|
|
}
|