diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index abda7258d..76f408d22 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -44,6 +44,7 @@ pub mod pngtext; pub mod ppi; pub mod progress; pub mod psdread; +mod pxrread; mod save; mod screens; mod svg; @@ -205,6 +206,8 @@ mod tests_psdread; mod tests_jp2read; #[cfg(test)] mod tests_pcxread; +#[cfg(test)] +mod tests_pxrread; #[cfg(test)] mod tests_epsimport; diff --git a/crates/engine/src/cmd/fileio/formats.rs b/crates/engine/src/cmd/fileio/formats.rs index adbc05c93..09e8de829 100644 --- a/crates/engine/src/cmd/fileio/formats.rs +++ b/crates/engine/src/cmd/fileio/formats.rs @@ -494,6 +494,8 @@ pub const FORMATS: &[Format] = &[ 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), + // Pixar picture files, read by `pxrread` and stored as PNG; exports don't write them. + reader("pxr", "Pixar", &["pxr"], "image/x-pixar", true), // JP2 and raw codestreams are decoded to PNG; JPX composition is not supported. reader("jpeg2000", "JPEG 2000", &["jpf", "jpx", "jp2", "j2k", "j2c", "jpc"], "image/jp2", true), ]; @@ -523,6 +525,7 @@ pub const OPEN_EXTS: &[&str] = &[ "icb", "vst", "pcx", + "pxr", "jpf", "jpx", "jp2", @@ -546,11 +549,11 @@ 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`, Targa, PCX or JPEG 2000 type, nor one for the `.pns`, `.dib` and `.rle` names of +/// `.epsf`, `.ps`, Targa, PCX, Pixar or JPEG 2000 type, nor one for the `.pns`, `.dib` and `.rle` names of /// PNG and BMP files (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", "pdd", "psb", "epsf", "ps", "pns", "dib", "rle", "tga", "vda", "icb", "vst", "pcx", "jpf", "jpx", "jp2", "j2k", "j2c", "jpc"]; + &["psd", "pdd", "psb", "epsf", "ps", "pns", "dib", "rle", "tga", "vda", "icb", "vst", "pcx", "pxr", "jpf", "jpx", "jp2", "j2k", "j2c", "jpc"]; /// 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. @@ -606,6 +609,7 @@ pub const PLACE_EXTS: &[&str] = &[ "icb", "vst", "pcx", + "pxr", "jpf", "jpx", "jp2", diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index b2a0eb71f..a318616d5 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -97,6 +97,9 @@ pub fn detect(name: &str, bytes: &[u8]) -> Option<&'static Format> { if psdread::is_psd(bytes) { return format(if psdread::is_psb(bytes) { "psb" } else { "psd" }); } + if super::pxrread::is_pxr(bytes) { + return format("pxr"); + } if super::jp2read::is_jpeg2000(bytes) { return format("jpeg2000"); } @@ -642,6 +645,8 @@ pub(crate) fn size_note(bytes: &[u8]) -> Option { /// 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 Pixar picture file is recognized by its magic number ([`super::pxrread::is_pxr`]) and read by +/// [`super::pxrread::decode`], which does the same. /// 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 @@ -660,6 +665,9 @@ pub fn raster_image(bytes: &[u8]) -> Result { if super::jp2read::is_jpeg2000(bytes) { return as_png(super::jp2read::decode(bytes, MAX_RASTER_ALLOC).map_err(err)?, ppi); } + if super::pxrread::is_pxr(bytes) { + return as_png(super::pxrread::decode(bytes, MAX_RASTER_ALLOC).map_err(err)?, ppi); + } let mut reader = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().map_err(err)?; if reader.format().is_none() && super::tgaread::is_targa(bytes) { reader.set_format(image::ImageFormat::Tga); diff --git a/crates/engine/src/cmd/fileio/pxrread.rs b/crates/engine/src/cmd/fileio/pxrread.rs new file mode 100644 index 000000000..eb7957ad9 --- /dev/null +++ b/crates/engine/src/cmd/fileio/pxrread.rs @@ -0,0 +1,269 @@ +//! Reading Pixar picture files (`.pxr`) for File › Open and File › Place, from "The Format of +//! Stored Pictures" in the Pixar Image Computer manuals (release 1.2, December 1986). [`is_pxr`] +//! recognizes a file by the magic number at the start of its 512-byte header, and [`decode`] reads +//! its tiles into RGBA pixels: any subset of the R, G, B and A channels, 8 or 12 bits a sample, +//! dumped or run-length encoded, with alpha matted to black or unassociated. The file is +//! untrusted: every read is checked. +//! +//! Display rules for what the format leaves open: a picture whose only color channel is R is +//! gray; a missing color channel is 0 and a missing A is opaque; a null tile is transparent when +//! the picture has A and black otherwise. The x and y offsets are not read. + +/// The size of the header, after which the tile pointer table starts. +const HEADER: usize = 512; + +/// The first four bytes of every picture: the magic number `0x0000E880`, least significant byte +/// first. +const MAGIC: [u8; 4] = [0x80, 0xe8, 0x00, 0x00]; + +/// The message for a file whose header, tile table or tile data ends early, or that holds a tile +/// whose writing was not finished. +const TRUNCATED: &str = "the Pixar file is cut short"; + +/// The message for a header whose sizes, channels, storage mode or alpha mode the format doesn't +/// define. +const DAMAGED: &str = "the Pixar file's header is damaged"; + +/// The message for encoded tile data with a packet the format doesn't define, or one that runs +/// past the end of its scan line. +const BAD_DATA: &str = "the Pixar file's image data is damaged"; + +/// The channel bits of the picture format field (header byte 424): R, G, B and A, stored in this +/// order in each pixel. +const CHANNELS: [u16; 4] = [8, 4, 2, 1]; + +/// Does `bytes` start with a Pixar picture header's magic number? +pub(super) fn is_pxr(bytes: &[u8]) -> bool { + bytes.starts_with(&MAGIC) +} + +/// How one tile's samples are stored and how they become pixels. +struct Layout { + /// The channel indices (0 R, 1 G, 2 B, 3 A) the file stores, in the order it stores them. + stored: Vec, + /// Is a sample 12 bits in two bytes, rather than 8 bits in one? + wide: bool, + /// The bytes of one stored sample. + sample: usize, + /// Is the only color channel R, so the picture is gray? + gray: bool, + /// Are R, G and B matted to black (multiplied by A)? + matted: bool, + /// Does the picture store A? + alpha: bool, +} + +impl Layout { + /// The display value of the sample at `at` in `bytes`. A 12-bit sample is the low 12 bits of a + /// two-byte value, where 0 is 0.0, 2048 is 1.0, 2049 to 3071 are past 1.0 and 3072 to 4095 are + /// below 0.0; values outside 0.0 to 1.0 are clamped. + fn sample(&self, bytes: &[u8], at: usize) -> Result { + if !self.wide { + return bytes.get(at).copied().ok_or_else(|| TRUNCATED.to_string()); + } + let pair = bytes.get(at..at + 2).ok_or(TRUNCATED)?; + let v = u32::from(u16::from_le_bytes([pair[0], pair[1]]) & 0x0fff); + let v = if v >= 3072 { 0 } else { v.min(2048) }; + Ok(((v * 255 + 1024) / 2048) as u8) + } + + /// The RGBA pixel of the samples for `channels` read from `bytes` at `at`, combined with + /// `base` (the channels not read, such as a packet's common A). + fn pixel(&self, bytes: &[u8], at: usize, channels: &[usize], mut base: [u8; 4]) -> Result<[u8; 4], String> { + for (i, &c) in channels.iter().enumerate() { + base[c] = self.sample(bytes, at + i * self.sample)?; + } + Ok(base) + } + + /// The color a pixel of stored samples is shown in: gray from R, and straight color from color + /// matted to black (0 where A is 0). + fn shown(&self, [r, g, b, a]: [u8; 4]) -> [u8; 4] { + let [r, g, b] = if self.gray { [r; 3] } else { [r, g, b] }; + if !self.alpha { + return [r, g, b, 255]; + } + if !self.matted { + return [r, g, b, a]; + } + let unmatte = |c: u8| if a == 0 { 0 } else { ((u32::from(c) * 255 + u32::from(a) / 2) / u32::from(a)).min(255) as u8 }; + [unmatte(r), unmatte(g), unmatte(b), a] + } +} + +/// The pixels of Pixar picture `bytes`, which [`is_pxr`] accepts, refused when they take more +/// than `max_alloc` bytes 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| u16::from_le_bytes([head[at], head[at + 1]]); + let (h, w, tile_h, tile_w) = (le(416), le(418), le(420), le(422)); + let (format, storage, block, alpha_mode) = (le(424), le(426), le(428), le(430)); + // Sizes must be positive; the format field holds only the four channel bits; the storage modes + // are 0 to 3 and the alpha modes 0 and 1. + if w == 0 || h == 0 || tile_w == 0 || tile_h == 0 { + return Err(DAMAGED.into()); + } + if format == 0 || format > 15 || storage > 3 || alpha_mode > 1 { + return Err(DAMAGED.into()); + } + let (w, h, tile_w, tile_h) = (usize::from(w), usize::from(h), usize::from(tile_w), usize::from(tile_h)); + if w as u64 * h as u64 * 4 > max_alloc { + return Err(format!("an image of {w} × {h} pixels is too large to draw")); + } + let stored: Vec = CHANNELS.iter().enumerate().filter(|(_, bit)| format & **bit != 0).map(|(i, _)| i).collect(); + let wide = storage % 2 == 1; + let layout = + Layout { gray: format & 0b1110 == 0b1000, matted: alpha_mode == 0, alpha: format & 1 != 0, sample: if wide { 2 } else { 1 }, wide, stored }; + let (across, down) = (w.div_ceil(tile_w), h.div_ceil(tile_h)); + let table = bytes.get(HEADER..HEADER + across * down * 8).ok_or(TRUNCATED)?; + let empty = if layout.alpha { [0; 4] } else { [0, 0, 0, 255] }; + let mut img = image::RgbaImage::from_pixel(w as u32, h as u32, image::Rgba(empty)); + // The encoded data read in all, which a file whose tiles each hold their own data stays within. + let mut left = bytes.len(); + for (n, entry) in table.as_chunks::<8>().0.iter().enumerate() { + let pointer = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize; + let length = i32::from_le_bytes([entry[4], entry[5], entry[6], entry[7]]); + if pointer == 0 { + continue; + } + // A tile that starts past the end of the file; every offset read from here stays near the + // file's length, so it fits a 32-bit address. + if length == -1 || pointer >= bytes.len() { + return Err(TRUNCATED.into()); + } + let (x0, y0) = (n % across * tile_w, n / across * tile_h); + // The part of the tile inside the picture; the pixels of border tiles past its edges are + // stored and skipped. + let tile = Tile { w: tile_w, h: tile_h, shown_w: tile_w.min(w - x0), shown_h: tile_h.min(h - y0) }; + let mut put = |x: usize, y: usize, px: [u8; 4]| { + img.put_pixel((x0 + x) as u32, (y0 + y) as u32, image::Rgba(layout.shown(px))); + }; + if storage >= 2 { + dumped(bytes, pointer, tile, &layout, &mut put)?; + } else { + encoded(bytes, pointer, tile, usize::from(block), &layout, &mut left, &mut put)?; + } + } + Ok(img) +} + +/// A tile's size, and the size of its part inside the picture (the pixels that are drawn). +#[derive(Clone, Copy)] +struct Tile { + w: usize, + h: usize, + shown_w: usize, + shown_h: usize, +} + +/// Read a dumped tile at `at`: its pixels row by row, each pixel's samples in channel order. The +/// whole tile must be in the file; only its part inside the picture is read. +fn dumped(bytes: &[u8], at: usize, tile: Tile, layout: &Layout, put: &mut impl FnMut(usize, usize, [u8; 4])) -> Result<(), String> { + let size = layout.stored.len() * layout.sample; + let end = tile.w.checked_mul(tile.h).and_then(|n| n.checked_mul(size)).and_then(|n| n.checked_add(at)).ok_or(TRUNCATED)?; + if end > bytes.len() { + return Err(TRUNCATED.into()); + } + for y in 0..tile.shown_h { + for x in 0..tile.shown_w { + put(x, y, layout.pixel(bytes, at + (y * tile.w + x) * size, &layout.stored, [0, 0, 0, 255])?); + } + } + Ok(()) +} + +/// Read an encoded tile at `at`: each scan line is a series of packets, each headed by a 16-bit +/// value whose top 4 bits are its kind and whose low 12 bits are its count less one. +/// +/// - 0 ends the disk block: reading goes on at the next multiple of `block` bytes in the file. +/// - 1 dumps count + 1 pixels with every channel. +/// - 2 holds count + 1 runs, each a length byte (pixels less one) and one pixel with every channel. +/// - 3 dumps count + 1 pixels' color channels after one A sample they share. +/// - 4 holds count + 1 runs of color channels, each a length byte and one pixel, after one A +/// sample they share. +/// +/// No packet runs past the end of its scan line, and a block end never follows another one (an +/// empty block). Reading stops after the last scan line inside the picture, pixels past its right +/// edge are skipped, and the bytes read count against `left`, the encoded data left to read in +/// the whole file: tiles that share their data are refused once it is used up. +fn encoded( + bytes: &[u8], + mut at: usize, + tile: Tile, + block: usize, + layout: &Layout, + left: &mut usize, + put: &mut impl FnMut(usize, usize, [u8; 4]), +) -> Result<(), String> { + let all = &layout.stored; + let colors: Vec = all.iter().copied().filter(|c| *c != 3).collect(); + let mut block_ended = false; + for y in 0..tile.shown_h { + let mut x = 0; + while x < tile.w { + let start = at; + let head = bytes.get(at..at + 2).ok_or(TRUNCATED)?; + let head = u16::from_le_bytes([head[0], head[1]]); + let (kind, count) = (head >> 12, usize::from(head & 0x0fff) + 1); + if kind == 0 { + if block == 0 || block_ended { + return Err(BAD_DATA.into()); + } + block_ended = true; + at = (at / block + 1) * block; + if at >= bytes.len() { + return Err(TRUNCATED.into()); + } + *left = left.checked_sub(at - start).ok_or(BAD_DATA)?; + continue; + } + block_ended = false; + at += 2; + let shared = matches!(kind, 3 | 4); + if shared && (!layout.alpha || colors.is_empty()) { + return Err(BAD_DATA.into()); + } + let (channels, base) = if shared { + let a = layout.sample(bytes, at)?; + at += layout.sample; + (&colors, [0, 0, 0, a]) + } else { + (all, [0, 0, 0, 255]) + }; + let size = channels.len() * layout.sample; + match kind { + 1 | 3 => { + if x + count > tile.w { + return Err(BAD_DATA.into()); + } + let end = at + count * size; + if end > bytes.len() { + return Err(TRUNCATED.into()); + } + for i in 0..count.min(tile.shown_w.saturating_sub(x)) { + put(x + i, y, layout.pixel(bytes, at + i * size, channels, base)?); + } + at = end; + x += count; + } + 2 | 4 => { + for _ in 0..count { + let run = usize::from(*bytes.get(at).ok_or(TRUNCATED)?) + 1; + if x + run > tile.w { + return Err(BAD_DATA.into()); + } + let px = layout.pixel(bytes, at + 1, channels, base)?; + at += 1 + size; + for i in 0..run.min(tile.shown_w.saturating_sub(x)) { + put(x + i, y, px); + } + x += run; + } + } + _ => return Err(BAD_DATA.into()), + } + *left = left.checked_sub(at - start).ok_or(BAD_DATA)?; + } + } + Ok(()) +} diff --git a/crates/engine/src/cmd/fileio/tests_pxrread.rs b/crates/engine/src/cmd/fileio/tests_pxrread.rs new file mode 100644 index 000000000..0562aacbb --- /dev/null +++ b/crates/engine/src/cmd/fileio/tests_pxrread.rs @@ -0,0 +1,363 @@ +//! Reading Pixar picture files: every channel set, storage mode, packet kind and alpha mode +//! decodes to the pixels it stores, tiled pictures assemble with their border and null tiles, the +//! files open, place and paste, and damaged files are refused with a message. + +use serde_json::json; + +use super::pxrread::{decode, is_pxr}; +use super::*; + +/// The header fields a test sets: the picture's (width, height), the tile's (width, height), the +/// channel bits (R 8, G 4, B 2, A 1), the storage mode, the blocking factor and the alpha mode. +#[derive(Clone, Copy)] +struct Head { + size: (u16, u16), + tile: (u16, u16), + format: u16, + storage: u16, + block: u16, + alpha: u16, +} + +/// A one-tile picture of `w` × `h` pixels. +fn head(w: u16, h: u16, format: u16, storage: u16) -> Head { + Head { size: (w, h), tile: (w, h), format, storage, block: 0, alpha: 1 } +} + +/// A Pixar picture file of `version` with header `h`, whose tiles' data `tiles` (`None` for a +/// null tile) follow one another from byte `start` (at least the end of the tile pointer table). +fn pxr(version: u16, h: Head, start: usize, tiles: &[Option>]) -> Vec { + let mut f = vec![0x80, 0xe8, 0, 0]; + f.extend(version.to_le_bytes()); + f.extend(b"a test picture"); + f.resize(416, 0); + for v in [h.size.1, h.size.0, h.tile.1, h.tile.0, h.format, h.storage, h.block, h.alpha, 7, 9] { + f.extend(v.to_le_bytes()); + } + f.resize(512, 0); + let mut at = start; + let mut data: Vec = vec![]; + for tile in tiles { + match tile { + Some(bytes) => { + f.extend((at as u32).to_le_bytes()); + f.extend((bytes.len() as i32).to_le_bytes()); + data.extend(bytes); + at += bytes.len(); + } + None => f.extend([0; 8]), + } + } + assert!(f.len() <= start, "the tile data starts inside the table"); + f.resize(start, 0); + f.extend(data); + f +} + +/// A one-tile picture whose tile data follows the table. +fn one(h: Head, data: &[u8]) -> Vec { + pxr(0, h, 520, &[Some(data.to_vec())]) +} + +/// A packet header: `kind` in the top four bits, `count` less one in the low twelve. +fn packet(kind: u16, count: u16) -> [u8; 2] { + ((kind << 12) | (count - 1)).to_le_bytes() +} + +/// 12-bit samples, two bytes each. +fn wide(samples: &[u16]) -> Vec { + samples.iter().flat_map(|s| s.to_le_bytes()).collect() +} + +/// 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() +} + +const RGBA: u16 = 15; +const RGB: u16 = 14; +const R: u16 = 8; + +#[test] +fn every_channel_set_and_dumped_storage_reads() { + // 8 bits dumped, RGBA unassociated: the samples as they are. + let f = one(head(2, 1, RGBA, 2), &[10, 20, 30, 40, 50, 60, 70, 255]); + assert_eq!(pixels(&f), [[10, 20, 30, 40], [50, 60, 70, 255]]); + // RGB is opaque; R alone is gray; G and B alone leave the other colors at 0. + assert_eq!(pixels(&one(head(2, 1, RGB, 2), &[1, 2, 3, 4, 5, 6])), [[1, 2, 3, 255], [4, 5, 6, 255]]); + assert_eq!(pixels(&one(head(3, 1, R, 2), &[0, 128, 255])), [[0, 0, 0, 255], [128, 128, 128, 255], [255; 4]]); + assert_eq!(pixels(&one(head(1, 1, 0b0110, 2), &[9, 8])), [[0, 9, 8, 255]]); + // A alone: black at the stored coverage. + assert_eq!(pixels(&one(head(1, 1, 1, 2), &[77])), [[0, 0, 0, 77]]); + + // 12 bits dumped: 0 is black, 2048 white, 1024 half (127.5, rounded up); values past 1.0 + // (2049 to 3071) are white and values below 0.0 (3072 to 4095) black; the top four bits of + // each pair aren't part of the sample. + let f = one(head(2, 1, RGB, 3), &wide(&[0, 1024, 2048, 3000, 3072, 0xf800])); + assert_eq!(pixels(&f), [[0, 128, 255, 255], [255, 0, 255, 255]]); + let f = one(head(1, 1, R, 3), &wide(&[512])); + assert_eq!(pixels(&f), [[64, 64, 64, 255]], "512 of 2048 is 63.75 of 255"); +} + +#[test] +fn alpha_matted_to_black_becomes_straight_color() { + // Alpha mode 0: color is multiplied by A. 50 and 100 at A 128 are 99.6 and 199.2 straight; + // color past its A is white; A 0 is transparent black. + let mut h = head(3, 1, RGBA, 2); + h.alpha = 0; + let f = one(h, &[50, 100, 0, 128, 200, 0, 0, 100, 30, 30, 30, 0]); + assert_eq!(pixels(&f), [[100, 199, 0, 128], [255, 0, 0, 100], [0, 0, 0, 0]]); + // Alpha mode 1 keeps the color as it is. + h.alpha = 1; + let f = one(h, &[50, 100, 0, 128, 200, 0, 0, 100, 30, 30, 30, 0]); + assert_eq!(pixels(&f), [[50, 100, 0, 128], [200, 0, 0, 100], [30, 30, 30, 0]]); + // Without A, the alpha mode changes nothing. + let mut h = head(1, 1, RGB, 2); + h.alpha = 0; + assert_eq!(pixels(&one(h, &[50, 100, 0])), [[50, 100, 0, 255]]); +} + +#[test] +fn every_packet_kind_decodes() { + // Kind 1 dumps two full pixels; kind 2 holds one run of two pixels (length byte 1). + let mut data = packet(1, 2).to_vec(); + data.extend([1, 2, 3, 4, 5, 6, 7, 8]); + data.extend(packet(2, 1)); + data.extend([1, 9, 10, 11, 12]); + assert_eq!(pixels(&one(head(4, 1, RGBA, 0), &data)), [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [9, 10, 11, 12]]); + + // Kind 3 dumps one pixel's RGB after A 200; kind 4 holds runs of one and two pixels after A 100. + let mut data = packet(3, 1).to_vec(); + data.extend([200, 1, 2, 3]); + data.extend(packet(4, 2)); + data.extend([100, 0, 4, 5, 6, 1, 7, 8, 9]); + assert_eq!(pixels(&one(head(4, 1, RGBA, 0), &data)), [[1, 2, 3, 200], [4, 5, 6, 100], [7, 8, 9, 100], [7, 8, 9, 100]]); + + // Kind 2 in a gray picture: three runs over five pixels. + let mut data = packet(2, 3).to_vec(); + data.extend([0, 10, 2, 20, 0, 30]); + assert_eq!( + pixels(&one(head(5, 1, R, 0), &data)), + [[10, 10, 10, 255], [20, 20, 20, 255], [20, 20, 20, 255], [20, 20, 20, 255], [30, 30, 30, 255]] + ); + + // 12 bits encoded: samples take two bytes; run lengths and the packet header don't change. + let mut data = packet(1, 1).to_vec(); + data.extend(wide(&[2048, 1024, 0])); + data.extend(packet(2, 1)); + data.push(0); + data.extend(wide(&[0, 0, 2048])); + assert_eq!(pixels(&one(head(2, 1, RGB, 1), &data)), [[255, 128, 0, 255], [0, 0, 255, 255]]); + let mut data = packet(3, 1).to_vec(); + data.extend(wide(&[1024, 2048, 0, 0])); + assert_eq!(pixels(&one(head(1, 1, RGBA, 1), &data)), [[255, 0, 0, 128]]); + + // Several rows, one packet each, and a block end: with a blocking factor of 16, kind 0 sends + // reading to byte 32 of the file, where the second row starts. Packets of both rows mix kinds. + let mut h = head(2, 2, RGB, 0); + h.block = 16; + let mut data = packet(1, 2).to_vec(); + data.extend([1, 1, 1, 2, 2, 2]); + data.extend(packet(0, 1)); + data.resize(16, 0); + data.extend(packet(2, 1)); + data.extend([1, 3, 3, 3]); + let f = pxr(0, h, 528, &[Some(data)]); + assert_eq!(528 % 16, 0); + assert_eq!(pixels(&f), [[1, 1, 1, 255], [2, 2, 2, 255], [3, 3, 3, 255], [3, 3, 3, 255]]); +} + +#[test] +fn tiles_assemble_with_border_and_null_tiles() { + // A 3 × 3 picture in 2 × 2 tiles: two across and two down, numbered across. The border tiles' + // pixels past the picture are stored and dropped. Tile 2 (bottom left) is null: black, since + // the picture has no A. + let mut h = head(3, 3, R, 2); + h.tile = (2, 2); + let tiles = [Some(vec![1, 2, 4, 5]), Some(vec![3, 99, 6, 99]), None, Some(vec![9, 99, 99, 99])]; + let f = pxr(0, h, 600, &tiles); + let gray = |v: u8| [v, v, v, 255]; + assert_eq!(pixels(&f), [gray(1), gray(2), gray(3), gray(4), gray(5), gray(6), gray(0), gray(0), gray(9)]); + + // With A, a null tile is transparent. + let mut h = head(2, 1, RGBA, 2); + h.tile = (1, 1); + let f = pxr(0, h, 600, &[None, Some(vec![1, 2, 3, 4])]); + assert_eq!(pixels(&f), [[0, 0, 0, 0], [1, 2, 3, 4]]); + + // A picture smaller than its tile is one border tile: a 1 × 1 picture in a 2 × 2 tile keeps its + // one pixel, dumped and encoded. + let mut h = head(1, 1, RGB, 2); + h.tile = (2, 2); + assert_eq!(pixels(&one(h, &[1, 2, 3, 99, 99, 99, 99, 99, 99, 99, 99, 99])), [[1, 2, 3, 255]]); + h.storage = 0; + let mut data = packet(2, 1).to_vec(); + data.extend([1, 4, 5, 6]); + data.extend(packet(2, 1)); + data.extend([1, 99, 99, 99]); + assert_eq!(pixels(&one(h, &data)), [[4, 5, 6, 255]]); + + // Tile data may lie anywhere after the table, in any order, and a length of 0 is read like any + // other complete tile. + let mut f = pxr(0, head(1, 1, RGB, 2), 1000, &[Some(vec![7, 8, 9])]); + f[516..520].copy_from_slice(&0i32.to_le_bytes()); + assert_eq!(pixels(&f), [[7, 8, 9, 255]]); +} + +#[test] +fn version_2_headers_read_like_version_0() { + let data = [10, 20, 30, 40]; + let (v0, v2) = (pxr(0, head(1, 1, RGBA, 2), 520, &[Some(data.to_vec())]), pxr(2, head(1, 1, RGBA, 2), 520, &[Some(data.to_vec())])); + assert_eq!(v2[4..6], [2, 0]); + assert_eq!(pixels(&v2), pixels(&v0)); +} + +#[test] +fn pxr_files_open_place_and_paste() { + use crate::cmd::clipboard::{PASTE_ORDER, file_flavour}; + let b64 = vectorcraft_format::base64_encode; + let f = one(head(2, 1, RGB, 2), &[10, 20, 30, 40, 50, 60]); + // The header has no resolution: the image takes the default for images without one. + assert_eq!(ppi::resolution(&f), None); + assert_eq!(detect("picture.dat", &f).map(|f| f.id), Some("pxr"), "by its magic number"); + + let mut s = Session::new(); + let r = s.execute("document.open", &json!({"name": "art.pxr", "dataBase64": b64(&f)})).unwrap(); + assert_eq!((&r["format"], &r["warnings"]), (&json!("pxr"), &json!([])), "{r}"); + let size = s.doc().unwrap().doc.artboards[0].rect.size(); + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + let r = s.execute("file.place", &json!({"name": "ART.PXR", "dataBase64": b64(&f)})).unwrap(); + assert_eq!((&r["format"], r["ids"].as_array().map(Vec::len)), (&json!("pxr"), Some(1)), "{r}"); + // Placed from a path, linked and embedded: both show the file's pixels at its pixel size. + let dir = std::env::temp_dir().join(format!("vectorcraft-pxr-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("art.pxr"); + std::fs::write(&path, &f).unwrap(); + for link in [true, false] { + let r = s.execute("file.place", &json!({"path": path.to_string_lossy(), "link": link})).unwrap(); + assert_eq!((&r["format"], r["ids"].as_array().map(Vec::len)), (&json!("pxr"), Some(1)), "link {link}: {r}"); + let doc = &s.doc().unwrap().doc; + let node = doc.node(vectorcraft_doc::NodeId(r["ids"][0].as_u64().unwrap())).unwrap(); + let vectorcraft_doc::NodeKind::Image(im) = &node.kind else { panic!("link {link}: an image, got {:?}", node.kind) }; + assert_eq!((im.link.is_some(), im.width, im.height), (link, 2, 1), "link {link}"); + let shown = image::load_from_memory(&doc.images[&im.key].bytes).unwrap().to_rgba8(); + assert_eq!(shown.pixels().map(|p| p.0).collect::>(), pixels(&f), "link {link}"); + } + let _ = std::fs::remove_dir_all(&dir); + // Open sizes it as a PNG of 2 × 1 pixels without a resolution. + let mut png = vec![]; + image::RgbaImage::new(2, 1).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap(); + let mut t = Session::new(); + t.execute("document.open", &json!({"name": "same.png", "dataBase64": b64(&png)})).unwrap(); + assert_eq!(t.doc().unwrap().doc.artboards[0].rect.size(), size); + + // Copied in a file manager, a Pixar file pastes as a bitmap. + assert_eq!(file_flavour("art.pxr", &f, &PASTE_ORDER), Some("image/x-pixar")); + let r = s.execute("clipboard.importImage", &json!({"mime": "image/x-pixar", "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 Pixar; the desktop packaging doesn't associate it; nothing writes it. + assert!(OPEN_EXTS.contains(&"pxr") && PLACE_EXTS.contains(&"pxr") && UNASSOCIATED_EXTS.contains(&"pxr")); + let filter = ("Pixar", &["pxr"][..]); + assert!(open_filters().any(|f| f == filter) && place_filters().any(|f| f == filter)); + let r = s.execute("document.formats", &json!({})).unwrap(); + let pxr_format = r["formats"].as_array().unwrap().iter().find(|f| f["id"] == "pxr").unwrap().clone(); + assert_eq!((pxr_format["read"].as_bool(), pxr_format["write"].as_bool()), (Some(true), Some(false))); + assert_eq!(format_for_name("a.pxr").map(|f| f.id), Some("pxr")); +} + +#[test] +fn damaged_files_are_refused_with_a_message() { + let message = |f: &[u8]| raster_image(f).err().expect("refused").to_string(); + let rgb = one(head(2, 1, RGB, 2), &[1, 2, 3, 4, 5, 6]); + let (short, header, data) = ("the Pixar file is cut short", "the Pixar file's header is damaged", "the Pixar file's image data is damaged"); + + // A header, tile table or dumped tile cut short, and a tile whose writing wasn't finished. + assert_eq!(message(&rgb[..300]), short); + assert_eq!(message(&rgb[..516]), short); + assert_eq!(message(&rgb[..rgb.len() - 1]), short); + let mut unfinished = rgb.clone(); + unfinished[516..520].copy_from_slice(&(-1i32).to_le_bytes()); + assert_eq!(message(&unfinished), short); + let mut far = rgb.clone(); + far[512..516].copy_from_slice(&u32::MAX.to_le_bytes()); + assert_eq!(message(&far), short, "a pointer past the end of the file"); + // A refused open leaves the active document as it was. + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 50})).unwrap(); + let before = s.doc().unwrap().doc.artboards[0].rect.size(); + let e = s.execute("document.open", &json!({"name": "a.pxr", "dataBase64": vectorcraft_format::base64_encode(&rgb[..516])})); + assert!(e.expect_err("refused").to_string().contains(short)); + assert_eq!(s.doc().unwrap().doc.artboards[0].rect.size(), before); + + // Encoded data cut short: in a packet header, a sample and a run length. + let mut cut = packet(1, 2).to_vec(); + cut.extend([1, 2, 3, 4]); + assert_eq!(message(&one(head(2, 1, RGB, 0), &cut)), short); + assert_eq!(message(&one(head(2, 1, RGB, 0), &[0x01])), short); + assert_eq!(message(&one(head(2, 1, RGB, 0), &packet(2, 1))), short); + // A block end that moves past the end of the file. + let mut h = head(1, 1, RGB, 0); + h.block = 4096; + assert_eq!(message(&one(h, &packet(0, 1))), short); + + // Header values the format doesn't define. + let with = |at: usize, v: u16| { + let mut f = rgb.clone(); + f[at..at + 2].copy_from_slice(&v.to_le_bytes()); + message(&f) + }; + for (at, v, what) in [ + (416, 0, "no height"), + (418, 0, "no width"), + (420, 0, "no tile height"), + (422, 0, "no tile width"), + (424, 0, "no channels"), + (424, 16, "a channel bit past A"), + (426, 4, "storage mode 4"), + (430, 2, "alpha mode 2"), + ] { + assert_eq!(with(at, v), header, "{what}"); + } + + // Packets the format doesn't define, packets past the end of the scan line, packets with a + // shared A in a picture without A, and a block end without a blocking factor. + let encoded = |format: u16, data: &[u8]| message(&one(head(2, 1, format, 0), data)); + assert_eq!(encoded(RGB, &[&packet(5, 1)[..], &[0; 8]].concat()), data, "kind 5"); + assert_eq!(encoded(RGB, &[&packet(1, 3)[..], &[0; 9]].concat()), data, "three pixels in a row of two"); + assert_eq!(encoded(RGB, &[&packet(2, 1)[..], &[2, 0, 0, 0]].concat()), data, "a run of three in a row of two"); + assert_eq!(encoded(RGB, &[&packet(3, 1)[..], &[0; 4]].concat()), data, "a shared A without A"); + assert_eq!(encoded(1, &[&packet(4, 1)[..], &[0; 4]].concat()), data, "a shared A without color"); + assert_eq!(encoded(RGB, &packet(0, 1)), data, "a block end without a blocking factor"); + // A block end right after another one: an empty block. + let mut h = head(2, 1, RGB, 0); + h.block = 2; + assert_eq!(message(&one(h, &[&packet(0, 1)[..], &packet(0, 1), &packet(1, 2), &[0; 6]].concat())), data, "two block ends in a row"); + // Tiles that share their encoded data read more than the file holds: a scan line of 2000 + // one-pixel packets, shown one pixel wide, is read once per tile. + let row: Vec = (0..2000).flat_map(|_| [&packet(1, 1)[..], &[5, 6, 7]].concat()).collect(); + let mut h = head(1, 1, RGB, 0); + h.tile = (2000, 1); + assert_eq!(pixels(&pxr(0, h, 520, &[Some(row.clone())])), [[5, 6, 7, 255]], "the one tile alone"); + h.size = (1, 3); + let mut shared = pxr(0, h, 600, &[Some(row.clone()), None, None]); + let first = shared[512..520].to_vec(); + shared[520..528].copy_from_slice(&first); + shared[528..536].copy_from_slice(&first); + assert_eq!(message(&shared), data, "three tiles on the same data"); + + // Past the allowance, from the header, before any tile is read. + let huge = pxr(0, head(65535, 65535, RGBA, 2), 520, &[None]); + assert_eq!(message(&huge), "an image of 65535 × 65535 pixels is too large to draw"); + assert!(decode(&rgb, 7).is_err(), "2 × 1 RGBA is 8 bytes"); + assert!(decode(&rgb, 8).is_ok()); + + // Only the magic number makes a Pixar file. + assert!(is_pxr(&rgb)); + for bad in [[0x80, 0xe8, 0, 1], [0xe8, 0x80, 0, 0], [0, 0, 0xe8, 0x80]] { + assert!(!is_pxr(&bad), "{bad:?}"); + } +} diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index bc2350f83..60bcc3fbe 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -1796,6 +1796,74 @@ proptest! { } } +/// Pixar picture files, 3 × 2 pixels: RGBA dumped in 8 bits, RGB dumped in 12 bits, RGBA encoded +/// with every packet kind and a block end, and gray in 2 × 1 tiles with a null tile. +fn pxr_samples() -> Vec<(&'static str, Vec)> { + let pxr = |tile: (u16, u16), format: u16, storage: u16, tiles: &[Option>]| { + let mut f = vec![0x80, 0xe8, 0, 0]; + f.resize(416, 0); + for v in [2, 3, tile.1, tile.0, format, storage, 16, 1, 0, 0] { + f.extend(u16::to_le_bytes(v)); + } + f.resize(512, 0); + let mut at = (512 + tiles.len() * 8).next_multiple_of(16); + let mut data: Vec = vec![]; + for t in tiles { + match t { + Some(bytes) => { + f.extend((at as u32).to_le_bytes()); + f.extend((bytes.len() as u32).to_le_bytes()); + data.extend(bytes); + at += bytes.len(); + } + None => f.extend([0; 8]), + } + } + f.resize((512 + tiles.len() * 8).next_multiple_of(16), 0); + f.extend(data); + f + }; + let rgba: Vec = (0..24u8).map(|i| i.wrapping_mul(41)).collect(); + let wide: Vec = (0..18u16).flat_map(|i| (i * 300).to_le_bytes()).collect(); + let mut packets = vec![0x01, 0x10, 1, 2, 3, 4, 5, 6, 7, 8, 0x00, 0x20, 0, 9, 9, 9, 9]; + packets.extend([0x00, 0x00]); + packets.resize(32, 0); + packets.extend([0x00, 0x30, 200, 1, 2, 3, 0x01, 0x40, 100, 0, 4, 5, 6, 0, 7, 8, 9]); + vec![ + ("a.pxr", pxr((3, 2), 15, 2, &[Some(rgba)])), + ("b.pxr", pxr((3, 2), 14, 3, &[Some(wide)])), + ("c.pxr", pxr((3, 2), 15, 0, &[Some(packets)])), + ("d.pxr", pxr((2, 1), 8, 2, &[Some(vec![1, 2]), Some(vec![3, 4]), None, Some(vec![5, 6])])), + ] +} + +proptest! { + #![proptest_config(config())] + + /// Mutated Pixar picture files (header fields, the tile pointer table, samples and packets) + /// never crash placing, opening or pasting them. + #[test] + fn mutated_pxr_files_place_without_panics(which in 0usize..4, cut in 0usize..640, edits in prop::collection::vec((400usize..640, any::()), 0..12)) { + let (name, mut bytes) = pxr_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 ---------- proptest! { diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index e1c1eb8b7..ce4f9b1fb 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -4932,6 +4932,9 @@ imageTrace Outline Obris @msg the PCX file is cut short PCX datoteka je skraćena @msg the PCX file's header is damaged zaglavlje PCX datoteke je oštećeno @msg PCX files with {bits}-bit pixels in {planes} planes can't be read PCX datoteke s {bits}-bitnim pikselima u {planes} ravni nije moguće pročitati +@msg the Pixar file is cut short Pixar datoteka je skraćena +@msg the Pixar file's header is damaged zaglavlje Pixar datoteke je oštećeno +@msg the Pixar file's image data is damaged slikovni podaci Pixar datoteke su oštećeni @msg the JPEG 2000 file can't be read datoteku JPEG 2000 nije moguće pročitati @msg the JPEG 2000 file has an unsupported color space or JPX content datoteka JPEG 2000 ima nepodržan prostor boja ili JPX sadržaj @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD slika je prevelika za čitanje ({_1} × {_2} piksela) diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index c72b11a97..6cf1e5ae4 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5562,6 +5562,9 @@ digits Malayalam Malaiàlam @msg the program runs, deletes or renames a file, which the reader can't do el programa executa, suprimeix o reanomena un fitxer, cosa que el lector no pot fer @msg the PSD file has an unknown colour mode ({m}) el fitxer PSD té un mode de color desconegut ({m}) @msg the PSD file is cut short el fitxer PSD està truncat +@msg the Pixar file is cut short el fitxer Pixar està truncat +@msg the Pixar file's header is damaged la capçalera del fitxer Pixar està malmesa +@msg the Pixar file's image data is damaged les dades d’imatge del fitxer Pixar estan malmeses @msg the PSD file's header is damaged la capçalera del fitxer PSD està malmesa @msg the PSD file's image data can't be read: {e} no es poden llegir les dades d’imatge del fitxer PSD: {e} @msg the PSD file's image data uses an unknown compression ({m}) les dades d’imatge del fitxer PSD utilitzen una compressió desconeguda ({m}) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 9054417bf..310edbd4b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4989,6 +4989,9 @@ imageTrace Outline Obrys @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 Pixar file is cut short soubor Pixar je useknutý +@msg the Pixar file's header is damaged hlavička souboru Pixar je poškozená +@msg the Pixar file's image data is damaged obrazová data souboru Pixar jsou poškozená @msg the JPEG 2000 file can't be read soubor JPEG 2000 nelze přečíst @msg the JPEG 2000 file has an unsupported color space or JPX content soubor JPEG 2000 obsahuje nepodporovaný barevný prostor nebo obsah JPX @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ů) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index a0f272211..c85c90da2 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4730,6 +4730,9 @@ theme Appearance Erscheinungsbild @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 Pixar file is cut short die Pixar-Datei ist unvollständig +@msg the Pixar file's header is damaged der Dateikopf der Pixar-Datei ist beschädigt +@msg the Pixar file's image data is damaged die Bilddaten der Pixar-Datei sind beschädigt @msg the JPEG 2000 file can't be read die JPEG-2000-Datei kann nicht gelesen werden @msg the JPEG 2000 file has an unsupported color space or JPX content die JPEG-2000-Datei enthält einen nicht unterstützten Farbraum oder JPX-Inhalt @msg the PSD image is too large to read ({_1} × {_2} pixels) das PSD-Bild ist zu groß zum Lesen ({_1} × {_2} Pixel) diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 5260c7dc9..ac02f4986 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4670,6 +4670,9 @@ digits Malayalam Μαλαγιάλαμ @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 Pixar file is cut short το αρχείο Pixar είναι περικομμένο +@msg the Pixar file's header is damaged η κεφαλίδα του αρχείου Pixar είναι κατεστραμμένη +@msg the Pixar file's image data is damaged τα δεδομένα εικόνας του αρχείου Pixar είναι κατεστραμμένα @msg the JPEG 2000 file can't be read δεν είναι δυνατή η ανάγνωση του αρχείου JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content το αρχείο JPEG 2000 έχει μη υποστηριζόμενο χρωματικό χώρο ή περιεχόμενο JPX @msg the PSD image is too large to read ({_1} × {_2} pixels) η εικόνα PSD είναι πολύ μεγάλη για ανάγνωση ({_1} × {_2} εικονοστοιχεία) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 2ac467dab..ef00c08fa 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4709,6 +4709,9 @@ axis Both Ambos @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 Pixar file is cut short el archivo Pixar está incompleto +@msg the Pixar file's header is damaged el encabezado del archivo Pixar está dañado +@msg the Pixar file's image data is damaged los datos de imagen del archivo Pixar están dañados @msg the JPEG 2000 file can't be read no se puede leer el archivo JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content el archivo JPEG 2000 tiene un espacio de color o contenido JPX no compatible @msg the PSD image is too large to read ({_1} × {_2} pixels) la imagen PSD es demasiado grande para leerla ({_1} × {_2} píxeles) diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 284735fbe..c7b018f80 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4891,6 +4891,9 @@ imageTrace Outline Ääriviiva @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 Pixar file is cut short tämä Pixar-tiedosto on katkennut +@msg the Pixar file's header is damaged Pixar-tiedoston otsake on vahingoittunut +@msg the Pixar file's image data is damaged Pixar-tiedoston kuvadata on vahingoittunut @msg the JPEG 2000 file can't be read tätä JPEG 2000 -tiedostoa ei voi lukea @msg the JPEG 2000 file has an unsupported color space or JPX content tässä JPEG 2000 -tiedostossa on tukematon väriavaruus tai JPX-sisältö @msg the PSD image is too large to read ({_1} × {_2} pixels) tämä PSD-kuva on liian suuri luettavaksi ({_1} × {_2} pikseliä) diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 0078b6566..11bef0de4 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4717,6 +4717,9 @@ axis Both Les deux @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 Pixar file is cut short le fichier Pixar est tronqué +@msg the Pixar file's header is damaged l'en-tête du fichier Pixar est endommagé +@msg the Pixar file's image data is damaged les données d'image du fichier Pixar sont endommagées @msg the JPEG 2000 file can't be read le fichier JPEG 2000 ne peut pas être lu @msg the JPEG 2000 file has an unsupported color space or JPX content le fichier JPEG 2000 contient un espace colorimétrique ou un contenu JPX non pris en charge @msg the PSD image is too large to read ({_1} × {_2} pixels) l'image PSD est trop grande pour être lue ({_1} × {_2} pixels) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 3018d5845..eb67ac75d 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5233,6 +5233,9 @@ imageTrace Outline Körvonal @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 Pixar file is cut short a Pixar-fájl csonka +@msg the Pixar file's header is damaged a Pixar-fájl fejléce sérült +@msg the Pixar file's image data is damaged a Pixar-fájl képadatai sérültek @msg the JPEG 2000 file can't be read a JPEG 2000 fájl nem olvasható @msg the JPEG 2000 file has an unsupported color space or JPX content a JPEG 2000 fájl nem támogatott színteret vagy JPX-tartalmat tartalmaz @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) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index eb066f48e..dfd836c86 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4751,6 +4751,9 @@ axis Both Entrambi @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 Pixar file is cut short il file Pixar è troncato +@msg the Pixar file's header is damaged l'intestazione del file Pixar è danneggiata +@msg the Pixar file's image data is damaged i dati immagine del file Pixar sono danneggiati @msg the JPEG 2000 file can't be read impossibile leggere il file JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content il file JPEG 2000 contiene uno spazio colore o un contenuto JPX non supportato @msg the PSD image is too large to read ({_1} × {_2} pixels) l'immagine PSD è troppo grande per essere letta ({_1} × {_2} pixel) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 71336831a..674e56cf3 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4792,6 +4792,9 @@ swatch Bright Violet ビビッドバイオレット @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 Pixar file is cut short Pixar ファイルが途中で切れています +@msg the Pixar file's header is damaged Pixar ファイルのヘッダーが壊れています +@msg the Pixar file's image data is damaged Pixar ファイルの画像データが壊れています @msg the JPEG 2000 file can't be read JPEG 2000 ファイルを読み込めません @msg the JPEG 2000 file has an unsupported color space or JPX content JPEG 2000 ファイルには未対応の色空間または JPX コンテンツがあります @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 画像が大きすぎて読み込めません({_1} × {_2} ピクセル) diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index dbcd8bfb2..89171df20 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4408,6 +4408,9 @@ font-technology Variable ცვლადი @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 Pixar file is cut short Pixar ფაილი შეკვეცილია +@msg the Pixar file's header is damaged Pixar ფაილის სათაური დაზიანებულია +@msg the Pixar file's image data is damaged Pixar ფაილის გამოსახულების მონაცემები დაზიანებულია @msg the JPEG 2000 file can't be read JPEG 2000 ფაილის წაკითხვა შეუძლებელია @msg the JPEG 2000 file has an unsupported color space or JPX content JPEG 2000 ფაილს აქვს მხარდაუჭერელი ფერთა სივრცე ან JPX შიგთავსი @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD გამოსახულება ზედმეტად დიდია წასაკითხად ({_1} × {_2} პიქსელი) diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index a98074101..5fbd7cfad 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4719,6 +4719,9 @@ theme Appearance 외관 @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 Pixar file is cut short Pixar 파일이 잘려 있습니다 +@msg the Pixar file's header is damaged Pixar 파일의 헤더가 손상되었습니다 +@msg the Pixar file's image data is damaged Pixar 파일의 이미지 데이터가 손상되었습니다 @msg the JPEG 2000 file can't be read JPEG 2000 파일을 읽을 수 없습니다 @msg the JPEG 2000 file has an unsupported color space or JPX content JPEG 2000 파일에 지원되지 않는 색 공간 또는 JPX 콘텐츠가 있습니다 @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 이미지가 너무 커서 읽을 수 없습니다({_1} × {_2}픽셀) diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 208610868..b5343349a 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4815,6 +4815,9 @@ imageTrace Outline Kontur @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 Pixar file is cut short plik Pixar jest ucięty +@msg the Pixar file's header is damaged nagłówek pliku Pixar jest uszkodzony +@msg the Pixar file's image data is damaged dane obrazu pliku Pixar są uszkodzone @msg the JPEG 2000 file can't be read nie można odczytać pliku JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content plik JPEG 2000 zawiera nieobsługiwaną przestrzeń barw lub zawartość JPX @msg the PSD image is too large to read ({_1} × {_2} pixels) obraz PSD jest zbyt duży, aby go odczytać ({_1} × {_2} pikseli) diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 6a0b875ca..525313a1e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4262,6 +4262,9 @@ axis Both Ambos @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 Pixar file is cut short o arquivo Pixar está incompleto +@msg the Pixar file's header is damaged o cabeçalho do arquivo Pixar está danificado +@msg the Pixar file's image data is damaged os dados de imagem do arquivo Pixar estão danificados @msg the JPEG 2000 file can't be read não é possível ler o arquivo JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content o arquivo JPEG 2000 contém um espaço de cores ou conteúdo JPX sem suporte @msg the PSD image is too large to read ({_1} × {_2} pixels) a imagem PSD é grande demais para ser lida ({_1} × {_2} pixels) diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3dd370fa6..777c19eac 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4748,6 +4748,9 @@ axis Both Оба @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 Pixar file is cut short файл Pixar обрезан +@msg the Pixar file's header is damaged заголовок файла Pixar повреждён +@msg the Pixar file's image data is damaged данные изображения в файле Pixar повреждены @msg the JPEG 2000 file can't be read не удается прочитать файл JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content файл JPEG 2000 содержит неподдерживаемое цветовое пространство или содержимое JPX @msg the PSD image is too large to read ({_1} × {_2} pixels) изображение PSD слишком велико для чтения ({_1} × {_2} пикс.) diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index cf78d8f59..b682c5753 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4626,6 +4626,9 @@ axis Both Båda @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 Pixar file is cut short Pixar-filen är avkortad +@msg the Pixar file's header is damaged Pixar-filens huvud är skadat +@msg the Pixar file's image data is damaged Pixar-filens bilddata är skadade @msg the JPEG 2000 file can't be read JPEG 2000-filen kan inte läsas @msg the JPEG 2000 file has an unsupported color space or JPX content JPEG 2000-filen har en färgrymd eller ett JPX-innehåll som inte stöds @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) diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 9a31ba088..19b2b983b 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4459,6 +4459,9 @@ axis Both Her İkisi @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 Pixar file is cut short Pixar dosyası kısa kesildi +@msg the Pixar file's header is damaged Pixar dosyasının başlığı bozuk +@msg the Pixar file's image data is damaged Pixar dosyasının görüntü verileri bozuk @msg the JPEG 2000 file can't be read JPEG 2000 dosyası okunamıyor @msg the JPEG 2000 file has an unsupported color space or JPX content JPEG 2000 dosyasında desteklenmeyen bir renk alanı veya JPX içeriği var @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) diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 87ba2b473..5ee30d69f 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4735,6 +4735,9 @@ axis Both Обидві @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 Pixar file is cut short файл Pixar обрізаний +@msg the Pixar file's header is damaged заголовок файлу Pixar пошкоджений +@msg the Pixar file's image data is damaged дані зображення у файлі Pixar пошкоджені @msg the JPEG 2000 file can't be read не вдається прочитати файл JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content файл JPEG 2000 містить непідтримуваний колірний простір або вміст JPX @msg the PSD image is too large to read ({_1} × {_2} pixels) зображення PSD завелике для читання ({_1} × {_2} пікс.) diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 3f95c4a3c..34ad90057 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4892,6 +4892,9 @@ theme Appearance Giao diện @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 Pixar file is cut short tệp Pixar bị cắt ngắn +@msg the Pixar file's header is damaged phần đầu của tệp Pixar bị hỏng +@msg the Pixar file's image data is damaged dữ liệu ảnh của tệp Pixar bị hỏng @msg the JPEG 2000 file can't be read không thể đọc tệp JPEG 2000 @msg the JPEG 2000 file has an unsupported color space or JPX content tệp JPEG 2000 có không gian màu hoặc nội dung JPX không được hỗ trợ @msg the PSD image is too large to read ({_1} × {_2} pixels) ảnh PSD quá lớn để đọc ({_1} × {_2} pixel) diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index ff7bf05e1..83b12fe25 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4506,6 +4506,9 @@ imageTrace Outline 轮廓 @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 Pixar file is cut short Pixar 文件不完整 +@msg the Pixar file's header is damaged Pixar 文件头已损坏 +@msg the Pixar file's image data is damaged Pixar 文件的图像数据已损坏 @msg the JPEG 2000 file can't be read 无法读取 JPEG 2000 文件 @msg the JPEG 2000 file has an unsupported color space or JPX content JPEG 2000 文件包含不受支持的色彩空间或 JPX 内容 @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 图像过大,无法读取 ({_1} × {_2} 像素) diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 883a36e87..76041d1a1 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2408,6 +2408,9 @@ file-filter Templates 範本 @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 Pixar file is cut short Pixar 檔案不完整 +@msg the Pixar file's header is damaged Pixar 檔案標頭已損毀 +@msg the Pixar file's image data is damaged Pixar 檔案的影像資料已損毀 @msg the JPEG 2000 file can't be read 無法讀取 JPEG 2000 檔案 @msg the JPEG 2000 file has an unsupported color space or JPX content JPEG 2000 檔案包含不支援的色彩空間或 JPX 內容 @msg the PSD image is too large to read ({_1} × {_2} pixels) PSD 影像過大,無法讀取 ({_1} × {_2} 像素) diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index cdb22fb65..61b891886 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 | 28 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, PICT, Pixar, PS | +| Formats opened or placed | ~30 | 29 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, PICT, 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 | @@ -145,7 +145,8 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | HEIC / HEIF | open, place | no | no | | | 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` | | JPEG 2000 | open, place | yes (JP2 and raw codestream images; multiple codestreams and JPX composition or layers are refused) | no | `.jpf`, `.jpx`, `.jp2`, `.j2k`, `.j2c`, `.jpc`; resolution uses the valid `resd` (display) value on each axis, or else the valid `resc` (capture) value, or 72 ppi; bounded decoding; `tests_jp2read.rs` | -| PICT, Pixar PXR | open, place (legacy) | no | no | rarely used | +| PICT | open, place (legacy) | no | no | rarely used | +| Pixar PXR | open, place (legacy) | yes (any set of the R, G, B and A channels in 8 or 12 bits, dumped or run-length encoded, in tiles; R alone is gray; alpha matted to black or unassociated) | no | recognized by its magic number; 12-bit samples are clamped to 0.0 to 1.0; null tiles are transparent with A and black without; the offsets aren't read, and the image takes the default resolution; a file cut short, with header values or packets the format doesn't define, or past the pixel allowance is refused with a message; tested on synthetic files only; tests in `crates/engine/src/cmd/fileio/tests_pxrread.rs` and `crates/engine/tests/import_fuzz.rs` | | 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 0b327b819..3f2da383a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -427,7 +427,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 (`.png`/`.pns`), JPEG, GIF, WebP, TIFF, BMP (`.bmp`/`.dib`/`.rle`), Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), PSD (`.psd`/`.pdd`)/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 (`.png`/`.pns`), JPEG, GIF, WebP, TIFF, BMP (`.bmp`/`.dib`/`.rle`), Targa (`.tga`/`.vda`/`.icb`/`.vst`), PCX, Pixar (`.pxr`), JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), PSD (`.psd`/`.pdd`)/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. | @@ -3899,7 +3899,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, PSB, PCX or JPEG 2000 file, decoded within the pixel allowance the +`{name?, dataBase64}`: a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD, PSB, PCX, Pixar or JPEG 2000 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 1ec5c221c..b28ada6d6 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, Targa, PCX and JPEG 2000, so file managers list VectorCraft under Open With for them. +PSB, `.ps` PostScript, Targa, PCX, Pixar and JPEG 2000, 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 73f3c02f5..0f0c07452 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -541,7 +541,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, Targa, PCX and JPEG 2000, `.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/tests.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, PCX, Pixar and JPEG 2000, `.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/tests.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:** @@ -1121,6 +1121,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - 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`). - BMP files named `.dib` or `.rle` (run-length encoded bitmaps included), Photoshop files named `.pdd` and PNG files named `.pns` open and place, linked or embedded, as BMP, PSD and PNG files, in any letter case: the Open and Place dialogs list them with their format, the Links panel names their format, and Edit Original opens them. Exports write only `.bmp`, `.psd` and `.png` (`crates/engine/src/cmd/fileio/tests_tiffbmp.rs`, `crates/engine/src/cmd/fileio/tests_psdread.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`). + - Pixar picture files (`.pxr`) open, place and paste, from "The Format of Stored Pictures" in the Pixar Image Computer manuals: any set of the R, G, B and A channels in 8 or 12 bits, dumped or run-length encoded with all four packet kinds and block ends, in tiles of any size (pixels of border tiles past the picture are dropped). R alone is gray, a missing color channel is 0 and a missing A opaque; 12-bit samples are clamped to 0.0 to 1.0; color matted to black becomes straight color; a null tile is transparent when the picture has A and black otherwise. The offsets aren't read, and the image takes the default resolution. Version 2 headers read like version 0. A file cut short, one with a tile whose writing wasn't finished, one with header values or packets the format doesn't define, and one whose pixels as RGBA pass the pixel allowance are refused with a message. Every reader recognizes a Pixar file by its magic number. Tested on synthetic files only (`crates/engine/src/cmd/fileio/tests_pxrread.rs`). - JPEG 2000 JP2 and raw codestream images open and place from `.jpf`, `.jpx`, `.jp2`, `.j2k`, `.j2c` and `.jpc`. Each axis uses the valid display resolution (`resd`), else the valid capture resolution (`resc`), whichever box comes first ([ITU-T T.800](https://www.itu.int/rec/T-REC-T.800/en), I.5.3.7.2); an axis with none uses 72 ppi. Multiple codestreams, JPX composition or layers, and unsupported color spaces are refused with a message. The decoder checks image size before allocating (`crates/engine/src/cmd/fileio/tests_jp2read.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`). @@ -1235,7 +1236,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, PSB, PCX or JPEG 2000 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, PCX, Pixar or JPEG 2000 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. - **Bounded effect length inputs (#4638):** Effect dialog fields with defined length ranges (such as Color Halftone Max Radius 4..127 pt) clamp immediately on commit and reject invalid inputs visibly with an accent highlight without reverting to sub-minimum values. - **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).