Files
craft-libs/crates/raster-codecs/src/pcx.rs
T
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

145 lines
6.3 KiB
Rust

//! ZSoft PCX (Paintbrush) reader: strictly bounded scanline RLE, 16/256-colour
//! palettes, 8-bit three-plane RGB and four-plane RGBA. Read-only: modern saves should use PNG.
use crate::{ChannelLayout, CodecError, Image, Limits, SampleType};
const HEADER: usize = 128;
fn bad(reason: impl std::fmt::Display) -> CodecError {
CodecError::malformed("PCX", reason)
}
fn le16(b: &[u8], i: usize) -> u16 {
u16::from_le_bytes([b[i], b[i + 1]])
}
/// PCX lacks a strong magic number. Require plausible complete header fields before
/// identifying it, rather than classifying every file starting with 0x0a as PCX.
pub fn looks_like_pcx(b: &[u8]) -> bool {
if b.len() < HEADER || b[0] != 0x0a || !matches!(b[1], 0 | 2..=5) || b[2] != 1 {
return false;
}
let bits = b[3];
let planes = b[65];
let stride = u32::from(le16(b, 66));
let (x0, y0, x1, y1) = (le16(b, 4), le16(b, 6), le16(b, 8), le16(b, 10));
if x1 < x0 || y1 < y0 || !matches!(bits, 1 | 2 | 4 | 8) || !(1..=4).contains(&planes) {
return false;
}
let width = u32::from(x1 - x0) + 1;
let min_stride = (width * u32::from(bits)).div_ceil(8);
stride >= min_stride && stride > 0 && (bits != 8 || matches!(planes, 1 | 3 | 4)) && (bits == 8 || u32::from(bits) * u32::from(planes) <= 4)
}
/// Read one compressed scanline, including all planes. RLE packets never cross a scanline.
fn read_row(b: &[u8], pos: &mut usize, out: &mut [u8]) -> Result<(), CodecError> {
let mut n = 0;
while n < out.len() {
let code = *b.get(*pos).ok_or_else(|| bad("truncated pixel RLE"))?;
*pos += 1;
let (count, value) = if code >= 0xc0 {
let count = usize::from(code & 0x3f);
let value = *b.get(*pos).ok_or_else(|| bad("truncated RLE packet"))?;
*pos += 1;
(count, value)
} else {
(1, code)
};
if count == 0 || count > out.len() - n {
return Err(bad("RLE packet crosses a scanline or has zero length"));
}
out[n..n + count].fill(value);
n += count;
}
Ok(())
}
/// The pixel size a PCX header declares, without decoding; `None` when [`looks_like_pcx`] is false.
pub fn dimensions(b: &[u8]) -> Option<(u32, u32)> {
looks_like_pcx(b).then(|| (u32::from(le16(b, 8) - le16(b, 4)) + 1, u32::from(le16(b, 10) - le16(b, 6)) + 1))
}
/// Decodes to 8-bit RGB, or RGBA for four 8-bit planes (red, green, blue, alpha).
pub fn decode(b: &[u8], limits: &Limits) -> Result<Image, CodecError> {
let (w, h) = dimensions(b).ok_or_else(|| bad("invalid PCX header or unsupported pixel layout"))?;
let (bits, planes, stride) = (usize::from(b[3]), usize::from(b[65]), usize::from(le16(b, 66)));
let layout = if bits == 8 && planes == 4 { ChannelLayout::Rgba } else { ChannelLayout::Rgb };
let ch = layout.channels();
limits.check(w, h, layout, SampleType::U8)?;
let row_len = stride.checked_mul(planes).ok_or_else(|| bad("row size overflow"))?;
let total = (w as usize).checked_mul(h as usize).and_then(|n| n.checked_mul(ch)).ok_or_else(|| bad("pixel size overflow"))?;
// The last 769 bytes are the optional VGA palette for 8-bit indexed images.
let vga = bits == 8 && planes == 1 && b.len() >= HEADER + 769 && b[b.len() - 769] == 0x0c;
if bits == 8 && planes == 1 && !vga && le16(b, 68) != 2 {
return Err(bad("8-bit indexed PCX has no 256-colour palette"));
}
let image_end = if vga { b.len() - 769 } else { b.len() };
// A 63-byte RLE packet takes 2 bytes; a trailing remainder of 1 byte below 0xC0
// can be encoded as a 1-byte literal, while any larger remainder takes at least 2 bytes.
// Because RLE packets may span planes within a scanline (`row_len`), compute the
// minimum encoded bytes across the full scanline (`row_len`) and multiply by `h`.
// Reject inputs that cannot possibly contain all scanlines before allocating
// the decoded image buffer.
let min_packets = (row_len / 63)
.checked_mul(2)
.and_then(|n| {
n.checked_add(match row_len % 63 {
0 => 0,
1 => 1,
_ => 2,
})
})
.ok_or_else(|| bad("compressed row size overflow"))?;
let min_encoded = min_packets.checked_mul(h as usize).ok_or_else(|| bad("compressed image size overflow"))?;
let encoded_len = image_end.saturating_sub(HEADER);
if encoded_len < min_encoded {
return Err(bad("PCX pixel data is too short for the declared image"));
}
let mut row = vec![0u8; row_len];
let mut pixels = Vec::new();
pixels.try_reserve_exact(total).map_err(|_| bad("PCX pixel buffer is too large"))?;
pixels.resize(total, 0);
let mut p = HEADER;
let width = w as usize;
for y in 0..h as usize {
read_row(&b[..image_end], &mut p, &mut row)?;
for x in 0..width {
let at = (y * width + x) * ch;
let rgb = if bits == 8 && planes >= 3 {
if planes == 4 {
pixels[at + 3] = row[3 * stride + x];
}
[row[x], row[stride + x], row[2 * stride + x]]
} else {
let index = if bits == 8 {
usize::from(row[x])
} else {
let bit = x * bits;
let shift = 8 - bits - (bit % 8);
let mask = (1usize << bits) - 1;
(0..planes).fold(0usize, |v, plane| {
let sample = (usize::from(row[plane * stride + bit / 8]) >> shift) & mask;
v | sample << (plane * bits)
})
};
if vga {
let o = image_end + 1 + index * 3;
[b[o], b[o + 1], b[o + 2]]
} else if bits == 8 {
let g = index as u8;
[g, g, g] // palette-info 2: gray scale
} else {
let o = 16 + index * 3;
[b[o], b[o + 1], b[o + 2]]
}
};
pixels[at..at + 3].copy_from_slice(&rgb);
}
}
let mut image = Image::from_u8(w, h, layout, pixels)?;
let (xdpi, ydpi) = (le16(b, 12), le16(b, 14));
if xdpi > 0 && ydpi > 0 {
image.dpi = Some((f32::from(xdpi), f32::from(ydpi)));
}
Ok(image)
}