File formats: PCX (.pcx) files open and place

A new reader, written from the ZSoft PCX Technical Reference Manual, decodes
the run-length encoded scan lines of 1-bit images in the header palette's
first two colors (black and white when the version has no palette or the two
colors are the same), 1-bit images in two to four planes and 4-bit images
with the header's palette (the EGA colors for versions 0 and 3), 2-bit CGA
images in the colors that the header's CGA background and palette settings
pick, 8-bit images with the 256-color palette after the image data (gray
levels without one) and 24-bit images in three planes. The image is stored as PNG at the resolution of the header's HDpi and
VDpi fields.

Open and Place list PCX, and pasting a copied .pcx file and Glass's Load
Texture… read it by its header. A file of another depth or plane count, one
cut short and one whose pixels as RGBA pass the pixel allowance are refused
with a message before its pixels are decoded. The desktop packaging does not
associate .pcx files, and no export writes them.

Docs: docs/file-format-parity.md, docs/target-app-parity.md, docs/mcp.md,
docs/releasing.md.
This commit is contained in:
dotCooCoo
2026-10-30 00:15:56 +07:00
committed by bflatastic
parent c3cd065ef3
commit f32699bea7
31 changed files with 503 additions and 12 deletions
+4
View File
@@ -36,6 +36,7 @@ pub(crate) use load::{MAX_RASTER_ALLOC, size_note};
mod metafile;
mod naming;
mod native;
mod pcxread;
pub mod pdf;
mod pdfimport;
pub mod pngtext;
@@ -192,6 +193,9 @@ mod tests_psd;
#[cfg(test)]
mod tests_psdread;
#[cfg(test)]
mod tests_pcxread;
#[cfg(test)]
mod tests_epsimport;
+8 -4
View File
@@ -489,6 +489,8 @@ pub const FORMATS: &[Format] = &[
reader("psb", "PSB", &["psb"], "image/x-psb", true),
// PostScript programs, read as EPS files are; Print to PostScript writes them, exports don't.
reader("ps", "PostScript", &["ps"], "application/postscript", false),
// ZSoft PCX images, read by `pcxread` and stored as PNG; exports don't write them.
reader("pcx", "PCX", &["pcx"], "image/x-pcx", true),
];
/// Every extension `document.open` reads (the "All readable files" filter of open dialogs).
@@ -512,6 +514,7 @@ pub const OPEN_EXTS: &[&str] = &[
"vda",
"icb",
"vst",
"pcx",
"psd",
"psb",
"vctemplate",
@@ -528,10 +531,10 @@ pub const OPEN_EXTS: &[&str] = &[
/// Extensions in [`OPEN_EXTS`] the packaging declares no type for: Photoshop documents belong to
/// raster editors, so Finder and file managers don't offer a vector app for them, and there is no
/// `.epsf`, `.ps` or Targa type (File › Open and Place still read them all). On Linux the desktop
/// entry's EPS type (shared-mime-info's) also covers `.epsf`, so file managers offer VectorCraft
/// for it.
pub const UNASSOCIATED_EXTS: &[&str] = &["psd", "psb", "epsf", "ps", "tga", "vda", "icb", "vst"];
/// `.epsf`, `.ps`, Targa or PCX type (File › Open and Place still read them all). On Linux the
/// desktop entry's EPS type (shared-mime-info's) also covers `.epsf`, so file managers offer
/// VectorCraft for it.
pub const UNASSOCIATED_EXTS: &[&str] = &["psd", "psb", "epsf", "ps", "tga", "vda", "icb", "vst", "pcx"];
/// The extension that picks each writable format when exporting (the format's first; PNG-8 shares
/// `.png` with PNG, so `.png` comes once), in [`FORMATS`] order.
@@ -577,6 +580,7 @@ pub const PLACE_EXTS: &[&str] = &[
"vda",
"icb",
"vst",
"pcx",
"psd",
"psb",
"vctemplate",
+5
View File
@@ -561,6 +561,8 @@ pub(crate) fn size_note(bytes: &[u8]) -> Option<String> {
/// too ([`vectorcraft_doc::cmyk::group3_tiff_rgba`]).
/// Grayscale TIFFs with an alpha channel or other extra channels, which the decoder can't read
/// either, become RGBA too, whether they store their channels together or one plane per channel.
/// A PCX file is recognized by its header ([`super::pcxread::is_pcx`]) and read by
/// [`super::pcxread::decode`], which refuses an image past [`MAX_RASTER_ALLOC`] before decoding.
/// A Targa file, which starts with no signature, is recognized by its header
/// ([`super::tgaread::is_targa`]) and becomes RGBA with the transparency it stores
/// ([`super::tgaread::fix_alpha`]). An image stored as PNG whose decoded samples, or whose pixels
@@ -580,6 +582,9 @@ pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
if reader.format().is_none() && super::tgaread::is_targa(bytes) {
reader.set_format(image::ImageFormat::Tga);
}
if reader.format().is_none() && super::pcxread::is_pcx(bytes) {
return as_png(super::pcxread::decode(bytes, MAX_RASTER_ALLOC).map_err(err)?, ppi);
}
let mut limits = image::Limits::default();
limits.max_alloc = Some(MAX_RASTER_ALLOC);
reader.limits(limits);
+151
View File
@@ -0,0 +1,151 @@
//! Reading PCX files (`.pcx`) for File › Open and File › Place, from the ZSoft PCX Technical
//! Reference Manual (release 5). [`is_pcx`] recognizes a file by the first fields of its 128-byte
//! header, and [`decode`] reads its run-length encoded scan lines into RGBA pixels: 1-bit with two
//! colors; 1-bit in two to four planes and 4-bit with up to 16 colors; 2-bit CGA pictures in the
//! four colors their CGA palette settings pick; 8-bit with the
//! 256-color palette after the image data (gray levels without one); and 24-bit color in three
//! 8-bit planes. The file is untrusted: every read is checked.
/// The size of the header, after which the image data starts.
const HEADER: usize = 128;
/// The message for a file whose header or image data ends early.
const TRUNCATED: &str = "the PCX file is cut short";
/// The message for a header whose window or line length can't hold its image.
const DAMAGED: &str = "the PCX file's header is damaged";
/// The 16 colors of the EGA palette, which files of versions 0 and 3 use instead of the header's.
const EGA: [[u8; 3]; 16] = [
[0x00, 0x00, 0x00],
[0x00, 0x00, 0xaa],
[0x00, 0xaa, 0x00],
[0x00, 0xaa, 0xaa],
[0xaa, 0x00, 0x00],
[0xaa, 0x00, 0xaa],
[0xaa, 0x55, 0x00],
[0xaa, 0xaa, 0xaa],
[0x55, 0x55, 0x55],
[0x55, 0x55, 0xff],
[0x55, 0xff, 0x55],
[0x55, 0xff, 0xff],
[0xff, 0x55, 0x55],
[0xff, 0x55, 0xff],
[0xff, 0xff, 0x55],
[0xff, 0xff, 0xff],
];
/// Does `bytes` start with a PCX header? The manufacturer byte is `0x0A`, the version 0, 2, 3, 4
/// or 5, the encoding 1 (run-length) and the bits per pixel of each plane 1, 2, 4 or 8.
pub(super) fn is_pcx(bytes: &[u8]) -> bool {
matches!(bytes, [0x0a, 0 | 2 | 3 | 4 | 5, 1, 1 | 2 | 4 | 8, ..])
}
/// The resolution in the header's HDpi and VDpi fields; `None` when either is 0 or the header is
/// cut short.
pub(super) fn resolution(bytes: &[u8]) -> Option<(f64, f64)> {
let le = |at: usize| Some(u16::from_le_bytes([*bytes.get(at)?, *bytes.get(at + 1)?]));
let (x, y) = (le(12)?, le(14)?);
(x > 0 && y > 0).then(|| (f64::from(x), f64::from(y)))
}
/// The pixels of PCX file `bytes`, which [`is_pcx`] accepts, refused when they or the decoded
/// scan lines take more than `max_alloc` bytes (the pixels counted as RGBA) before anything is
/// decoded.
pub(super) fn decode(bytes: &[u8], max_alloc: u64) -> Result<image::RgbaImage, String> {
let head = bytes.get(..HEADER).ok_or(TRUNCATED)?;
let le = |at: usize| usize::from(u16::from_le_bytes([head[at], head[at + 1]]));
let (version, bits, planes) = (head[1], head[3], head[65]);
let (x_min, y_min, x_max, y_max) = (le(4), le(6), le(8), le(10));
let line = le(66);
if !matches!((bits, planes), (1, 1..=4) | (2 | 4, 1) | (8, 1 | 3)) {
return Err(format!("PCX files with {bits}-bit pixels in {planes} planes can't be read"));
}
if x_max < x_min || y_max < y_min {
return Err(DAMAGED.into());
}
let (w, h) = (x_max - x_min + 1, y_max - y_min + 1);
// Each plane's line holds the row's pixels, padded to an even number of bytes.
if line * 8 < w * usize::from(bits) {
return Err(DAMAGED.into());
}
let scan = line * usize::from(planes);
let rgba = w as u64 * h as u64 * 4;
if rgba > max_alloc || scan as u64 * h as u64 > max_alloc {
return Err(format!("an image of {w} × {h} pixels is too large to draw"));
}
// An 8-bit image's 256-color palette is its last 769 bytes, `0x0C` and then the colors; the
// image data ends before it.
let tail = bytes.len().checked_sub(769).filter(|at| *at >= HEADER && (bits, planes) == (8, 1) && bytes.get(*at) == Some(&0x0c));
let data = unpack(bytes.get(HEADER..tail.unwrap_or(bytes.len())).unwrap_or_default(), scan * h)?;
let palette: Vec<[u8; 3]> = match (bits, planes) {
(8, 1) => match tail.and_then(|at| bytes.get(at + 1..)) {
Some(colors) => colors.as_chunks::<3>().0.to_vec(),
None => (0..=255).map(|v| [v; 3]).collect(),
},
// A 1-bit image takes the header palette's first two colors; versions without a palette,
// and headers whose two colors are the same (left unset), give black and white.
(1, 1) if matches!(version, 0 | 3) || head[16..19] == head[19..22] => vec![[0; 3], [255; 3]],
// A 2-bit image is a CGA picture, whose header holds CGA palette settings instead of colors.
(2, 1) => cga(head[16], head[19]),
_ if matches!(version, 0 | 3) => EGA.to_vec(),
_ => head[16..64].as_chunks::<3>().0.to_vec(),
};
let mut img = image::RgbaImage::new(w as u32, h as u32);
for (y, row) in data.chunks_exact(scan).enumerate() {
let plane = |p: usize| row.get(p * line..(p + 1) * line).unwrap_or_default();
for x in 0..w {
let rgb = if bits == 8 && planes == 3 {
[0, 1, 2].map(|p| plane(p).get(x).copied().unwrap_or(0))
} else {
// Plane p gives bits p × bits and up of the color index.
let index = (0..usize::from(planes)).fold(0, |index, p| {
let at = x * usize::from(bits);
let byte = plane(p).get(at / 8).copied().unwrap_or(0);
let value = (byte >> (8 - usize::from(bits) - at % 8)) & (0xff >> (8 - bits));
index | usize::from(value) << (p * usize::from(bits))
});
palette.get(index).copied().unwrap_or([0; 3])
};
img.put_pixel(x as u32, y as u32, image::Rgba([rgb[0], rgb[1], rgb[2], 255]));
}
}
Ok(img)
}
/// The four colors of a CGA picture from header bytes 16 and 19. The top four bits of `background`
/// are the EGA color of index 0. The top three bits of `settings` are the color burst, palette and
/// intensity bits (C, P, I), which pick the other three: with color burst on (C = 0), P = 0 gives
/// green, red and brown and P = 1 gives cyan, magenta and light gray; with it off (C = 1) they are
/// cyan, red and light gray. I = 1 gives the bright version of each (brown becomes yellow).
fn cga(background: u8, settings: u8) -> Vec<[u8; 3]> {
let foreground: [usize; 3] = match settings >> 6 {
0 => [2, 4, 6],
1 => [3, 5, 7],
_ => [3, 4, 7],
};
let bright = if settings & 0x20 != 0 { 8 } else { 0 };
let ega = |i: usize| EGA.get(i).copied().unwrap_or([0; 3]);
let mut colors = vec![ega(usize::from(background >> 4))];
colors.extend(foreground.map(|i| ega(i + bright)));
colors
}
/// `len` bytes of run-length encoded image data: a byte with its top two bits set repeats the
/// next byte as many times as its other six bits count, and any other byte stands for itself.
/// Runs may cross the end of a scan line.
fn unpack(data: &[u8], len: usize) -> Result<Vec<u8>, String> {
let mut out = Vec::with_capacity(len);
let mut bytes = data.iter();
while out.len() < len {
let &b = bytes.next().ok_or(TRUNCATED)?;
if b & 0xc0 == 0xc0 {
let &v = bytes.next().ok_or(TRUNCATED)?;
let n = usize::from(b & 0x3f).min(len - out.len());
out.resize(out.len() + n, v);
} else {
out.push(b);
}
}
Ok(out)
}
+4 -1
View File
@@ -1,5 +1,6 @@
//! Image resolution metadata (pixels per inch): PNG `pHYs`, JPEG JFIF density or EXIF, TIFF and
//! WebP EXIF resolution tags, BMP pixels per metre, a Photoshop document's ResolutionInfo. Place
//! WebP EXIF resolution tags, BMP pixels per metre, a Photoshop document's ResolutionInfo, a PCX
//! header's HDpi and VDpi. Place
//! sizes an image by it (72 ppi when the file has none); images converted to PNG on import keep it
//! as a `pHYs` chunk.
@@ -21,6 +22,8 @@ pub fn resolution(bytes: &[u8]) -> Option<(f64, f64)> {
bmp(bytes)
} else if super::psdread::is_psd(bytes) {
super::psdread::resolution(bytes)
} else if super::pcxread::is_pcx(bytes) {
super::pcxread::resolution(bytes)
} else {
None
};
@@ -0,0 +1,208 @@
//! Reading PCX files: every depth and plane layout decodes to the pixels it stores, the files open,
//! place and paste at the resolution their header gives, and damaged files are refused with a
//! message.
use serde_json::json;
use super::pcxread::{decode, is_pcx};
use super::*;
/// A PCX file of `version`, `bits` per pixel in `planes` planes, `w` × `h` pixels from (`x0`, 0),
/// `line` bytes per plane line, `dpi` both ways, the header palette `colors` (up to 16 entries),
/// then the encoded image data `data`.
#[allow(clippy::too_many_arguments)]
fn pcx(version: u8, bits: u8, planes: u8, x0: u16, (w, h): (u16, u16), line: u16, dpi: u16, colors: &[[u8; 3]], data: &[u8]) -> Vec<u8> {
let mut f = vec![0x0a, version, 1, bits];
for v in [x0, 0, x0.wrapping_add(w).wrapping_sub(1), h.wrapping_sub(1), dpi, dpi] {
f.extend(v.to_le_bytes());
}
let mut palette = [0; 48];
for (i, c) in colors.iter().take(16).enumerate() {
palette[i * 3..i * 3 + 3].copy_from_slice(c);
}
f.extend(palette);
f.extend([0, planes]);
f.extend(line.to_le_bytes());
f.extend(1u16.to_le_bytes());
f.resize(128, 0);
f.extend(data);
f
}
/// `raw` run-length encoded as the manual describes: runs of up to 63 bytes, and single bytes with
/// their top two bits set written as runs of one.
fn rle(raw: &[u8]) -> Vec<u8> {
let mut out = vec![];
let mut i = 0;
while i < raw.len() {
let n = raw[i..].iter().take(63).take_while(|b| **b == raw[i]).count();
if n > 1 || raw[i] >= 0xc0 {
out.extend([0xc0 | n as u8, raw[i]]);
} else {
out.push(raw[i]);
}
i += n;
}
out
}
/// The pixels `raster_image` stores for `f`, row by row.
fn pixels(f: &[u8]) -> Vec<[u8; 4]> {
let img = raster_image(f).unwrap();
image::load_from_memory(&img.blob.bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect()
}
/// The 16 colors of a header palette: entry `i` is (16 i, 255 − 16 i, i).
fn header_colors() -> Vec<[u8; 3]> {
(0..16u8).map(|i| [i * 16, 255 - i * 16, i]).collect()
}
fn opaque([r, g, b]: [u8; 3]) -> [u8; 4] {
[r, g, b, 255]
}
#[test]
fn every_depth_and_plane_layout_reads() {
let colors = header_colors();
let (white, black) = ([255; 4], [0, 0, 0, 255]);
// 1 bit, three pixels in a two-byte line: 101 and 010, in the header palette's first two colors
// (here inverted: white, then black).
let bits = rle(&[0b1010_0000, 0, 0b0100_0000, 0]);
let f = pcx(5, 1, 1, 0, (3, 2), 2, 0, &[[255; 3], [0; 3]], &bits);
assert_eq!(pixels(&f), [black, white, black, white, black, white]);
let f = pcx(5, 1, 1, 0, (3, 2), 2, 0, &colors, &bits);
let (zero, one) = (opaque(colors[0]), opaque(colors[1]));
assert_eq!(pixels(&f), [one, zero, one, zero, one, zero]);
// Black and white when the palette is left unset or the version has none.
for (version, palette) in [(5, &[][..]), (3, &colors[..])] {
let f = pcx(version, 1, 1, 0, (3, 2), 2, 0, palette, &bits);
assert_eq!(pixels(&f), [white, black, white, black, white, black], "version {version}");
}
// 1 bit in four planes: plane p gives bit p of the index; indices 5 and 10.
let planes = rle(&[0x80, 0, 0x40, 0, 0x80, 0, 0x40, 0]);
let f = pcx(5, 1, 4, 0, (2, 1), 2, 0, &colors, &planes);
assert_eq!(pixels(&f), [opaque(colors[5]), opaque(colors[10])], "the header palette");
// Versions 0 and 3 have no palette in their header: the EGA colors.
for version in [0, 3] {
let f = pcx(version, 1, 4, 0, (2, 1), 2, 0, &colors, &planes);
assert_eq!(pixels(&f), [[0xaa, 0, 0xaa, 255], [0x55, 0xff, 0x55, 255]], "version {version}");
}
// Two planes give four colors, three give eight: index 3 and 6 (planes 1 and 2).
let f = pcx(5, 1, 3, 0, (2, 1), 2, 0, &colors, &rle(&[0x80, 0, 0xc0, 0, 0x40, 0]));
assert_eq!(pixels(&f), [opaque(colors[3]), opaque(colors[6])]);
// 2 bits: indices 0 to 3 in one byte, the first pixel in the top bits, in the CGA colors that
// header bytes 16 (the background, in its top four bits) and 19 (C, P and I) pick.
let cga = |background: u8, settings: u8| {
let f = pcx(5, 2, 1, 0, (4, 1), 2, 0, &[[background, 0, 0], [settings, 0, 0]], &rle(&[0b0001_1011, 0]));
pixels(&f)
};
let (blue, cyan, magenta, light_gray) = ([0, 0, 0xaa, 255], [0, 0xaa, 0xaa, 255], [0xaa, 0, 0xaa, 255], [0xaa; 3]);
assert_eq!(cga(0x10, 0b0100_0000), [blue, cyan, magenta, opaque(light_gray)], "P = 1, dim");
let (light_green, light_red, yellow) = ([0x55, 0xff, 0x55, 255], [0xff, 0x55, 0x55, 255], [0xff, 0xff, 0x55, 255]);
assert_eq!(cga(0x0f, 0b0011_1111), [black, light_green, light_red, yellow], "P = 0, bright; low bits ignored");
assert_eq!(cga(0xf0, 0b1010_0000), [white, [0x55, 0xff, 0xff, 255], light_red, white], "C = 1, bright");
assert_eq!(cga(0, 0)[1..], [[0, 0xaa, 0, 255], [0xaa, 0, 0, 255], [0xaa, 0x55, 0, 255]], "P = 0, dim");
// 4 bits: indices 15, 1 and 7.
let f = pcx(5, 4, 1, 0, (3, 1), 2, 0, &colors, &rle(&[0xf1, 0x70]));
assert_eq!(pixels(&f), [opaque(colors[15]), opaque(colors[1]), opaque(colors[7])]);
// 8 bits with the 256-color palette after the image data, from a window that starts at x 10:
// entry i is (i, 255 − i, 7).
let mut f = pcx(5, 8, 1, 10, (2, 2), 2, 0, &[], &rle(&[0, 1, 254, 255]));
f.push(0x0c);
f.extend((0..=255u8).flat_map(|i| [i, 255 - i, 7]));
assert_eq!(pixels(&f), [[0, 255, 7, 255], [1, 254, 7, 255], [254, 1, 7, 255], [255, 0, 7, 255]]);
// Without it, the index is a gray level.
let f = pcx(5, 8, 1, 0, (2, 2), 2, 0, &[], &rle(&[0, 1, 254, 255]));
assert_eq!(pixels(&f), [[0, 0, 0, 255], [1, 1, 1, 255], [254, 254, 254, 255], [255; 4]]);
// A run may cross the end of a scan line.
let f = pcx(5, 8, 1, 0, (2, 2), 2, 0, &[], &[0xc4, 9]);
assert_eq!(pixels(&f), [[9, 9, 9, 255]; 4]);
// 24 bits: each line holds its red, green and blue planes in turn; the padding byte is skipped.
let f = pcx(5, 8, 3, 0, (1, 2), 2, 0, &[], &rle(&[10, 0, 30, 0, 50, 0, 20, 0, 40, 0, 60, 0]));
assert_eq!(pixels(&f), [[10, 30, 50, 255], [20, 40, 60, 255]]);
}
#[test]
fn pcx_files_open_place_and_paste_at_their_resolution() {
use crate::cmd::clipboard::{PASTE_ORDER, file_flavour};
let b64 = vectorcraft_format::base64_encode;
let f = pcx(5, 8, 3, 0, (2, 1), 2, 144, &[], &rle(&[10, 20, 30, 40, 50, 60]));
assert_eq!(ppi::resolution(&f), Some((144.0, 144.0)));
assert_eq!(ppi::resolution(&pcx(5, 8, 3, 0, (2, 1), 2, 0, &[], &[])), None, "no resolution");
let mut s = Session::new();
let r = s.execute("document.open", &json!({"name": "art.pcx", "dataBase64": b64(&f)})).unwrap();
assert_eq!((&r["format"], &r["warnings"]), (&json!("pcx"), &json!([])), "{r}");
assert_eq!(s.doc().unwrap().doc.artboards[0].rect.size(), vectorcraft_geom::Size::new(1.0, 0.5), "2 × 1 px at 144 ppi");
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let r = s.execute("file.place", &json!({"name": "ART.PCX", "dataBase64": b64(&f)})).unwrap();
assert_eq!((&r["format"], r["ids"].as_array().map(Vec::len)), (&json!("pcx"), Some(1)), "{r}");
// Copied in a file manager, a PCX file pastes as a bitmap.
assert_eq!(file_flavour("art.pcx", &f, &PASTE_ORDER), Some("image/x-pcx"));
let r = s.execute("clipboard.importImage", &json!({"mime": "image/x-pcx", "dataBase64": b64(&f)})).unwrap();
assert_eq!((&r["count"], &r["pixelWidth"], &r["pixelHeight"]), (&json!(1), &json!(2), &json!(1)), "{r}");
// Glass's Load Texture… reads it too.
assert!(texture_image(&f).is_ok());
// The dialogs list PCX; the desktop packaging doesn't associate it; nothing writes it.
assert!(OPEN_EXTS.contains(&"pcx") && PLACE_EXTS.contains(&"pcx") && UNASSOCIATED_EXTS.contains(&"pcx"));
let filter = ("PCX", &["pcx"][..]);
assert!(open_filters().any(|f| f == filter) && place_filters().any(|f| f == filter));
let r = s.execute("document.formats", &json!({})).unwrap();
let pcx_format = r["formats"].as_array().unwrap().iter().find(|f| f["id"] == "pcx").unwrap().clone();
assert_eq!((pcx_format["read"].as_bool(), pcx_format["write"].as_bool()), (Some(true), Some(false)));
assert_eq!(format_for_name("a.pcx").map(|f| f.id), Some("pcx"));
}
#[test]
fn damaged_and_unsupported_files_are_refused_with_a_message() {
let message = |f: &[u8]| raster_image(f).err().expect("refused").to_string();
let gray = pcx(5, 8, 1, 0, (2, 2), 2, 0, &[], &[1, 2, 3, 4]);
// A header or image data cut short.
assert_eq!(message(&gray[..60]), "the PCX file is cut short");
assert_eq!(message(&gray[..131]), "the PCX file is cut short");
assert_eq!(message(&pcx(5, 8, 1, 0, (2, 2), 2, 0, &[], &[1, 0xc2])), "the PCX file is cut short", "a run without its byte");
// The palette after the image data isn't read as pixels the image data lacks.
let mut short = pcx(5, 8, 1, 0, (2, 2), 2, 0, &[], &[1, 2, 3]);
short.push(0x0c);
short.extend([7; 768]);
assert_eq!(message(&short), "the PCX file is cut short", "three pixels and a palette");
let mut s = Session::new();
let e = s.execute("document.open", &json!({"name": "a.pcx", "dataBase64": vectorcraft_format::base64_encode(&gray[..131])}));
assert!(e.expect_err("refused").to_string().contains("the PCX file is cut short"));
// Planes and depths the manual doesn't define.
assert_eq!(message(&pcx(5, 8, 2, 0, (2, 2), 2, 0, &[], &[])), "PCX files with 8-bit pixels in 2 planes can't be read");
assert_eq!(message(&pcx(5, 4, 3, 0, (2, 2), 2, 0, &[], &[])), "PCX files with 4-bit pixels in 3 planes can't be read");
assert_eq!(message(&pcx(5, 1, 5, 0, (2, 2), 2, 0, &[], &[])), "PCX files with 1-bit pixels in 5 planes can't be read");
// A window that ends before it starts, and lines too short for the width.
let mut backwards = gray.clone();
backwards[8..10].copy_from_slice(&0u16.to_le_bytes());
backwards[4..6].copy_from_slice(&5u16.to_le_bytes());
assert_eq!(message(&backwards), "the PCX file's header is damaged");
assert_eq!(message(&pcx(5, 8, 1, 0, (3, 1), 2, 0, &[], &[1, 2, 3, 4])), "the PCX file's header is damaged");
assert_eq!(message(&pcx(5, 1, 1, 0, (17, 1), 2, 0, &[], &[0; 4])), "the PCX file's header is damaged");
// Past the allowance, from the header: the pixels as RGBA, and the scan lines of a narrow image
// with long lines.
let huge = pcx(5, 1, 1, 0, (0, 0), 8192, 0, &[], &[]);
assert_eq!(message(&huge), "an image of 65536 × 65536 pixels is too large to draw");
let long = pcx(5, 8, 3, 0, (1, 0), 65535, 0, &[], &[]);
assert_eq!(message(&long), "an image of 1 × 65536 pixels is too large to draw");
assert!(decode(&long, 1 << 20).is_err());
// Headers with values the manual doesn't define aren't PCX files.
assert!(is_pcx(&gray));
for bad in [[0x0a, 1, 1, 8], [0x0a, 6, 1, 8], [0x0a, 5, 0, 8], [0x0a, 5, 1, 3], [0x0b, 5, 1, 8]] {
assert!(!is_pcx(&bad), "{bad:?}");
}
}