Merge pull request #7378 from storytold/dot/pcx-shared-cutover

Create: decode PCX through craft-image-codecs
This commit is contained in:
Dylan M. Taylor
2026-11-15 03:53:06 -05:00
committed by GitHub
2 changed files with 85 additions and 87 deletions
+47 -87
View File
@@ -533,85 +533,6 @@ fn le16(b: &[u8], at: usize) -> Option<u16> {
b.get(at..at + 2).map(|x| u16::from_le_bytes([x[0], x[1]]))
}
/// How a PCX image holds its pixels: bits per plane and planes.
#[derive(Clone, Copy, PartialEq)]
enum PcxLayout {
/// One byte per pixel per colour: each scanline has a red, a green and a blue row.
Rgb,
/// One byte per pixel, an index into the palette after the image (grey without one).
Indexed,
/// One bit per pixel in each of 1 to 4 planes, an index into the header's palette.
Bits(usize),
}
impl PcxLayout {
fn of(bits: u8, planes: usize) -> Option<PcxLayout> {
match (bits, planes) {
(8, 3) => Some(PcxLayout::Rgb),
(8, 1) => Some(PcxLayout::Indexed),
(1, 1..=4) => Some(PcxLayout::Bits(planes)),
_ => None,
}
}
}
/// The palette of a PCX image, and where its run-length coded scanlines end. An 8-bit image ends
/// with a 0x0C byte and 256 colours; a one-plane bit image is black and white whatever its header
/// holds; other bit images use the 16 colours at byte 16.
fn pcx_palette(bytes: &[u8], layout: PcxLayout) -> (Vec<u8>, usize) {
match layout {
PcxLayout::Rgb => (Vec::new(), bytes.len()),
PcxLayout::Indexed => match bytes.len().checked_sub(769).filter(|at| *at >= 128 && bytes.get(*at) == Some(&0x0C)) {
Some(at) => (bytes.get(at + 1..).unwrap_or_default().to_vec(), at),
None => ((0..=255u8).flat_map(|v| [v, v, v]).collect(), bytes.len()),
},
PcxLayout::Bits(1) => (vec![0, 0, 0, 255, 255, 255], bytes.len()),
PcxLayout::Bits(_) => (bytes.get(16..64).unwrap_or_default().to_vec(), bytes.len()),
}
}
/// The `rows` scanlines of `line` bytes each, decoded. A run may cross from one plane's row into the
/// next, but not out of its scanline.
fn pcx_scanlines(mut body: &[u8], rows: usize, line: usize) -> Option<Vec<u8>> {
let mut out = Vec::new();
out.try_reserve_exact(rows.checked_mul(line)?).ok()?;
for row in 1..=rows {
let end = row * line;
while out.len() < end {
let (&c, rest) = body.split_first()?;
body = rest;
if c < 0xC0 {
out.push(c);
continue;
}
let (&value, rest) = body.split_first()?;
body = rest;
let count = usize::from(c & 0x3F);
if count > end - out.len() {
return None;
}
out.resize(out.len() + count, value);
}
}
Some(out)
}
/// The RGB samples of the `w` pixels of scanline `row` (planes `stride` bytes apart).
fn pcx_row(out: &mut Vec<u8>, row: &[u8], w: usize, stride: usize, layout: PcxLayout, palette: &[u8]) {
let at = |i: usize| row.get(i).copied().unwrap_or(0);
for x in 0..w {
let index = match layout {
PcxLayout::Rgb => {
out.extend([at(x), at(stride + x), at(2 * stride + x)]);
continue;
}
PcxLayout::Indexed => usize::from(at(x)),
PcxLayout::Bits(planes) => (0..planes).fold(0, |index, k| index | (usize::from(at(k * stride + x / 8) >> (7 - x % 8) & 1) << k)),
};
out.extend((0..3).map(|c| palette.get(index * 3 + c).copied().unwrap_or(0)));
}
}
/// A PCX image: a 128-byte header (size at 4-11, resolution at 12 and 14, plane count at 65, bytes
/// per plane row at 66), then run-length coded scanlines, each holding one row of every plane.
fn pcx(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
@@ -627,7 +548,9 @@ fn pcx(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
if planes == 0 || stride == 0 {
return Err(bad("a damaged PCX header"));
}
let layout = PcxLayout::of(bits, planes).ok_or_else(|| bad("this PCX plane layout is not supported"))?;
if !matches!((bits, planes), (8, 1 | 3) | (1, 1..=4)) {
return Err(bad("this PCX plane layout is not supported"));
}
if stride < (w * usize::from(bits)).div_ceil(8) {
return Err(bad("a damaged PCX header"));
}
@@ -635,14 +558,51 @@ fn pcx(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
if line.saturating_mul(h) > MAX_DECODED_BYTES || w.saturating_mul(h).saturating_mul(3) > MAX_DECODED_BYTES {
return Err(bad("unusual PCX image size"));
}
let (palette, end) = pcx_palette(bytes, layout);
let body = bytes.get(128..end).unwrap_or_default();
let scanlines = pcx_scanlines(body, h, line).ok_or_else(|| bad("a damaged PCX body"))?;
let mut data = Vec::new();
data.try_reserve_exact(w * h * 3).map_err(|_| bad("not enough memory for this image"))?;
for row in scanlines.chunks_exact(line) {
pcx_row(&mut data, row, w, stride, layout, &palette);
// Preserve Create PDF's legacy palette policy, not a second pixel decoder: monochrome
// is black/white and an indexed file without its VGA palette is interpreted as gray.
// The shared reader owns all RLE, plane unpacking and palette application.
let has_vga = bytes.len().checked_sub(769).is_some_and(|at| at >= 128 && bytes.get(at) == Some(&0x0C));
// The pinned decoder predates shared version-4 recognition. Its version byte is
// only a header acceptance check, not a switch for pixel or palette semantics.
// Keep this app's existing version-4 acceptance without replacing the shared pin.
let version_four = byte(1) == 4;
let normalize = version_four || (bits == 1 && planes == 1) || (bits == 8 && planes == 1 && !has_vga);
let mut input = std::borrow::Cow::Borrowed(bytes);
if normalize {
// Normalization must not add an unbounded copy of hostile compressed input.
if bytes.len() > MAX_DECODED_BYTES {
return Err(bad("unusual PCX input size"));
}
let mut copy = Vec::new();
copy.try_reserve_exact(bytes.len()).map_err(|_| bad("not enough memory for this image"))?;
copy.extend_from_slice(bytes);
if version_four && let Some(version) = copy.get_mut(1) {
*version = 5;
}
if bits == 1 && planes == 1 {
if let Some(palette) = copy.get_mut(16..22) {
palette.copy_from_slice(&[0, 0, 0, 255, 255, 255]);
}
} else if bits == 8
&& planes == 1
&& !has_vga
&& let Some(info) = copy.get_mut(68..70)
{
info.copy_from_slice(&2u16.to_le_bytes());
}
input = std::borrow::Cow::Owned(copy);
}
let opts = craft_image_codecs::DecodeOptions {
limits: craft_image_codecs::Limits { max_alloc: MAX_DECODED_BYTES as u64, ..Default::default() },
..Default::default()
};
let image = craft_image_codecs::decode_as_with(craft_image_codecs::Format::Pcx, &input, &opts).map_err(|e| bad(&e.to_string()))?;
// The accepted layouts above are RGB8 in the pinned shared decoder. Refuse an unexpected
// contract rather than interpreting alpha or high-depth bytes as DeviceRGB samples.
if image.layout() != craft_image_codecs::ChannelLayout::Rgb || image.sample_type() != craft_image_codecs::SampleType::U8 {
return Err(bad("unexpected PCX pixel layout"));
}
let data = image.into_data();
let dpi = (f64::from(word(12)), f64::from(word(14)));
let dpi = if dpi.0 > 0.0 && dpi.1 > 0.0 { dpi } else { (72.0, 72.0) };
let mut d = Dict::new();
+38
View File
@@ -3119,3 +3119,41 @@ fn flate_wrapped_jpeg_rejects_corruption_and_spent_budget() {
assert_eq!(out.skipped.len(), 1, "{:?}", out.skipped);
}
}
#[test]
fn shared_pcx_keeps_monochrome_policy_and_resolution() {
let mut bytes = pcx_header(2, 1, 1, 2, 2, 1);
bytes[16..22].copy_from_slice(&[100, 20, 30, 10, 200, 40]);
bytes[12..14].copy_from_slice(&144u16.to_le_bytes());
bytes[14..16].copy_from_slice(&288u16.to_le_bytes());
bytes.extend_from_slice(&[0x80, 0]);
let image = super::pcx("legacy.pcx", &bytes).unwrap();
assert_eq!(image.data, [255, 255, 255, 0, 0, 0]);
assert_eq!(image.dpi, (144.0, 288.0));
bytes[12..14].fill(0);
assert_eq!(super::pcx("legacy.pcx", &bytes).unwrap().dpi, (72.0, 72.0));
}
#[test]
fn shared_pcx_rejects_zero_length_runs_and_oversized_planes() {
let mut zero = pcx_header(5, 8, 3, 1, 1, 1);
zero.extend_from_slice(&[0xC0, 9, 1, 2, 3]);
assert!(super::pcx("zero.pcx", &zero).is_err());
let oversized = pcx_header(5, 8, 3, u16::MAX, 1, u16::MAX);
assert!(super::pcx("large.pcx", &oversized).is_err());
let rgba = pcx_header(5, 8, 4, 1, 1, 1);
assert!(super::pcx("rgba.pcx", &rgba).is_err(), "do not silently expand accepted layouts");
}
#[test]
fn shared_pcx_keeps_version_four_rgb_and_palette_pixels() {
let mut rgb = pcx_header(4, 8, 3, 1, 1, 1);
rgb.extend_from_slice(&[10, 20, 30]);
assert!(super::is_image(&rgb));
assert_eq!(super::pcx("windows.pcx", &rgb).unwrap().data, [10, 20, 30]);
let mut palette = pcx_header(4, 1, 2, 1, 1, 1);
palette[25..28].copy_from_slice(&[4, 5, 6]);
palette.extend_from_slice(&[0x80, 0x80]);
assert_eq!(super::pcx("windows.pcx", &palette).unwrap().data, [4, 5, 6]);
assert!(from_images(&[("windows.pcx".into(), rgb)]).is_ok());
}