mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-25 10:27:07 -06:00
* feat(codecs): import XPM3 images with transparency (#3692) * Correct XPM test expectation for green palette name * feat(codecs): decode XPM2 plain-text image files --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
4.1 KiB
Rust
124 lines
4.1 KiB
Rust
use photocraft_codecs::{ChannelLayout, CodecError, Format, Image, caps, decode, decode_as, detect, encode, from_extension};
|
|
|
|
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_eq!(detect(SAMPLE), Some(Format::Xpm));
|
|
assert_eq!(from_extension("icon.XPM"), Some(Format::Xpm));
|
|
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_as(Format::Xpm, bytes).is_err(), "{:?}", bytes);
|
|
}
|
|
assert!(decode_as(Format::Xpm, b"/* XPM */\n\"4294967295 4294967295 1 1\"").is_err());
|
|
assert!(decode_as(Format::Xpm, 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_eq!(detect(bytes), Some(Format::Xpm));
|
|
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_as(Format::Xpm, bytes).is_err(), "{bytes:?}");
|
|
}
|
|
assert_ne!(detect(b"! XPM23\n1 1 1 1\na c black\na\n"), Some(Format::Xpm));
|
|
}
|
|
|
|
#[test]
|
|
fn xpm_is_import_only() {
|
|
assert!(caps(Format::Xpm).read);
|
|
assert!(!caps(Format::Xpm).write);
|
|
let image = Image::from_u8(1, 1, ChannelLayout::Rgba, vec![0, 0, 0, 0]).unwrap();
|
|
assert!(matches!(encode(&image, Format::Xpm, &Default::default()), Err(CodecError::Unsupported { .. })));
|
|
}
|