mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 10:03:51 +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>
177 lines
6.2 KiB
Rust
177 lines
6.2 KiB
Rust
use craft_raster_codecs::{ChannelLayout, CodecError, Image, Limits, SampleType, pcx};
|
|
|
|
fn decode(b: &[u8]) -> Result<Image, CodecError> {
|
|
pcx::decode(b, &Limits::default())
|
|
}
|
|
|
|
fn header(bits: u8, planes: u8, width: u16, height: u16, stride: u16) -> Vec<u8> {
|
|
let mut b = vec![0u8; 128];
|
|
b[0] = 0x0a; // ZSoft
|
|
b[1] = 5; // PC Paintbrush 3.0+
|
|
b[2] = 1; // RLE
|
|
b[3] = bits;
|
|
b[8..10].copy_from_slice(&(width - 1).to_le_bytes());
|
|
b[10..12].copy_from_slice(&(height - 1).to_le_bytes());
|
|
b[12..14].copy_from_slice(&300u16.to_le_bytes());
|
|
b[14..16].copy_from_slice(&150u16.to_le_bytes());
|
|
b[65] = planes;
|
|
b[66..68].copy_from_slice(&stride.to_le_bytes());
|
|
b[68..70].copy_from_slice(&1u16.to_le_bytes());
|
|
b
|
|
}
|
|
|
|
fn rle_row(out: &mut Vec<u8>, row: &[u8]) {
|
|
for &value in row {
|
|
if value >= 0xc0 {
|
|
out.extend_from_slice(&[0xc1, value]);
|
|
} else {
|
|
out.push(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_three_plane_rgb_and_keeps_dpi() {
|
|
let mut b = header(8, 3, 2, 1, 2);
|
|
for row in [&[255, 0][..], &[0, 255][..], &[0, 0][..]] {
|
|
rle_row(&mut b, row);
|
|
}
|
|
assert!(pcx::looks_like_pcx(&b));
|
|
let im = decode(&b).unwrap();
|
|
assert_eq!(im.layout(), ChannelLayout::Rgb);
|
|
assert_eq!(im.sample_type(), SampleType::U8);
|
|
assert_eq!(im.dimensions(), (2, 1));
|
|
assert_eq!(im.data(), &[255, 0, 0, 0, 255, 0]);
|
|
assert_eq!(im.dpi, Some((300.0, 150.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_256_colour_vga_palette() {
|
|
let mut b = header(8, 1, 2, 1, 2);
|
|
rle_row(&mut b, &[1, 2]);
|
|
b.push(0x0c);
|
|
let mut palette = vec![0; 256 * 3];
|
|
palette[3..6].copy_from_slice(&[255, 0, 0]);
|
|
palette[6..9].copy_from_slice(&[0, 255, 0]);
|
|
b.extend(palette);
|
|
assert_eq!(decode(&b).unwrap().data(), &[255, 0, 0, 0, 255, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_4bit_and_monochrome_palettes() {
|
|
let mut four = header(4, 1, 2, 1, 2);
|
|
four[16 + 3..16 + 6].copy_from_slice(&[255, 0, 0]);
|
|
four[16 + 6..16 + 9].copy_from_slice(&[0, 0, 255]);
|
|
rle_row(&mut four, &[0x12, 0]);
|
|
assert_eq!(decode(&four).unwrap().data(), &[255, 0, 0, 0, 0, 255]);
|
|
|
|
let mut one = header(1, 1, 2, 1, 2);
|
|
one[16 + 3..16 + 6].copy_from_slice(&[255, 0, 0]);
|
|
rle_row(&mut one, &[0x80, 0]);
|
|
assert_eq!(decode(&one).unwrap().data(), &[255, 0, 0, 0, 0, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_truncated_and_overshooting_rle_without_panicking() {
|
|
let mut b = header(8, 3, 2, 1, 2);
|
|
assert!(decode(&b).is_err(), "header alone is truncated");
|
|
b.extend([0xc7, 0xff]); // a seven-byte run cannot cross a six-byte scanline
|
|
assert!(decode(&b).is_err());
|
|
let mut b = header(8, 3, 2, 1, 2);
|
|
b.extend([0xc1]); // missing run value
|
|
assert!(decode(&b).is_err());
|
|
let mut b = header(8, 3, 2, 1, 2);
|
|
b[66..68].copy_from_slice(&0u16.to_le_bytes());
|
|
assert!(!pcx::looks_like_pcx(&b), "invalid stride is not a PCX signature");
|
|
|
|
// A solid image with stride of the form 63k + 1 (64, 127) encodes as k full 63-byte runs
|
|
// plus a 1-byte literal (2k + 1 bytes per plane row), which is valid and must decode.
|
|
for width in [64u16, 127] {
|
|
let mut solid = header(8, 3, width, 1, width);
|
|
for _plane in 0..3 {
|
|
let mut rem = usize::from(width);
|
|
while rem >= 63 {
|
|
solid.extend_from_slice(&[0xff, 0x00]); // 63-byte run of 0x00
|
|
rem -= 63;
|
|
}
|
|
if rem == 1 {
|
|
solid.push(0x00); // 1-byte literal 0x00
|
|
}
|
|
}
|
|
let im = decode(&solid).unwrap();
|
|
assert_eq!(im.dimensions(), (u32::from(width), 1));
|
|
assert!(im.data().iter().all(|&v| v == 0));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn refuses_headers_that_exceed_allocation_budget() {
|
|
let mut b = header(8, 3, 3000, 3000, 3000);
|
|
// The default decoder rejects the allocation before reading any scanline.
|
|
let limits = Limits { max_alloc: 1024, ..Default::default() };
|
|
assert!(matches!(pcx::decode(&b, &limits), Err(CodecError::LimitExceeded(_))));
|
|
b[2] = 0;
|
|
assert!(!pcx::looks_like_pcx(&b), "unsupported uncompressed encoding");
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_rgb_runs_across_planes() {
|
|
let mut b = header(8, 3, 2, 1, 2);
|
|
b.extend([0xc4, 0xff, 0xc2, 0]);
|
|
let im = decode(&b).unwrap();
|
|
assert_eq!(im.data(), &[255, 255, 0, 255, 255, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_four_plane_runs_across_planes() {
|
|
let mut b = header(1, 4, 2, 1, 2);
|
|
b[16 + 15 * 3..16 + 16 * 3].copy_from_slice(&[255, 255, 0]);
|
|
b.extend([0xc8, 0xff]);
|
|
assert_eq!(decode(&b).unwrap().data(), &[255, 255, 0, 255, 255, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn pcx_header_declaring_gigabytes_needs_a_plausible_file_size() {
|
|
// 24-bit (three 8-bit planes) 65535 x 16384, with one row of data.
|
|
let mut b = vec![0u8; 128];
|
|
b[0] = 0x0a;
|
|
b[1] = 5;
|
|
b[2] = 1;
|
|
b[3] = 8;
|
|
b[8..10].copy_from_slice(&65534u16.to_le_bytes());
|
|
b[10..12].copy_from_slice(&16383u16.to_le_bytes());
|
|
b[65] = 3;
|
|
b[66..68].copy_from_slice(&65535u16.to_le_bytes());
|
|
b[68..70].copy_from_slice(&1u16.to_le_bytes());
|
|
b.extend_from_slice(&[0xff, 0]);
|
|
// PCX's own minimum-packet check refuses it before the output is allocated.
|
|
match decode(&b) {
|
|
Err(CodecError::Malformed { message, .. }) => assert!(message.contains("too short"), "{message}"),
|
|
other => panic!("expected a too-short refusal, got {:?}", other.map(|i| (i.width(), i.height()))),
|
|
}
|
|
}
|
|
|
|
/// Four 8-bit planes are red, green, blue and alpha.
|
|
#[test]
|
|
fn decodes_four_8_bit_planes_as_rgba() {
|
|
let mut b = header(8, 4, 2, 1, 2);
|
|
rle_row(&mut b, &[10, 40, 20, 50, 30, 60, 255, 0]);
|
|
assert_eq!(pcx::dimensions(&b), Some((2, 1)));
|
|
let im = decode(&b).unwrap();
|
|
assert_eq!((im.layout(), im.data()), (ChannelLayout::Rgba, &[10, 20, 30, 255, 40, 50, 60, 0][..]));
|
|
// Two 8-bit planes are still not a PCX layout.
|
|
b[65] = 2;
|
|
assert!(!pcx::looks_like_pcx(&b) && pcx::dimensions(&b).is_none() && decode(&b).is_err());
|
|
}
|
|
|
|
/// Version 4 (PC Paintbrush for Windows) files read like version 5.
|
|
#[test]
|
|
fn version_4_headers_are_pcx() {
|
|
let mut b = header(8, 3, 1, 1, 2);
|
|
rle_row(&mut b, &[1, 0, 2, 0, 3, 0]);
|
|
b[1] = 4;
|
|
assert_eq!(decode(&b).unwrap().data(), &[1, 2, 3]);
|
|
b[1] = 6;
|
|
assert!(decode(&b).is_err());
|
|
}
|