mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 06:07:06 -06:00
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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user