From f32699bea7c2e0c5ebf465faa74abe60b8ff5336 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 27 Oct 2026 23:03:57 -0700 Subject: [PATCH 1/2] File formats: PCX (.pcx) files open and place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/engine/src/cmd/fileio.rs | 4 + crates/engine/src/cmd/fileio/formats.rs | 12 +- crates/engine/src/cmd/fileio/load.rs | 5 + crates/engine/src/cmd/fileio/pcxread.rs | 151 +++++++++++++ crates/engine/src/cmd/fileio/ppi.rs | 5 +- crates/engine/src/cmd/fileio/tests_pcxread.rs | 208 ++++++++++++++++++ crates/engine/tests/import_fuzz.rs | 54 +++++ crates/ui-egui/src/i18n/cs.tsv | 3 + crates/ui-egui/src/i18n/de.tsv | 3 + crates/ui-egui/src/i18n/el.tsv | 3 + crates/ui-egui/src/i18n/es.tsv | 3 + crates/ui-egui/src/i18n/fi.tsv | 3 + crates/ui-egui/src/i18n/fr.tsv | 3 + crates/ui-egui/src/i18n/hu.tsv | 3 + crates/ui-egui/src/i18n/it.tsv | 3 + crates/ui-egui/src/i18n/ja.tsv | 3 + crates/ui-egui/src/i18n/ka.tsv | 3 + crates/ui-egui/src/i18n/ko.tsv | 3 + crates/ui-egui/src/i18n/pl.tsv | 3 + crates/ui-egui/src/i18n/pt-br.tsv | 3 + crates/ui-egui/src/i18n/ru.tsv | 3 + crates/ui-egui/src/i18n/sv.tsv | 3 + crates/ui-egui/src/i18n/tr.tsv | 3 + crates/ui-egui/src/i18n/uk.tsv | 3 + crates/ui-egui/src/i18n/vi.tsv | 3 + crates/ui-egui/src/i18n/zh-hans.tsv | 3 + crates/ui-egui/src/i18n/zh-hant.tsv | 3 + docs/file-format-parity.md | 5 +- docs/mcp.md | 4 +- docs/releasing.md | 2 +- docs/target-app-parity.md | 5 +- 31 files changed, 503 insertions(+), 12 deletions(-) create mode 100644 crates/engine/src/cmd/fileio/pcxread.rs create mode 100644 crates/engine/src/cmd/fileio/tests_pcxread.rs diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index 6893c6f84..c8cb7218a 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -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; diff --git a/crates/engine/src/cmd/fileio/formats.rs b/crates/engine/src/cmd/fileio/formats.rs index fbf9efe92..f72721534 100644 --- a/crates/engine/src/cmd/fileio/formats.rs +++ b/crates/engine/src/cmd/fileio/formats.rs @@ -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", diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index ecb1c3099..cf1ecc6a8 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -561,6 +561,8 @@ pub(crate) fn size_note(bytes: &[u8]) -> Option { /// 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 { 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); diff --git a/crates/engine/src/cmd/fileio/pcxread.rs b/crates/engine/src/cmd/fileio/pcxread.rs new file mode 100644 index 000000000..ec8f1801b --- /dev/null +++ b/crates/engine/src/cmd/fileio/pcxread.rs @@ -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 { + 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, 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) +} diff --git a/crates/engine/src/cmd/fileio/ppi.rs b/crates/engine/src/cmd/fileio/ppi.rs index 1c04db9c8..61b741756 100644 --- a/crates/engine/src/cmd/fileio/ppi.rs +++ b/crates/engine/src/cmd/fileio/ppi.rs @@ -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 }; diff --git a/crates/engine/src/cmd/fileio/tests_pcxread.rs b/crates/engine/src/cmd/fileio/tests_pcxread.rs new file mode 100644 index 000000000..e951e31ac --- /dev/null +++ b/crates/engine/src/cmd/fileio/tests_pcxread.rs @@ -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 { + 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 { + 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:?}"); + } +} diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 6e29818d3..81a728930 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -1719,6 +1719,60 @@ proptest! { } } +/// PCX files of each layout, 3 × 2 pixels at 150 dpi: 1 bit in two colors, 1 bit in four planes, +/// 2 bits with CGA palette settings, 4 bits with the header palette, 8 bits with the 256-color palette after the image data, +/// and 24 bits in three planes, with runs that cross a scan line. +fn pcx_samples() -> Vec<(&'static str, Vec)> { + let pcx = |bits: u8, planes: u8, line: u16, data: &[u8]| { + let mut f = vec![0x0a, 5, 1, bits, 0, 0, 0, 0, 2, 0, 1, 0, 150, 0, 150, 0]; + f.extend((0..48u8).map(|i| i.wrapping_mul(37))); + f.extend([0, planes]); + f.extend(line.to_le_bytes()); + f.extend(1u16.to_le_bytes()); + f.resize(128, 0); + f.extend(data); + f + }; + let mut paletted = pcx(8, 1, 4, &[0xc5, 7, 0xc1, 0xff, 1, 2]); + paletted.push(0x0c); + paletted.extend((0..=255u8).flat_map(|i| [i, 255 - i, i / 2])); + vec![ + ("a.pcx", pcx(1, 1, 2, &[0xa0, 0, 0x40, 0])), + ("b.pcx", pcx(1, 4, 2, &[0xc8, 0xe0, 0x20, 0, 0x60, 0, 0xc4, 0x80])), + ("c.pcx", pcx(2, 1, 2, &[0x1b, 0, 0xc2, 0xe4])), + ("d.pcx", pcx(4, 1, 2, &[0xf1, 0x70, 0x23, 0x40])), + ("e.pcx", paletted), + ("f.pcx", pcx(8, 3, 4, &[0xc9, 200, 0xcf, 40])), + ] +} + +proptest! { + #![proptest_config(config())] + + /// Mutated PCX files (header fields, run-length bytes and the palette after the image data) + /// never crash placing, opening or pasting them. + #[test] + fn mutated_pcx_files_place_without_panics(which in 0usize..6, cut in 0usize..960, edits in prop::collection::vec((0usize..960, any::()), 0..12)) { + let (name, mut bytes) = pcx_samples().swap_remove(which); + for (at, b) in edits { + if let Some(x) = bytes.get_mut(at) { + *x = b; + } + } + bytes.truncate(cut.max(4)); + let r = catch_quiet(|| { + let mut s = vectorcraft_engine::Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + let p = json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes), "thumbnail": 8}); + let _ = no_panic(s.execute("file.place.info", &p)); + let _ = no_panic(s.execute("file.place", &p)); + let _ = no_panic(s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes)}))); + let _ = no_panic(s.execute("clipboard.importImage", &json!({"dataBase64": vectorcraft_format::base64_encode(&bytes)}))); + }); + prop_assert!(r.is_ok(), "{name}: panicked: {:?}", r.err()); + } +} + // ---------- SVG saved with hidden layers and editing data ---------- /// The rich document with a hidden layer, saved as SVG with its editing data. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 7aa201da0..18aef48aa 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4870,6 +4870,9 @@ imageTrace Outline Obrys @msg not a PSD file nejde o soubor PSD @msg the PSD file has an unknown colour mode ({m}) soubor PSD má neznámý barevný režim ({m}) @msg the PSD file's image data uses an unknown compression ({m}) obrazová data souboru PSD používají neznámou kompresi ({m}) +@msg the PCX file is cut short soubor PCX je useknutý +@msg the PCX file's header is damaged hlavička souboru PCX je poškozená +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read soubory PCX s {bits}bitovými obrazovými body v {planes} rovinách nelze přečíst @msg the PSD image is too large to read ({_1} × {_2} pixels) obraz PSD je příliš velký na přečtení ({_1} × {_2} obrazových bodů) @msg zipped 32-bit PSD image data with prediction can't be read zazipovaná 32bitová obrazová data PSD s predikcí nelze přečíst @msg {depth}-bit PSD files in this colour mode can't be read {depth}bitové soubory PSD v tomto barevném režimu nelze přečíst diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 54e8c7c93..28af9abf7 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4610,6 +4610,9 @@ theme Appearance Erscheinungsbild @msg the PSD file's header is damaged der Header der PSD-Datei ist beschädigt @msg the PSD file's image data can't be read: {e} die Bilddaten der PSD-Datei können nicht gelesen werden: {e} @msg the PSD file's image data uses an unknown compression ({m}) die Bilddaten der PSD-Datei verwenden eine unbekannte Komprimierung ({m}) +@msg the PCX file is cut short die PCX-Datei ist unvollständig +@msg the PCX file's header is damaged der Dateikopf der PCX-Datei ist beschädigt +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read PCX-Dateien mit {bits}-Bit-Pixeln in {planes} Ebenen können nicht gelesen werden @msg the PSD image is too large to read ({_1} × {_2} pixels) das PSD-Bild ist zu groß zum Lesen ({_1} × {_2} Pixel) @msg zipped 32-bit PSD image data with prediction can't be read ZIP-komprimierte 32-Bit-PSD-Bilddaten mit Vorhersage können nicht gelesen werden @msg {depth}-bit PSD files in this colour mode can't be read {depth}-Bit-PSD-Dateien in diesem Farbmodus können nicht gelesen werden diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index dbf8ff595..e754c2acf 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4551,6 +4551,9 @@ theme Appearance Εμφάνιση @msg not a PSD file δεν είναι αρχείο PSD @msg the PSD file has an unknown colour mode ({m}) το αρχείο PSD χρησιμοποιεί άγνωστη λειτουργία χρώματος ({m}) @msg the PSD file's image data uses an unknown compression ({m}) τα δεδομένα εικόνας PSD χρησιμοποιούν άγνωστη συμπίεση ({m}) +@msg the PCX file is cut short το αρχείο PCX είναι περικομμένο +@msg the PCX file's header is damaged η κεφαλίδα του αρχείου PCX είναι κατεστραμμένη +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read τα αρχεία PCX με εικονοστοιχεία {bits} bit σε {planes} επίπεδα δεν διαβάζονται @msg the PSD image is too large to read ({_1} × {_2} pixels) η εικόνα PSD είναι πολύ μεγάλη για ανάγνωση ({_1} × {_2} εικονοστοιχεία) @msg zipped 32-bit PSD image data with prediction can't be read τα συμπιεσμένα δεδομένα εικόνας PSD 32 bit με πρόβλεψη δεν διαβάζονται @msg {depth}-bit PSD files in this colour mode can't be read τα αρχεία PSD {depth} bit δεν διαβάζονται σε αυτήν τη λειτουργία χρώματος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index b22a812ca..ed7843eba 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4591,6 +4591,9 @@ axis Both Ambos @msg not a PSD file no es un documento PSD @msg the PSD file has an unknown colour mode ({m}) el archivo PSD tiene un modo de color desconocido ({m}) @msg the PSD file's image data uses an unknown compression ({m}) los datos de imagen del archivo PSD usan una compresión desconocida ({m}) +@msg the PCX file is cut short el archivo PCX está incompleto +@msg the PCX file's header is damaged el encabezado del archivo PCX está dañado +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read los archivos PCX con píxeles de {bits} bits en {planes} planos no se pueden leer @msg the PSD image is too large to read ({_1} × {_2} pixels) la imagen PSD es demasiado grande para leerla ({_1} × {_2} píxeles) @msg zipped 32-bit PSD image data with prediction can't be read no se pueden leer datos de imagen PSD de 32 bits comprimidos en ZIP con predicción @msg {depth}-bit PSD files in this colour mode can't be read los archivos PSD de {depth} bits en este modo de color no se pueden leer diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 4e1981a58..303ac4ced 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4773,6 +4773,9 @@ imageTrace Outline Ääriviiva @msg not a PSD file ei PSD-tiedosto @msg the PSD file has an unknown colour mode ({m}) tämän PSD-tiedoston väritila on tuntematon ({m}) @msg the PSD file's image data uses an unknown compression ({m}) tämän PSD-tiedoston kuvadata käyttää tuntematonta pakkausta ({m}) +@msg the PCX file is cut short tämä PCX-tiedosto on katkennut +@msg the PCX file's header is damaged PCX-tiedoston otsake on vahingoittunut +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read PCX-tiedostoja, joissa on {bits}-bittisiä pikseleitä {planes} tasossa, ei voi lukea @msg the PSD image is too large to read ({_1} × {_2} pixels) tämä PSD-kuva on liian suuri luettavaksi ({_1} × {_2} pikseliä) @msg zipped 32-bit PSD image data with prediction can't be read ennustusta käyttävää zip-pakattua 32-bittistä PSD-kuvadataa ei voi lukea @msg {depth}-bit PSD files in this colour mode can't be read tässä väritilassa olevia {depth}-bittisiä PSD-tiedostoja ei voi lukea diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index ac49fcf19..d5856320b 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4602,6 +4602,9 @@ axis Both Les deux @msg not a PSD file ce n'est pas un document PSD @msg the PSD file has an unknown colour mode ({m}) le fichier PSD a un mode colorimétrique inconnu ({m}) @msg the PSD file's image data uses an unknown compression ({m}) les données d'image du fichier PSD utilisent une compression inconnue ({m}) +@msg the PCX file is cut short le fichier PCX est tronqué +@msg the PCX file's header is damaged l'en-tête du fichier PCX est endommagé +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read les fichiers PCX à pixels de {bits} bits sur {planes} plans ne peuvent pas être lus @msg the PSD image is too large to read ({_1} × {_2} pixels) l'image PSD est trop grande pour être lue ({_1} × {_2} pixels) @msg zipped 32-bit PSD image data with prediction can't be read les données d'image PSD 32 bits compressées en ZIP avec prédiction ne peuvent pas être lues @msg {depth}-bit PSD files in this colour mode can't be read les fichiers PSD {depth} bits dans ce mode colorimétrique ne peuvent pas être lus diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 271fd9f0e..aa7629048 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5114,6 +5114,9 @@ imageTrace Outline Körvonal @msg the PSD file's header is damaged a PSD-fájl fejléce sérült @msg the PSD file's image data can't be read: {e} a PSD-fájl képadatai nem olvashatók: {e} @msg the PSD file's image data uses an unknown compression ({m}) a PSD-fájl képadatai ismeretlen tömörítést használnak ({m}) +@msg the PCX file is cut short a PCX-fájl csonka +@msg the PCX file's header is damaged a PCX-fájl fejléce sérült +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read a {bits} bites pixeleket {planes} síkban tároló PCX-fájlok nem olvashatók @msg the PSD image is too large to read ({_1} × {_2} pixels) a PSD-kép túl nagy a beolvasáshoz ({_1} × {_2} pixel) @msg the Save PDF dialog didn't open a PDF mentése párbeszédablak nem nyílt meg @msg the scale is not a usable number a méretarány nem használható szám diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 489924442..1c575f931 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4631,6 +4631,9 @@ axis Both Entrambi @msg not a PSD file non è un documento PSD @msg the PSD file has an unknown colour mode ({m}) il file PSD ha un metodo colore sconosciuto ({m}) @msg the PSD file's image data uses an unknown compression ({m}) i dati immagine del file PSD usano una compressione sconosciuta ({m}) +@msg the PCX file is cut short il file PCX è troncato +@msg the PCX file's header is damaged l'intestazione del file PCX è danneggiata +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read i file PCX con pixel a {bits} bit in {planes} piani non possono essere letti @msg the PSD image is too large to read ({_1} × {_2} pixels) l'immagine PSD è troppo grande per essere letta ({_1} × {_2} pixel) @msg zipped 32-bit PSD image data with prediction can't be read i dati immagine PSD a 32 bit compressi ZIP con previsione non possono essere letti @msg {depth}-bit PSD files in this colour mode can't be read i file PSD a {depth} bit in questo metodo colore non possono essere letti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 087d51a00..fd21ed98b 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4669,6 +4669,9 @@ swatch Bright Violet ビビッドバイオレット @msg not a PSD file PSD ドキュメントではありません @msg the PSD file has an unknown colour mode ({m}) PSD ファイルのカラーモードが不明です({m}) @msg the PSD file's image data uses an unknown compression ({m}) PSD ファイルの画像データの圧縮方式が不明です({m}) +@msg the PCX file is cut short PCX ファイルが途中で切れています +@msg the PCX file's header is damaged PCX ファイルのヘッダーが壊れています +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read {planes} プレーンに {bits} bit のピクセルを持つ PCX ファイルは読み込めません @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 画像が大きすぎて読み込めません({_1} × {_2} ピクセル) @msg zipped 32-bit PSD image data with prediction can't be read 予測付き ZIP 圧縮の 32 bit PSD 画像データは読み込めません @msg {depth}-bit PSD files in this colour mode can't be read このカラーモードの {depth} bit PSD ファイルは読み込めません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index b5eab9051..1dde0123b 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4311,6 +4311,9 @@ font-technology Variable ცვლადი @msg not a PSD file PSD ფაილი არ არის @msg the PSD file has an unknown colour mode ({m}) PSD ფაილს უცნობი ფერის რეჟიმი აქვს ({m}) @msg the PSD file's image data uses an unknown compression ({m}) PSD ფაილის გამოსახულების მონაცემები უცნობ შეკუმშვას იყენებს ({m}) +@msg the PCX file is cut short PCX ფაილი შეკვეცილია +@msg the PCX file's header is damaged PCX ფაილის სათაური დაზიანებულია +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read {planes} სიბრტყეში {bits}-ბიტიანი პიქსელების მქონე PCX ფაილების წაკითხვა ვერ ხერხდება @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD გამოსახულება ზედმეტად დიდია წასაკითხად ({_1} × {_2} პიქსელი) @msg zipped 32-bit PSD image data with prediction can't be read zip-ით შეკუმშული 32-ბიტიანი PSD გამოსახულების მონაცემების წაკითხვა პროგნოზირებით ვერ ხერხდება @msg {depth}-bit PSD files in this colour mode can't be read ამ ფერის რეჟიმში {depth}-ბიტიანი PSD ფაილების წაკითხვა ვერ ხერხდება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index a2c7e0f6a..4e40defb7 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4601,6 +4601,9 @@ theme Appearance 외관 @msg not a PSD file PSD 파일이 아닙니다 @msg the PSD file has an unknown colour mode ({m}) PSD 파일의 색상 모드({m})를 알 수 없습니다 @msg the PSD file's image data uses an unknown compression ({m}) PSD 파일의 이미지 데이터가 알 수 없는 압축({m})을 사용합니다 +@msg the PCX file is cut short PCX 파일이 잘려 있습니다 +@msg the PCX file's header is damaged PCX 파일의 헤더가 손상되었습니다 +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read {planes}개 평면에 {bits}비트 픽셀이 있는 PCX 파일은 읽을 수 없습니다 @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 이미지가 너무 커서 읽을 수 없습니다({_1} × {_2}픽셀) @msg zipped 32-bit PSD image data with prediction can't be read 예측이 적용된 ZIP 압축 32비트 PSD 이미지 데이터는 읽을 수 없습니다 @msg {depth}-bit PSD files in this colour mode can't be read 이 색상 모드의 {depth}비트 PSD 파일은 읽을 수 없습니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 3e8fe7459..f901a5709 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4697,6 +4697,9 @@ imageTrace Outline Kontur @msg not a PSD file to nie jest plik PSD @msg the PSD file has an unknown colour mode ({m}) plik PSD ma nieznany tryb koloru ({m}) @msg the PSD file's image data uses an unknown compression ({m}) dane obrazu pliku PSD używają nieznanej kompresji ({m}) +@msg the PCX file is cut short plik PCX jest ucięty +@msg the PCX file's header is damaged nagłówek pliku PCX jest uszkodzony +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read nie można odczytać plików PCX z {bits}-bitowymi pikselami w {planes} płaszczyznach @msg the PSD image is too large to read ({_1} × {_2} pixels) obraz PSD jest zbyt duży, aby go odczytać ({_1} × {_2} pikseli) @msg zipped 32-bit PSD image data with prediction can't be read nie można odczytać spakowanych 32-bitowych danych obrazu PSD z predykcją @msg {depth}-bit PSD files in this colour mode can't be read nie można odczytać {depth}-bitowych plików PSD w tym trybie koloru diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 603df2bde..9f353d453 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4134,6 +4134,9 @@ axis Both Ambos @msg not a PSD file não é um arquivo PSD @msg the PSD file has an unknown colour mode ({m}) o arquivo PSD tem um modo de cor desconhecido ({m}) @msg the PSD file's image data uses an unknown compression ({m}) os dados de imagem do arquivo PSD usam uma compactação desconhecida ({m}) +@msg the PCX file is cut short o arquivo PCX está incompleto +@msg the PCX file's header is damaged o cabeçalho do arquivo PCX está danificado +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read arquivos PCX com pixels de {bits} bits em {planes} planos não podem ser lidos @msg the PSD image is too large to read ({_1} × {_2} pixels) a imagem PSD é grande demais para ser lida ({_1} × {_2} pixels) @msg zipped 32-bit PSD image data with prediction can't be read dados de imagem PSD de 32 bits compactados em zip com previsão não podem ser lidos @msg {depth}-bit PSD files in this colour mode can't be read arquivos PSD de {depth} bits neste modo de cor não podem ser lidos diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8ab9549c3..ea23c6867 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4629,6 +4629,9 @@ axis Both Оба @msg not a PSD file это не документ PSD @msg the PSD file has an unknown colour mode ({m}) у файла PSD неизвестный цветовой режим ({m}) @msg the PSD file's image data uses an unknown compression ({m}) данные изображения файла PSD используют неизвестное сжатие ({m}) +@msg the PCX file is cut short файл PCX обрезан +@msg the PCX file's header is damaged заголовок файла PCX повреждён +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read файлы PCX с {bits}-битными пикселями в {planes} плоскостях не читаются @msg the PSD image is too large to read ({_1} × {_2} pixels) изображение PSD слишком велико для чтения ({_1} × {_2} пикс.) @msg zipped 32-bit PSD image data with prediction can't be read 32-битные данные изображения PSD со сжатием ZIP с предсказанием не читаются @msg {depth}-bit PSD files in this colour mode can't be read {depth}-битные файлы PSD в этом цветовом режиме не читаются diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 99854d41a..9bdf3210a 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4508,6 +4508,9 @@ axis Both Båda @msg not a PSD file inte en PSD-fil @msg the PSD file has an unknown colour mode ({m}) PSD-filen har ett okänt färgläge ({m}) @msg the PSD file's image data uses an unknown compression ({m}) PSD-filens bilddata använder en okänd komprimering ({m}) +@msg the PCX file is cut short PCX-filen är avkortad +@msg the PCX file's header is damaged PCX-filens huvud är skadat +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read PCX-filer med {bits}-bitars pixlar i {planes} plan kan inte läsas @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD-bilden är för stor för att läsas ({_1} × {_2} pixlar) @msg zipped 32-bit PSD image data with prediction can't be read zippad 32-bitars PSD-bilddata med prediktion kan inte läsas @msg {depth}-bit PSD files in this colour mode can't be read {depth}-bitars PSD-filer i det här färgläget kan inte läsas diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 6e7a4b46b..78bd846bd 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4352,6 +4352,9 @@ axis Both Her İkisi @msg not a PSD file PSD dosyası değil @msg the PSD file has an unknown colour mode ({m}) PSD dosyasında bilinmeyen bir renk modu var ({m}) @msg the PSD file's image data uses an unknown compression ({m}) PSD dosyasının görüntü verileri bilinmeyen bir sıkıştırma kullanıyor ({m}) +@msg the PCX file is cut short PCX dosyası kısa kesildi +@msg the PCX file's header is damaged PCX dosyasının başlığı bozuk +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read {planes} düzlemde {bits}-bit pikseller içeren PCX dosyaları okunamaz @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD görüntüsü okunamayacak kadar büyük ({_1} × {_2} piksel) @msg zipped 32-bit PSD image data with prediction can't be read Tahmin içeren sıkıştırılmış 32 bit PSD görüntü verileri okunamıyor @msg {depth}-bit PSD files in this colour mode can't be read Bu renk modunda {depth}-bit PSD dosyaları okunamaz diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index c40f4c770..7bede63d7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4615,6 +4615,9 @@ axis Both Обидві @msg not a PSD file це не документ PSD @msg the PSD file has an unknown colour mode ({m}) файл PSD має невідомий колірний режим ({m}) @msg the PSD file's image data uses an unknown compression ({m}) дані зображення файлу PSD використовують невідоме стиснення ({m}) +@msg the PCX file is cut short файл PCX обрізаний +@msg the PCX file's header is damaged заголовок файлу PCX пошкоджений +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read файли PCX з {bits}-бітними пікселями в {planes} площинах не читаються @msg the PSD image is too large to read ({_1} × {_2} pixels) зображення PSD завелике для читання ({_1} × {_2} пікс.) @msg zipped 32-bit PSD image data with prediction can't be read 32-бітні дані зображення PSD зі стисненням ZIP із передбаченням не читаються @msg {depth}-bit PSD files in this colour mode can't be read {depth}-бітні файли PSD у цьому колірному режимі не читаються diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index a75d32b96..67ffb71f8 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4771,6 +4771,9 @@ theme Appearance Giao diện @msg the PSD file's header is damaged tiêu đề tệp PSD bị hỏng @msg the PSD file's image data can't be read: {e} dữ liệu ảnh tệp PSD không thể đọc được: {e} @msg the PSD file's image data uses an unknown compression ({m}) dữ liệu ảnh tệp PSD dùng nén không xác định ({m}) +@msg the PCX file is cut short tệp PCX bị cắt ngắn +@msg the PCX file's header is damaged phần đầu của tệp PCX bị hỏng +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read tệp PCX có điểm ảnh {bits}-bit trong {planes} mặt phẳng không thể đọc được @msg the PSD image is too large to read ({_1} × {_2} pixels) ảnh PSD quá lớn để đọc ({_1} × {_2} pixel) @msg the Save PDF dialog didn't open hộp thoại Lưu PDF không mở @msg the scale is not a usable number tỷ lệ không phải là số dùng được diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 6351f5704..feb3f46dd 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4387,6 +4387,9 @@ imageTrace Outline 轮廓 @msg not a PSD file 不是 PSD 文件 @msg the PSD file has an unknown colour mode ({m}) PSD 文件的颜色模式未知 ({m}) @msg the PSD file's image data uses an unknown compression ({m}) PSD 文件的图像数据使用了未知的压缩方式 ({m}) +@msg the PCX file is cut short PCX 文件不完整 +@msg the PCX file's header is damaged PCX 文件头已损坏 +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read 无法读取 {planes} 个平面中含 {bits} 位像素的 PCX 文件 @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 图像过大,无法读取 ({_1} × {_2} 像素) @msg zipped 32-bit PSD image data with prediction can't be read 无法读取带预测的 ZIP 压缩 32 位 PSD 图像数据 @msg {depth}-bit PSD files in this colour mode can't be read 无法读取此颜色模式下的 {depth} 位 PSD 文件 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 1b076e629..f9ec0be10 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2326,6 +2326,9 @@ file-filter Templates 範本 @msg not a PSD file 不是 PSD 檔案 @msg the PSD file has an unknown colour mode ({m}) PSD 檔案的色彩模式不明 ({m}) @msg the PSD file's image data uses an unknown compression ({m}) PSD 檔案的影像資料使用了不明的壓縮方式 ({m}) +@msg the PCX file is cut short PCX 檔案不完整 +@msg the PCX file's header is damaged PCX 檔案標頭已損毀 +@msg PCX files with {bits}-bit pixels in {planes} planes can't be read 無法讀取 {planes} 個平面中含 {bits} 位元像素的 PCX 檔案 @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 影像過大,無法讀取 ({_1} × {_2} 像素) @msg zipped 32-bit PSD image data with prediction can't be read 無法讀取使用預測的 ZIP 壓縮 32 位元 PSD 影像資料 @msg {depth}-bit PSD files in this colour mode can't be read 無法讀取此色彩模式的 {depth} 位元 PSD 檔案 diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index eb54c8df2..a4fa06a83 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -17,7 +17,7 @@ from the public specifications, and users' own `.ai` files are used locally only | | Illustrator | VectorCraft | Notes | |---|---:|---:|---| -| Formats opened or placed | ~30 | 25 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, JPEG 2000, PICT, PCX, Pixar, Word `.doc`, PS | +| Formats opened or placed | ~30 | 26 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, JPEG 2000, PICT, Pixar, Word `.doc`, PS | | Formats saved (Save As) | 6 (AI, AIT, PDF, EPS, SVG, SVGZ) | 6 + native | `.ai` written PDF-compatible (no Illustrator editing data) or as legacy version 7 (layers and groups, one artboard per file) | | Formats exported | ~16 | 17 | missing: DWG, 3D (OBJ/USDA/glTF); extra: GIF/PNG-8 palette export, DXF up to 2018, layered PSD | | **Overall (weighted by use)** | | **~75%** | `.ai` and PDF dominate the weighting | @@ -106,7 +106,8 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | WebP | export (Export for Screens) | yes | yes (lossless only; lossy written lossless with a warning) | | | HEIC / HEIF | open, place | no | no | | | JPEG 2000 | open, place | no | no | | -| PICT, PCX, Pixar PXR | open, place (legacy) | no | no | rarely used | +| PCX | open, place | yes (1 bit in the header palette's first two colors, black and white without them; 1 bit in two to four planes and 4 bits with up to 16 colors; 2 bits in the CGA colors the header's CGA palette settings pick; 8 bits with the 256-color palette, gray without one; 24-bit color in three planes) | no | the header's HDpi and VDpi size the image; a file of another depth or plane count, cut short or past the pixel allowance is refused with a message; tests in `crates/engine/src/cmd/fileio/tests_pcxread.rs` and `crates/engine/tests/import_fuzz.rs` | +| PICT, Pixar PXR | open, place (legacy) | no | no | rarely used | | FreeHand `.fh*` | open (legacy) | no | no | | | CorelDRAW `.cdr` | open | no | no | | | Text `.txt` | place, export | place (Text Import Options) | yes | | diff --git a/docs/mcp.md b/docs/mcp.md index 6ddf4d843..775461bd5 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -422,7 +422,7 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr | `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. | | `open_panel` | `{panel}` | Remote only. `panel` is a panel id (`layers`, `swatches`, `colorGuide`, …, as `window.panel` takes) or its display label (`"Color Guide"`), in any case. | | `screenshot` | `{path?, scale?, artboard?, window?, max_side?, traceViews?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard with the pixels of a PNG `export` with `background: "white"` at the same scale; `traceViews:true` draws Image Trace objects in the view set with `imageTrace.setView`, as in the app's window; `window:true` captures the app window (remote only), as `ui_screenshot` does, and `max_side` (1–4096, window captures only) scales the returned image down as described for `ui_screenshot`. With `path` the backend (the headless session or the app) writes the PNG there too; a window capture written there keeps its full size with `max_side`. | -| `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, PNG, JPEG, GIF, WebP, TIFF, BMP, Targa (`.tga`/`.vda`/`.icb`/`.vst`), PSD/PSB (an image opens as a document of its pixel size; a Photoshop file as its merged image). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). `run_command document.formats` lists the formats. | +| `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, PNG, JPEG, GIF, WebP, TIFF, BMP, Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, PSD/PSB (an image opens as a document of its pixel size; a Photoshop file as its merged image). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). `run_command document.formats` lists the formats. | | `save_file` | `{path?, compress?, version?, preview?, options?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), and `preview: true` embeds a PNG of the first artboard. `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version` or `preview` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version` or `preview`). `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warning for a top-level `compress`, `version` or `preview`. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. | | `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG and SVGZ write one file per chosen artboard. EPS writes one file of the visible art; with `artboard`, `range`, `options.artboards` or `options: {"useArtboards": true}` it writes one file per chosen artboard, after a central file of the visible art when there are two or more, and `useArtboards: false` keeps the one file. Rasters, DXF, EMF and WMF write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `artboards`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. | | `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. | @@ -3559,7 +3559,7 @@ image `object.rasterize` makes with the same options. ## Glass textures Glass (`distort.glass`) takes an image file as its surface in `params.textureImage`, as `{path}` or -`{name?, dataBase64}`: a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD or PSB file, decoded within the pixel allowance the +`{name?, dataBase64}`: a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD, PSB or PCX file, decoded within the pixel allowance the renderer draws images in, counted as RGBA. `effect.apply` and `effect.setParams` store the image's gray levels in the effect as a grayscale PNG in base64 and set `texture: "loaded"` unless the params give a `texture`. `effect.list` reports the stored string, and a command given that string stores it unchanged. Any other string, a file that is not an image, or an image past diff --git a/docs/releasing.md b/docs/releasing.md index d97722cd9..ef476553b 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -164,7 +164,7 @@ to the target triple, so host build scripts aren't affected. AppStream metainfo, the MIME type for VectorCraft documents, the icons and the licence files. **Document types.** The desktop entry's `MimeType` lists every format File › Open reads but PSD, -PSB, `.ps` PostScript and Targa, so file managers list VectorCraft under Open With for them. +PSB, `.ps` PostScript, Targa and PCX, so file managers list VectorCraft under Open With for them. `ai.storyteller.vectorcraft.mime.xml` declares the types that shared-mime-info lacks: VectorCraft documents and templates, `.ait` templates and Affinity files (`application/x-affinity`). An `.af` file gets the Affinity type by its content, and `.afdesign` and `.afpub` files by their extension. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ceb4f020d..3a33c3a63 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -527,7 +527,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Artboard deletion keys:** Delete/Backspace delete selected boards in one Undo step, retaining the original first board in the current order when all are selected, as in the public [Delete artboards guide](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html). The last board and active drags stay protected; neither key clears selected artwork. Headless shortcut regressions in `tests/distortkeys.rs` cover reordered boards, artwork, saved state and Undo/Redo. - **Locked artboards:** a click in the Artboards panel's lock column locks or unlocks an artboard in one undo step (`artboard.setProps {index, locked}`), and the native format saves the lock. The Artboard tool doesn't move, copy, resize or delete a locked artboard, Rearrange and Fit to Artwork Bounds leave it as it is, and its name isn't edited on the canvas. The art on it (each object whose bounds overlap it, a group as one object) acts as locked art until it is unlocked: clicks, marquees, `select.set`, `select.all` and the tools pass over it, and commands that act on the selection leave it; art drawn or pasted onto it stays selected and editable until the selection leaves it out. Tests in `crates/doc/src/lib.rs`, `tests/panelcmds.rs`, `xform/artboard.rs`, `panels/artboards.rs` and `canvas.rs` cover them. Public references: [Lock artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/lock-artboards.html) and [Managing Artboards in Illustrator](https://creativepro.com/managing-artboards-in-illustrator/). - **Linux RPM packaging:** 64-bit runtime capabilities replace Fedora-specific required package names (#1275); glibc 2.35, libm and libgcc_s are declared alongside dynamically loaded xkbcommon and Vulkan/EGL. Generated x86_64/aarch64 metadata and isolated synthetic-provider resolution checked; real-distro install/start-up QA remains pending. -- **Linux file associations:** the desktop entry lists the MIME type of every format File › Open reads but PSD, PSB, `.ps` PostScript and Targa, `.ai`, `.ait` and EPS included, and the MIME package declares the types shared-mime-info lacks (VectorCraft documents and templates, `.ait`, Affinity), so file managers list VectorCraft under Open With for them, as Finder does on macOS. `the_linux_desktop_entry_opens_every_readable_format` in `apps/vectorcraft/src/main.rs` checks both files against `OPEN_EXTS`. On Debian 13, Ubuntu 24.04 and Fedora 42, `desktop-file-validate` accepts the desktop entry, `update-mime-database` accepts the MIME package, and `gio` registers the app for every one of these types. +- **Linux file associations:** the desktop entry lists the MIME type of every format File › Open reads but PSD, PSB, `.ps` PostScript, Targa and PCX, `.ai`, `.ait` and EPS included, and the MIME package declares the types shared-mime-info lacks (VectorCraft documents and templates, `.ait`, Affinity), so file managers list VectorCraft under Open With for them, as Finder does on macOS. `the_linux_desktop_entry_opens_every_readable_format` in `apps/vectorcraft/src/main.rs` checks both files against `OPEN_EXTS`. On Debian 13, Ubuntu 24.04 and Fedora 42, `desktop-file-validate` accepts the desktop entry, `update-mime-database` accepts the MIME package, and `gio` registers the app for every one of these types. - **Architecture:** 19+ crates with enforced layering (`cargo xtask layers`). Every action is a command (~400 engine + ~50 UI). Undo is unlimited via structural sharing. `command.batch` runs several commands as one transaction. - **Automation:** @@ -1005,6 +1005,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur `document.pdfInfo` reads an EPS, a `.ps` file or a `.ai` in PostScript form as one page the size of its bounding box (a US Letter page without one), and its `thumbnail: 1` is a PNG of what that page prints (`crates/engine/src/cmd/fileio/tests_pdfimport.rs`). - EPS export with Use Artboards writes a central EPS file of the whole document, bounded by its visible art, beside the file of each artboard when it writes two or more artboards; a document with no visible art gets only the artboard files (`crates/engine/src/cmd/fileio/tests_eps.rs`). - Targa files (`.tga`, `.vda`, `.icb`, `.vst`) open, place linked or embedded, relink and update at their pixel size: color-mapped, true-color and black-and-white, raw or run-length encoded, in any of the four row orders. The image descriptor's attribute bits and the extension area's Attributes Type decide the transparency: a file with no attribute bits, or with attribute data the Attributes Type calls undefined, opens opaque; with one bit, a 16-bit true-color pixel takes that bit as its alpha, and a color-mapped pixel takes the bit of the 16-bit color map entry it indexes; premultiplied colors are divided by their alpha. Open and Place recognize a Targa file by its extension, and `clipboard.importImage` and Glass's Load Texture… by its header; a Targa file copied in a file manager pastes as a bitmap (`crates/engine/src/cmd/fileio/tests_tiffbmp.rs`, `crates/engine/src/tests/links.rs`). + - PCX files (`.pcx`) open and place, from the ZSoft PCX Technical Reference Manual: 1 bit in the header palette's first two colors (black and white when the version has no palette or both colors are the same), 1 bit in two to four planes and 4 bits with up to 16 colors from the header's palette (the EGA colors for versions 0 and 3), 2 bits in the CGA colors that the header's CGA palette settings pick (background, color burst, palette and intensity), 8 bits with the 256-color palette after the image data (gray levels without one) and 24-bit color in three planes, run-length encoded. The header's HDpi and VDpi size the image. 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. Open and Place recognize a PCX file by its extension, and `clipboard.importImage` and Glass's Load Texture… by its header; a PCX file copied in a file manager pastes as a bitmap (`crates/engine/src/cmd/fileio/tests_pcxread.rs`). - EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export; grayscale TIFFs with an alpha channel or other extra channels place, relink and open, the alpha as the image's transparency (#1466 in part); PSD and PSB files open and place, linked or embedded, as their merged image in every colour mode and depth (#918). EPS import runs a PostScript Level 3 interpreter written from the PostScript Language Reference: resource categories and `resourceforall`, local and global VM (`currentglobal`/`setglobal`/`gcheck`), `clipsave`/`cliprestore`, subarrays and substrings that share storage, the standard dictionaries in `systemdict`, `dictstack`, `internaldict`, executable filters and strings, `load` by any key, `//name` immediate names, `token`, `execform`, user paths, `cshow`, `rootfont`, `cachestatus`, colour rendering and halftone queries, file `status` and data read with `flushfile`, and `pdfmark` marks dropped (Discord feedback, #466, #469, #474). Masked images (`ImageType 3` in its three interleavings) and reusable streams are read, and an image's several data sources are read in turn, as the reference defines (procedures that read one file get their own rows: such images came in banded, #498). `restore` puts dictionaries back as they were at the `save`, so programs that count their saves in a dictionary read to their end (part of #505). Tiling patterns come in as pattern swatches, uncoloured ones too; Type 3 fonts' glyphs as their outlines, spaced by `ashow`/`widthshow`/`xshow`/`kshow`; mesh and function-based shadings (types 1, 4–7) as gradient meshes; sampled functions (type 0) and Flate/LZW predictors are read. It is tested with programs written from the specification in the ways common generators use these features; Inkscape/cairo EPS files open as vectors. When a file's PostScript still can't be read, it opens as its TIFF preview, its Windows metafile preview (as vectors) or its EPSI preview, and the warning (in the status bar, `document.open`/`app.open`/MCP `open_file` replies and `vectorcraft-cli info`) names the error, the operator that raised it and the procedures it ran in. Legacy PostScript `.ai` files (Illustrator 3–8, written with the prolog that defines their operators) keep their groups and nested groups (`u` … `U`), between and inside clipping groups; EPS and EMF/WMF import nest clips (and groups) at most 128 deep, so a hostile file can't exhaust the stack. DXF TEXT justified Fit or Aligned spans its two points (stretched, or scaled as a whole). PDF export writes type as real, searchable text in embedded subset fonts by default, as Illustrator's PDF presets do (Convert Text to Outlines is an option; printer's marks stay shapes), so text-heavy PDFs stay small (#733). DXF export writes each fill as its outlines followed by its hatch, so laser and cutter software, which skips hatches, gets filled art and type too (#546). - TIFFs that store each channel as its own plane (PlanarConfiguration 2) place, relink and open as the same image stored pixel by pixel does, 8 and 16 bits: grayscale with an alpha channel or other extra channels, and CMYK with or without them. A planar CMYK TIFF keeps its ink amounts and is stored as a CMYK TIFF with each pixel's inks together, at the file's resolution. A TIFF whose header claims more strips or tiles than can be counted is refused as too large to draw (#1466 in part; `crates/engine/src/cmd/fileio/tests.rs`). - 1-bit TIFFs compressed with CCITT Group 3 (ITU-T T.4) place, relink and open: Compression 2 (modified Huffman) and Compression 3 with one-dimensional rows or, with T4Options bit 0, two-dimensional ones, with or without fill bits, in strips or tiles, with either FillOrder and either polarity. Rows in T.4's uncompressed mode are not read. A file whose pixels would take more than the decoding allowance as RGBA is refused as too large to draw (#1466 in part; `crates/engine/src/cmd/fileio/tests.rs`). @@ -1110,7 +1111,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Moving objects with raster effects (#1257):** during a drag or a dialog's preview, an object whose content only moved draws its cached drop shadows, outer glows and filtered content (Gaussian Blur, Feather and the Photoshop-style filters) from the rasters of an earlier frame at the nearest whole pixel, without running the effects again. The renderer first draws the moved content where the old content lies in its raster and compares the pixels. De-Interlace runs again. The frame after the drag runs the effects again and draws what a fresh renderer draws; on the desktop, the canvas renders that frame on the render worker. `texture_drag_perf` times a drag's frames and the frame after it. -- **Glass's Load Texture:** Effect › Distort › Glass… has a Load Texture… button that reads a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD or PSB file and adds Loaded Texture to the Texture menu (`textureImage`: `{path}` or `{name?, dataBase64}`, stored in the effect and saved with the document as a grayscale PNG in base64). The texture lies one image pixel per point at Scaling 100%, centered on the object and repeated; each pixel's luma is the surface's height, from 0 for black to 1 pt for white, and a transparent pixel counts as middle gray. Scaling and Invert act on it as on the built-in textures. A Glass saved without `textureImage` draws and saves as before. +- **Glass's Load Texture:** Effect › Distort › Glass… has a Load Texture… button that reads a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD, PSB or PCX file and adds Loaded Texture to the Texture menu (`textureImage`: `{path}` or `{name?, dataBase64}`, stored in the effect and saved with the document as a grayscale PNG in base64). The texture lies one image pixel per point at Scaling 100%, centered on the object and repeated; each pixel's luma is the surface's height, from 0 for black to 1 pt for white, and a transparent pixel counts as middle gray. Scaling and Invert act on it as on the built-in textures. A Glass saved without `textureImage` draws and saves as before. - **Raster effects after document and view changes:** a cached drop shadow, outer glow or filtered content is made again when something its content reads from outside the object changes: a pattern's art or tiling, a symbol shown inside another symbol, a brush definition, an image's pixels, the raster resolution (De-Interlace), Page Knockout Group, the substitution highlights (Highlight Substituted Fonts and Highlight Alternate Glyphs in Preferences › Type, Highlight Substituted Glyphs in Document Setup), a layer's Dim Images, the color settings, the fonts, Pixel Preview, Anti-aliased Artwork, Overprint Preview and isolation mode. A thumbnail under Color Halftone or Mezzotint is made again when the document's color mode changes. Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536). From dc9106939eb44607750f179d667883228f6cc40b Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 28 Oct 2026 03:48:26 -0700 Subject: [PATCH 2/2] File formats: a PCX palette cut short is refused An 8-bit PCX file whose image data is followed by the palette marker (0x0C) and fewer than 768 color bytes was read as gray levels. The reader now notes where the run-length encoded image data ends, and when the palette marker follows it without all 768 colors, refuses the file as cut short. An 8-bit file with nothing after its image data still reads as gray levels, and a gray level of 12 that falls 769 bytes before the end of such a file is read as a pixel instead of as the palette marker. --- crates/engine/src/cmd/fileio/pcxread.rs | 27 ++++++++++++++----- crates/engine/src/cmd/fileio/tests_pcxread.rs | 13 +++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/fileio/pcxread.rs b/crates/engine/src/cmd/fileio/pcxread.rs index ec8f1801b..aabbbc264 100644 --- a/crates/engine/src/cmd/fileio/pcxread.rs +++ b/crates/engine/src/cmd/fileio/pcxread.rs @@ -75,9 +75,24 @@ pub(super) fn decode(bytes: &[u8], max_alloc: u64) -> Result= 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)?; + // image data ends before it. A `0x0C` there is image data instead when the image data runs to + // the end of the file. + let candidate = bytes.len().checked_sub(769).filter(|at| *at >= HEADER && (bits, planes) == (8, 1) && bytes.get(*at) == Some(&0x0c)); + let body = bytes.get(HEADER..).unwrap_or_default(); + let (tail, decoded) = match candidate { + Some(at) => match unpack(body, scan * h) { + Ok((data, used)) if used == body.len() => (None, Ok((data, used))), + _ => (Some(at), unpack(bytes.get(HEADER..at).unwrap_or_default(), scan * h)), + }, + None => (None, unpack(body, scan * h)), + }; + let (data, used) = decoded?; + // A palette marker right after the image data with fewer than 768 colors after it is a palette + // cut short. + let after = bytes.get(HEADER + used..).unwrap_or_default(); + if tail.is_none() && (bits, planes) == (8, 1) && after.first() == Some(&0x0c) && after.len() < 769 { + return Err(TRUNCATED.into()); + } 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(), @@ -133,8 +148,8 @@ fn cga(background: u8, settings: u8) -> Vec<[u8; 3]> { /// `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, String> { +/// Runs may cross the end of a scan line. Also returns how many bytes of `data` were read. +fn unpack(data: &[u8], len: usize) -> Result<(Vec, usize), String> { let mut out = Vec::with_capacity(len); let mut bytes = data.iter(); while out.len() < len { @@ -147,5 +162,5 @@ fn unpack(data: &[u8], len: usize) -> Result, String> { out.push(b); } } - Ok(out) + Ok((out, data.len() - bytes.len())) } diff --git a/crates/engine/src/cmd/fileio/tests_pcxread.rs b/crates/engine/src/cmd/fileio/tests_pcxread.rs index e951e31ac..8cab621bd 100644 --- a/crates/engine/src/cmd/fileio/tests_pcxread.rs +++ b/crates/engine/src/cmd/fileio/tests_pcxread.rs @@ -119,6 +119,13 @@ fn every_depth_and_plane_layout_reads() { // 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 gray level 12 769 bytes before the end of image data that runs to the end of the file. + let mut levels = vec![1; 1000]; + levels[231] = 0x0c; + let f = pcx(5, 8, 1, 0, (1000, 1), 1000, 0, &[], &levels); + assert_eq!(f.len() - 769, 128 + 231); + let read = pixels(&f); + assert_eq!((read.len(), read[231], read[232]), (1000, [12, 12, 12, 255], [1, 1, 1, 255])); // 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]); @@ -175,6 +182,12 @@ fn damaged_and_unsupported_files_are_refused_with_a_message() { short.push(0x0c); short.extend([7; 768]); assert_eq!(message(&short), "the PCX file is cut short", "three pixels and a palette"); + // A palette marker after the image data with fewer than 768 colors after it. + let mut cut_palette = gray.clone(); + cut_palette.push(0x0c); + cut_palette.extend([7; 767]); + assert_eq!(message(&cut_palette), "the PCX file is cut short", "a palette cut short"); + assert_eq!(message(&[&gray[..], &[0x0c]].concat()), "the PCX file is cut short", "a marker alone"); 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"));