diff --git a/ASSETS.md b/ASSETS.md index 5bebefae6..3233c99d0 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -345,6 +345,9 @@ Generated-in-code art is original and has no file to list. This covers the defau | `examples/ribbons.vectorcraft` | VectorCraft contributors | Original artwork built through the VectorCraft command API | MIT OR Apache-2.0 | | | `examples/feature-sheet.vectorcraft` | VectorCraft contributors | Original artwork built through the VectorCraft command API | MIT OR Apache-2.0 | | | `examples/perspective-city.vectorcraft` | VectorCraft contributors | Original artwork built through the VectorCraft command API | MIT OR Apache-2.0 | | +| `crates/engine/tests/fixtures/jpeg2000/checker.jp2` | VectorCraft contributors | Original test image authored for VectorCraft's tests | MIT OR Apache-2.0 | a 2 × 2 checker encoded with FFmpeg's JPEG 2000 encoder; SHA-256 `3de9c2bb5b8aaec241d939b795002a68eb3d50e41d51c43368000804ad3de057` | +| `crates/engine/tests/fixtures/jpeg2000/checker-raw.j2k` | VectorCraft contributors | Original test image authored for VectorCraft's tests | MIT OR Apache-2.0 | a 2 × 2 checker encoded with FFmpeg's JPEG 2000 encoder; SHA-256 `76fa51ab8ccbc2534098b26b8e7fe16dd886e2b6298ba52d06a6b1bde9b61d9e` | +| `crates/engine/tests/fixtures/jpeg2000/checker-300ppi.jp2` | VectorCraft contributors | Original test image authored for VectorCraft's tests | MIT OR Apache-2.0 | a 2 × 2 checker encoded with FFmpeg's JPEG 2000 encoder; SHA-256 `1b1aeed8975d3cdc832da6ecf5855852d07d0c490105121e61a8aefb31af009b` | ## Assets embedded by the PDF backend diff --git a/Cargo.lock b/Cargo.lock index 9362671b0..934be1b12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6208,6 +6208,7 @@ dependencies = [ "craft-docpath", "crc32fast", "flate2", + "hayro-jpeg2000", "image", "kurbo 0.13.1", "log", diff --git a/crates/color/src/cms/icc.rs b/crates/color/src/cms/icc.rs index 8201b8b97..64f42f25b 100644 --- a/crates/color/src/cms/icc.rs +++ b/crates/color/src/cms/icc.rs @@ -149,11 +149,20 @@ impl IccProfile { /// Device values (RGB / CMYK / Gray, 0..1) → sRGB. pub fn to_srgb(&self, v: &[f32], intent: Intent) -> Option<[f32; 3]> { + let mut src = [0.0f32; 4]; + let channels = self.channels(); + src.get_mut(..channels)?.iter_mut().zip(v.iter()).for_each(|(dst, value)| *dst = *value); + self.to_srgb_into(src.get(..channels)?, intent) + } + + /// Convert an exact-width device pixel using the cached transform and stack storage. + pub fn to_srgb_into(&self, v: &[f32], intent: Intent) -> Option<[f32; 3]> { + if v.len() != self.channels() { + return None; + } let xf = self.to_xf(intent)?; - let mut src = v.to_vec(); - src.resize(self.channels(), 0.0); let mut dst = [0.0f32; 3]; - xf.transform(&src, &mut dst).ok()?; + xf.transform(v, &mut dst).ok()?; Some(dst.map(|x| x.clamp(0.0, 1.0))) } diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index f495697ce..7b1199277 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -36,6 +36,7 @@ vectorcraft-plugins = { workspace = true } vectorcraft-affinity = { workspace = true } vectorcraft-scriptcheck = { workspace = true } image = { workspace = true } +hayro-jpeg2000 = { version = "=0.3.5", default-features = false, features = ["std", "simd"] } flate2 = { workspace = true } # The XML parts of Word documents placed as type (place/docx.rs). roxmltree = "0.21" diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index c8cb7218a..7471ffb56 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -33,6 +33,7 @@ pub(crate) mod imagemap; mod legacy_ai; mod load; pub(crate) use load::{MAX_RASTER_ALLOC, size_note}; +mod jp2read; mod metafile; mod naming; mod native; @@ -193,6 +194,8 @@ mod tests_psd; #[cfg(test)] mod tests_psdread; +#[cfg(test)] +mod tests_jp2read; #[cfg(test)] mod tests_pcxread; diff --git a/crates/engine/src/cmd/fileio/formats.rs b/crates/engine/src/cmd/fileio/formats.rs index f72721534..a1687e214 100644 --- a/crates/engine/src/cmd/fileio/formats.rs +++ b/crates/engine/src/cmd/fileio/formats.rs @@ -491,6 +491,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), + // 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), ]; /// Every extension `document.open` reads (the "All readable files" filter of open dialogs). @@ -515,6 +517,12 @@ pub const OPEN_EXTS: &[&str] = &[ "icb", "vst", "pcx", + "jpf", + "jpx", + "jp2", + "j2k", + "j2c", + "jpc", "psd", "psb", "vctemplate", @@ -531,10 +539,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`, Targa or PCX type (File › Open and Place still read them all). On Linux the +/// `.epsf`, `.ps`, Targa, PCX or JPEG 2000 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"]; +pub const UNASSOCIATED_EXTS: &[&str] = &["psd", "psb", "epsf", "ps", "tga", "vda", "icb", "vst", "pcx", "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. @@ -581,6 +589,12 @@ pub const PLACE_EXTS: &[&str] = &[ "icb", "vst", "pcx", + "jpf", + "jpx", + "jp2", + "j2k", + "j2c", + "jpc", "psd", "psb", "vctemplate", diff --git a/crates/engine/src/cmd/fileio/jp2read.rs b/crates/engine/src/cmd/fileio/jp2read.rs new file mode 100644 index 000000000..9e0752c2c --- /dev/null +++ b/crates/engine/src/cmd/fileio/jp2read.rs @@ -0,0 +1,279 @@ +//! JPEG 2000 JP2 and codestream images for File › Open and File › Place. The decoded image is +//! bounded before allocation and converted to RGBA for the common PNG storage path. + +use hayro_jpeg2000::{ColorSpace, DecodeSettings, DecoderContext, Image}; +use vectorcraft_color::cms::{IccProfile, Intent}; + +/// Recognize a JP2 signature box or the start of a raw codestream. The decoder checks the rest. +pub(super) fn is_jpeg2000(bytes: &[u8]) -> bool { + bytes.starts_with(b"\0\0\0\x0cjP \r\n\x87\n") || bytes.starts_with(b"\xff\x4f") +} + +/// Boxes at one JP2 nesting level. Invalid lengths stop the walk at that level. +pub(super) fn boxes(b: &[u8]) -> impl Iterator { + let mut at = 0usize; + std::iter::from_fn(move || { + let header = b.get(at..at.checked_add(8)?)?; + let size = u32::from_be_bytes(header.get(..4)?.try_into().ok()?); + let (length, head) = match size { + 0 => (b.len().checked_sub(at)?, 8usize), + 1 => (usize::try_from(u64::from_be_bytes(b.get(at.checked_add(8)?..at.checked_add(16)?)?.try_into().ok()?)).ok()?, 16usize), + n => (n as usize, 8usize), + }; + if length < head { + return None; + } + let end = at.checked_add(length)?; + let data = b.get(at.checked_add(head)?..end)?; + let ty = header.get(4..8)?; + at = end; + Some((ty, data)) + }) +} + +/// The decoder ignores JPX layers and composition and silently uses the last top-level jp2c. +fn unsupported_jpx_content(bytes: &[u8]) -> bool { + fn association_has_content(data: &[u8], depth: usize) -> bool { + if depth >= 16 { + return true; + } + boxes(data).any(|(ty, child)| { + matches!(ty, b"jp2c" | b"jpch" | b"jplh" | b"comp" | b"ftbl") || (ty == b"asoc" && association_has_content(child, depth + 1)) + }) + } + let mut codestreams = 0u8; + for (ty, data) in boxes(bytes) { + match ty { + b"jp2c" => { + codestreams = codestreams.saturating_add(1); + if codestreams > 1 { + return true; + } + } + b"jpch" | b"jplh" | b"comp" | b"ftbl" => return true, + b"asoc" if association_has_content(data, 0) => return true, + _ => {} + } + } + false +} + +/// The largest embedded ICC profile read. +const MAX_PROFILE: usize = 4 * 1024 * 1024; +/// A codestream numbers its tiles 0 to 65,534 (Isot in SOT, ITU-T T.800 A.4.2). +const MAX_TILES: u64 = 65_535; +/// Bytes the decoder keeps for each component of each tile before decoding any of them. +const TILE_COMPONENT_BYTES: u64 = 256; + +/// SIZ follows SOC in the codestream; Lsiz includes its two length bytes. Returns the component +/// count, the image size and the tile count. +fn codestream_size(bytes: &[u8]) -> Option<(u64, u32, u32, u64)> { + let stream = if bytes.starts_with(b"\xff\x4f") { bytes } else { boxes(bytes).find_map(|(ty, data)| (ty == b"jp2c").then_some(data))? }; + if stream.get(..4)? != b"\xff\x4f\xff\x51" { + return None; + } + let length = u16::from_be_bytes(stream.get(4..6)?.try_into().ok()?) as usize; + let count = u16::from_be_bytes(stream.get(40..42)?.try_into().ok()?) as usize; + if count == 0 || length != 38usize.checked_add(count.checked_mul(3)?)? || stream.get(4..4usize.checked_add(length)?)?.len() != length { + return None; + } + let be32 = |range: std::ops::Range| stream.get(range).and_then(|v| v.try_into().ok()).map(u32::from_be_bytes); + let width = be32(8..12)?.checked_sub(be32(16..20)?)?; + let height = be32(12..16)?.checked_sub(be32(20..24)?)?; + // Tiles across and down (T.800 B-5): the grid from the tile offset, in whole tiles. + let tiles = |grid: u32, offset: u32, tile: u32| match (grid.checked_sub(offset), tile) { + (Some(span), 1..) => Some(u64::from(span).div_ceil(u64::from(tile))), + _ => None, + }; + let across = tiles(be32(8..12)?, be32(32..36)?, be32(24..28)?)?; + let down = tiles(be32(12..16)?, be32(36..40)?, be32(28..32)?)?; + Some((count as u64, width, height, across.checked_mul(down)?)) +} + +/// One `jp2h` header with at most one palette, component mapping and channel definition box, so +/// the checks below read the boxes the decoder uses. +fn single_header_boxes(bytes: &[u8]) -> bool { + let mut headers = boxes(bytes).filter(|(ty, _)| *ty == b"jp2h"); + let (Some((_, header)), None) = (headers.next(), headers.next()) else { + return false; + }; + [b"pclr", b"cmap", b"cdef"].iter().all(|kind| boxes(header).filter(|(ty, _)| ty == kind).count() <= 1) +} + +/// The number of channel definitions in a JP2 `cdef` box, `None` without one. Only color channels +/// 1 to n, in any order, followed by at most one opacity channel for the whole image are read; +/// any other layout (premultiplied opacity, opacity before a color, an unassociated channel) gives +/// `Some(0)`, which no image matches. +fn channel_definitions(bytes: &[u8]) -> Option { + if bytes.starts_with(b"\xff\x4f") { + return None; + } + let header = boxes(bytes).find_map(|(ty, data)| (ty == b"jp2h").then_some(data))?; + let cdef = boxes(header).find_map(|(ty, data)| (ty == b"cdef").then_some(data))?; + let count = cdef.get(..2).and_then(|n| n.try_into().ok()).map_or(0, |n: [u8; 2]| usize::from(u16::from_be_bytes(n))); + let Some(entries) = cdef.get(2..).filter(|rest| count > 0 && rest.len() == count * 6) else { + return Some(0); + }; + // (channel, type, association) by channel. + let mut defs: Vec<[u16; 3]> = + entries.as_chunks::<6>().0.iter().map(|&[a, b, c, d, e, f]| [[a, b], [c, d], [e, f]].map(u16::from_be_bytes)).collect(); + defs.sort_unstable(); + let opacity = defs.last().is_some_and(|&[_, kind, association]| kind == 1 && association == 0); + let colors = defs.len() - usize::from(opacity); + let mut seen = vec![false; colors]; + let layout_ok = defs.iter().enumerate().all(|(i, &[channel, kind, association])| { + usize::from(channel) == i + && (i >= colors + || (kind == 0 + && usize::from(association).checked_sub(1).and_then(|slot| seen.get_mut(slot)).is_some_and(|s| !std::mem::replace(s, true)))) + }); + Some(if layout_ok { defs.len() as u64 } else { 0 }) +} + +/// The column count of a JP2 palette (`pclr`), `Ok(None)` without one. A component mapping (`cmap`) +/// beside it needs one entry per column, each naming a codestream component and, when it maps +/// through the palette, a palette column; anything else is `Err`. +fn palette_columns(bytes: &[u8], components: u64) -> Result, ()> { + if bytes.starts_with(b"\xff\x4f") { + return Ok(None); + } + let Some(header) = boxes(bytes).find_map(|(ty, data)| (ty == b"jp2h").then_some(data)) else { + return Ok(None); + }; + let Some(palette) = boxes(header).find_map(|(ty, data)| (ty == b"pclr").then_some(data)) else { + return Ok(None); + }; + let columns = palette.get(2).copied().ok_or(())?; + // Each column's descriptor: unsigned, 1 to 31 bits, as codestream samples are. + let depths = palette.get(3..3 + usize::from(columns)).ok_or(())?; + if columns == 0 || depths.iter().any(|&d| d & 0x80 != 0 || (d & 0x7f) + 1 > 31) { + return Err(()); + } + // Without a mapping the decoder maps every column from the first component. + let Some(mapping) = boxes(header).find_map(|(ty, data)| (ty == b"cmap").then_some(data)) else { + return Ok(Some(u64::from(columns))); + }; + if mapping.len() != usize::from(columns) * 4 { + return Err(()); + } + for &[c0, c1, kind, column] in mapping.as_chunks::<4>().0 { + let valid = u64::from(u16::from_be_bytes([c0, c1])) < components + && match kind { + 0 => true, + 1 => column < columns, + _ => false, + }; + if !valid { + return Err(()); + } + } + Ok(Some(u64::from(columns))) +} + +/// Decode within the raster allocation allowance, counting source samples, RGBA output, and +/// decoder intermediates. The decoder may need multiple full-sized component planes. +pub(super) fn decode(bytes: &[u8], max_alloc: u64) -> Result { + if bytes.starts_with(b"\0\0\0\x0cjP \r\n\x87\n") && unsupported_jpx_content(bytes) { + return Err("the JPEG 2000 file has an unsupported color space or JPX content".into()); + } + if bytes.starts_with(b"\0\0\0\x0cjP \r\n\x87\n") && !single_header_boxes(bytes) { + return Err("the JPEG 2000 file can't be read".into()); + } + let (codestream_channels, declared_w, declared_h, tile_count) = codestream_size(bytes).ok_or("the JPEG 2000 file can't be read")?; + if tile_count == 0 || tile_count > MAX_TILES { + return Err("the JPEG 2000 file can't be read".into()); + } + let declared_pixels = u64::from(declared_w).checked_mul(u64::from(declared_h)); + if declared_w == 0 + || declared_h == 0 + || declared_pixels.and_then(|n| n.checked_mul(codestream_channels)).and_then(|n| n.checked_mul(4)).is_none_or(|n| n > max_alloc) + || tile_count.checked_mul(codestream_channels).and_then(|n| n.checked_mul(TILE_COMPONENT_BYTES)).is_none_or(|n| n > max_alloc) + { + return Err(format!("an image of {declared_w} × {declared_h} pixels is too large to draw")); + } + // The decoder copies an embedded profile (after a 3-byte `colr` header) when it reads the file. + let header = boxes(bytes).find_map(|(ty, data)| (ty == b"jp2h").then_some(data)).unwrap_or_default(); + if boxes(header).any(|(ty, data)| ty == b"colr" && data.len() > MAX_PROFILE + 3) { + return Err("the JPEG 2000 file has an unsupported color space or JPX content".into()); + } + let palette = palette_columns(bytes, codestream_channels).map_err(|()| "the JPEG 2000 file can't be read")?; + // The decoder keeps each palette value in 8 bytes and each row in its own list. + let palette_rows = boxes(header).find_map(|(ty, data)| (ty == b"pclr").then(|| data.get(..2)).flatten()); + if let (Some(columns), Some(&[r0, r1])) = (palette, palette_rows) + && u64::from(u16::from_be_bytes([r0, r1])) * (columns * 8 + 24) > max_alloc + { + return Err(format!("an image of {declared_w} × {declared_h} pixels is too large to draw")); + } + let image = Image::new(bytes, &DecodeSettings::default()).map_err(|_| "the JPEG 2000 file can't be read")?; + let (w, h) = (image.width(), image.height()); + let color = image.color_space(); + let channels = match color { + ColorSpace::Gray => 1, + ColorSpace::RGB => 3, + ColorSpace::CMYK => 4, + ColorSpace::Icc { num_channels: 1 | 3 | 4, profile } if profile.len() <= MAX_PROFILE => color.num_channels(), + _ => return Err("the JPEG 2000 file has an unsupported color space or JPX content".into()), + }; + let stride = u64::from(channels) + u64::from(image.has_alpha()); + // The decoder sizes its output by the palette's columns and writes one channel per mapping + // entry, so the columns, the entries and the output channels must all match. + if palette.is_some_and(|columns| columns != stride) { + return Err("the JPEG 2000 file can't be read".into()); + } + // The decoder reorders the channels by their definitions (`cdef`) and drops any channel without + // one, so a definition is needed for every channel it writes. + if channel_definitions(bytes).is_some_and(|count| count != stride || count != palette.unwrap_or(codestream_channels)) { + return Err("the JPEG 2000 file can't be read".into()); + } + let plane_channels = codestream_channels.max(stride).max(palette.unwrap_or(0)); + let too_large = || format!("an image of {w} × {h} pixels is too large to draw"); + let pixels = u64::from(w).checked_mul(u64::from(h)).ok_or_else(too_large)?; + let size = pixels.checked_mul(stride).ok_or_else(too_large)?; + let rgba_size = pixels.checked_mul(4).ok_or_else(too_large)?; + // hayro 0.3.5 keeps a f32 plane per component, including alpha, and a second + // f32 coefficient plane per component. Palette mapping or channel definitions + // can temporarily clone the component planes. Reserve another 8 bytes per + // pixel for IDWT and code-block scratch while samples and RGBA coexist. + let plane_bytes = pixels.checked_mul(plane_channels).and_then(|n| n.checked_mul(4)).ok_or_else(too_large)?; + let scratch_bytes = plane_bytes.checked_mul(2).and_then(|n| n.checked_add(pixels.checked_mul(8)?)).ok_or_else(too_large)?; + let working = + size.checked_add(rgba_size).and_then(|n| n.checked_add(plane_bytes)).and_then(|n| n.checked_add(scratch_bytes)).ok_or_else(too_large)?; + if w == 0 || h == 0 || working > max_alloc { + return Err(too_large()); + } + let len = usize::try_from(size).map_err(|_| too_large())?; + let mut samples = vec![0; len]; + image.decode_into(&mut samples, &mut DecoderContext::default()).map_err(|_| "the JPEG 2000 file can't be read")?; + let profile = match color { + ColorSpace::Icc { profile, .. } => { + Some(IccProfile::from_bytes(None, profile).map_err(|_| "the JPEG 2000 file has an unsupported color space or JPX content")?) + } + _ => None, + }; + let cms = vectorcraft_color::cms::active(); + let mut rgba = Vec::with_capacity(usize::try_from(rgba_size).map_err(|_| too_large())?); + for sample in samples.chunks_exact(stride as usize) { + let at = |i: usize| sample.get(i).copied().unwrap_or(0); + let rgb = match (color, profile.as_ref()) { + (ColorSpace::Gray, _) => [at(0); 3], + (ColorSpace::RGB, _) => [at(0), at(1), at(2)], + (ColorSpace::CMYK, _) => { + cms.cmyk_to_srgb([at(0), at(1), at(2), at(3)].map(|v| f32::from(v) / 255.0), false).map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8) + } + (ColorSpace::Icc { .. }, Some(profile)) => { + let mut values = [0.0f32; 4]; + for (dst, src) in values.iter_mut().zip(sample.iter().take(usize::from(channels))) { + *dst = f32::from(*src) / 255.0; + } + profile + .to_srgb_into(&values[..usize::from(channels)], Intent::RelativeColorimetric) + .ok_or("the JPEG 2000 file has an unsupported color space or JPX content")? + .map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8) + } + _ => return Err("the JPEG 2000 file has an unsupported color space or JPX content".into()), + }; + let [r, g, b] = rgb; + rgba.extend_from_slice(&[r, g, b, if image.has_alpha() { at(usize::from(channels)) } else { 255 }]); + } + image::RgbaImage::from_raw(w, h, rgba).ok_or_else(|| "the JPEG 2000 file can't be read".into()) +} diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index c2e58e39e..008313069 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::jp2read::is_jpeg2000(bytes) { + return format("jpeg2000"); + } if let Some(f) = image::guess_format(bytes).ok().and_then(image_format) { return Some(f); } @@ -631,6 +634,9 @@ pub fn raster_image(bytes: &[u8]) -> Result { let cms = vectorcraft_color::cms::active(); return as_png(psdread::decode(bytes, MAX_RASTER_ALLOC, |c| cms.cmyk_to_srgb(c, false)).map_err(err)?, ppi); } + if super::jp2read::is_jpeg2000(bytes) { + return as_png(super::jp2read::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/ppi.rs b/crates/engine/src/cmd/fileio/ppi.rs index 61b741756..fd2ffbf8c 100644 --- a/crates/engine/src/cmd/fileio/ppi.rs +++ b/crates/engine/src/cmd/fileio/ppi.rs @@ -1,6 +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, a PCX -//! header's HDpi and VDpi. Place +//! header's HDpi and VDpi, or JP2 capture and display resolution boxes. 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. @@ -24,12 +24,47 @@ pub fn resolution(bytes: &[u8]) -> Option<(f64, f64)> { super::psdread::resolution(bytes) } else if super::pcxread::is_pcx(bytes) { super::pcxread::resolution(bytes) + } else if super::jp2read::is_jpeg2000(bytes) { + jp2(bytes) } else { None }; r.filter(|(x, y)| [*x, *y].iter().all(|v| v.is_finite() && (1.0..=100_000.0).contains(v))) } +fn jp2_axis(n: u16, d: u16, e: i8) -> Option { + if d == 0 { + return None; + } + let ppi = f64::from(n) / f64::from(d) * 10f64.powi(i32::from(e)) * INCH_M; + (ppi.is_finite() && (1.0..=100_000.0).contains(&ppi)).then_some(ppi) +} + +/// JP2 resolution is independent on each axis. Later capture or display boxes replace earlier +/// valid values for that axis; an invalid later axis leaves the earlier value intact. +fn jp2(b: &[u8]) -> Option<(f64, f64)> { + if !b.starts_with(b"\0\0\0\x0cjP \r\n\x87\n") { + return Some((72.0, 72.0)); + } + let (mut x, mut y) = (None, None); + for (_, header) in super::jp2read::boxes(b).filter(|(ty, _)| *ty == b"jp2h") { + for (_, res) in super::jp2read::boxes(header).filter(|(ty, _)| *ty == b"res ") { + for (_, data) in super::jp2read::boxes(res).filter(|(ty, _)| *ty == b"resc" || *ty == b"resd") { + let Some([vn0, vn1, vd0, vd1, hn0, hn1, hd0, hd1, ve, he]): Option<[u8; 10]> = data.get(..10).and_then(|v| v.try_into().ok()) else { + continue; + }; + if let Some(value) = jp2_axis(u16::from_be_bytes([hn0, hn1]), u16::from_be_bytes([hd0, hd1]), he as i8) { + x = Some(value); + } + if let Some(value) = jp2_axis(u16::from_be_bytes([vn0, vn1]), u16::from_be_bytes([vd0, vd1]), ve as i8) { + y = Some(value); + } + } + } + } + Some((x.unwrap_or(72.0), y.unwrap_or(72.0))) +} + /// `n` bytes at `at` (`None` past the end; offsets come from the file, so no overflow either). fn slice(b: &[u8], at: usize, n: usize) -> Option<&[u8]> { b.get(at..at.checked_add(n)?) diff --git a/crates/engine/src/cmd/fileio/tests_jp2read.rs b/crates/engine/src/cmd/fileio/tests_jp2read.rs new file mode 100644 index 000000000..54481c403 --- /dev/null +++ b/crates/engine/src/cmd/fileio/tests_jp2read.rs @@ -0,0 +1,293 @@ +//! JPEG 2000 Open, Place, pixel and resolution regressions using original synthetic fixtures. + +use serde_json::json; + +use super::jp2read::is_jpeg2000; +use super::*; + +const JP2: &[u8] = include_bytes!("../../../tests/fixtures/jpeg2000/checker.jp2"); +const RAW: &[u8] = include_bytes!("../../../tests/fixtures/jpeg2000/checker-raw.j2k"); +const PPI300: &[u8] = include_bytes!("../../../tests/fixtures/jpeg2000/checker-300ppi.jp2"); +const COLORS: [[u8; 4]; 4] = [[255, 0, 0, 255], [0, 255, 0, 255], [0, 0, 255, 255], [255; 4]]; + +fn pixels(bytes: &[u8]) -> Vec<[u8; 4]> { + let raster = load::raster_image(bytes).unwrap(); + image::load_from_memory(&raster.blob.bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect() +} + +fn boxed(ty: &[u8; 4], data: &[u8]) -> Vec { + let mut out = ((data.len() + 8) as u32).to_be_bytes().to_vec(); + out.extend(ty); + out.extend(data); + out +} + +fn with_resolution(children: &[Vec]) -> Vec { + let mut file = JP2.to_vec(); + let at = file.windows(4).position(|w| w == b"jp2h").unwrap() - 4; + let old_len = u32::from_be_bytes(file[at..at + 4].try_into().unwrap()) as usize; + let mut header = file[at + 8..at + old_len].to_vec(); + if !children.is_empty() { + header.extend(boxed(b"res ", &children.concat())); + } + file.splice(at..at + old_len, boxed(b"jp2h", &header)); + file +} + +fn res(ty: &[u8; 4], vr: u16, hr: u16) -> Vec { + let mut data = Vec::new(); + for n in [vr, hr] { + data.extend(n.to_be_bytes()); + data.extend(254u16.to_be_bytes()); + } + data.extend([2, 2]); + boxed(ty, &data) +} + +#[test] +fn checker_opens_and_places_with_exact_pixels() { + for (name, bytes) in [("checker.jp2", JP2), ("checker-raw.j2k", RAW)] { + assert_eq!(pixels(bytes), COLORS, "{name}"); + let mut s = Session::new(); + let encoded = vectorcraft_format::base64_encode(bytes); + let opened = s.execute("document.open", &json!({"name": name, "dataBase64": encoded})).unwrap(); + assert_eq!(opened["format"], "jpeg2000"); + assert_eq!(s.doc().unwrap().doc.artboards[0].rect.size(), vectorcraft_geom::Size::new(2.0, 2.0)); + let blob = s.doc().unwrap().doc.images.values().next().unwrap(); + assert_eq!(image::load_from_memory(&blob.bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect::>(), COLORS); + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + let placed = s.execute("file.place", &json!({"name": name, "dataBase64": encoded})).unwrap(); + assert_eq!(placed["format"], "jpeg2000"); + let blob = s.doc().unwrap().doc.images.values().next().unwrap(); + assert_eq!(image::load_from_memory(&blob.bytes).unwrap().to_rgba8().pixels().map(|p| p.0).collect::>(), COLORS); + assert!(load::texture_image(bytes).is_ok()); + } +} + +#[test] +fn signatures_and_every_extension() { + assert!(is_jpeg2000(JP2) && is_jpeg2000(RAW)); + for ext in ["jpf", "jpx", "jp2", "j2k", "j2c", "jpc"] { + assert_eq!(load::detect(&format!("a.{ext}"), b"invalid").map(|f| f.id), Some("jpeg2000")); + assert!(OPEN_EXTS.contains(&ext) && PLACE_EXTS.contains(&ext) && UNASSOCIATED_EXTS.contains(&ext)); + } + assert_eq!(load::detect("unknown.bin", JP2).map(|f| f.id), Some("jpeg2000")); + assert_eq!(load::detect("unknown.bin", RAW).map(|f| f.id), Some("jpeg2000")); +} + +#[test] +fn resolution_boxes_use_last_valid_axis_and_72_fallback() { + let check = |children: &[Vec], want: (f64, f64)| { + let file = with_resolution(children); + let got = ppi::resolution(&file).unwrap(); + assert!((got.0 - want.0).abs() < 0.01 && (got.1 - want.1).abs() < 0.01, "{got:?} != {want:?}"); + let mut s = Session::new(); + s.execute("document.open", &json!({"name": "a.jp2", "dataBase64": vectorcraft_format::base64_encode(&file)})).unwrap(); + let size = s.doc().unwrap().doc.artboards[0].rect.size(); + assert!((size.width - 144.0 / want.0).abs() < 0.01 && (size.height - 144.0 / want.1).abs() < 0.01); + }; + check(&[], (72.0, 72.0)); + check(&[res(b"resc", 30000, 30000)], (300.0, 300.0)); + check(&[res(b"resd", 15000, 15000)], (150.0, 150.0)); + check(&[res(b"resc", 30000, 30000), res(b"resd", 15000, 15000)], (150.0, 150.0)); + check(&[res(b"resd", 15000, 15000), res(b"resc", 30000, 30000)], (300.0, 300.0)); + check(&[res(b"resc", 30000, 15000)], (150.0, 300.0)); + assert_eq!(ppi::resolution(RAW), Some((72.0, 72.0))); + assert!((ppi::resolution(PPI300).unwrap().0 - 300.0).abs() < 0.01); + let mut s = Session::new(); + s.execute("document.open", &json!({"name": "a.jp2", "dataBase64": vectorcraft_format::base64_encode(PPI300)})).unwrap(); + let size = s.doc().unwrap().doc.artboards[0].rect.size(); + assert!((size.width - 0.48).abs() < 0.001 && (size.height - 0.48).abs() < 0.001); + let blob = s.doc().unwrap().doc.images.values().next().unwrap(); + let stored = ppi::resolution(&blob.bytes).unwrap(); + assert!((stored.0 - 300.0).abs() < 0.01 && (stored.1 - 300.0).abs() < 0.01); +} + +#[test] +fn malformed_resolution_and_image_data_are_bounded() { + let good = res(b"resc", 30000, 15000); + let mut zero = res(b"resd", 15000, 15000); + zero[10..12].copy_from_slice(&0u16.to_be_bytes()); + let got = ppi::resolution(&with_resolution(&[good.clone(), zero])).unwrap(); + assert!((got.0 - 150.0).abs() < 0.01 && (got.1 - 300.0).abs() < 0.01); + let short = boxed(b"resd", &[0; 9]); + assert!(ppi::resolution(&with_resolution(&[good.clone(), short])).unwrap().0 > 149.0); + assert_eq!(ppi::resolution(&with_resolution(&[boxed(b"resc", &[0; 9])])), Some((72.0, 72.0))); + let mut over = good.clone(); + over[..4].copy_from_slice(&u32::MAX.to_be_bytes()); + assert_eq!(ppi::resolution(&with_resolution(&[over])), Some((72.0, 72.0))); + let mut outer_over = JP2.to_vec(); + let jp2h = outer_over.windows(4).position(|w| w == b"jp2h").unwrap() - 4; + outer_over[jp2h..jp2h + 4].copy_from_slice(&u32::MAX.to_be_bytes()); + assert_eq!(ppi::resolution(&outer_over), Some((72.0, 72.0))); + let mut extended = Vec::from(1u32.to_be_bytes()); + extended.extend(b"resd"); + extended.extend(26u64.to_be_bytes()); + extended.extend(&res(b"resd", 15000, 30000)[8..]); + assert!(ppi::resolution(&with_resolution(&[extended])).unwrap().0 > 299.0); + let mut to_end = res(b"resd", 15000, 30000); + to_end[..4].copy_from_slice(&0u32.to_be_bytes()); + assert!(ppi::resolution(&with_resolution(&[to_end])).unwrap().0 > 299.0); + for bytes in [&JP2[..12], &RAW[..4], &JP2[..JP2.len() - 5]] { + assert!(load::raster_image(bytes).is_err()); + } + assert!(super::jp2read::decode(JP2, 8).is_err()); + // RGBA (16 bytes) and the float component planes (48 bytes) each fit; + // their concurrent allocation with samples and scratch does not. + assert!(super::jp2read::decode(JP2, 80).unwrap_err().contains("too large to draw")); + let mut huge = RAW.to_vec(); + huge[8..12].copy_from_slice(&100_000u32.to_be_bytes()); + huge[12..16].copy_from_slice(&100_000u32.to_be_bytes()); + assert!(load::raster_image(&huge).is_err()); + let mut jpx = JP2.to_vec(); + jpx.extend(boxed(b"jp2c", RAW)); + assert!(load::raster_image(&jpx).err().unwrap().to_string().contains("unsupported color space or JPX content")); + let mut layered = JP2.to_vec(); + layered.extend(boxed(b"jpch", &[])); + assert!(load::raster_image(&layered).err().unwrap().to_string().contains("unsupported color space or JPX content")); + let mut wrapped = JP2.to_vec(); + wrapped.extend(boxed(b"asoc", &boxed(b"jp2c", RAW))); + assert!(load::raster_image(&wrapped).err().unwrap().to_string().contains("unsupported color space or JPX content")); +} + +#[test] +fn compatible_jpx_brand_alone_keeps_checker_pixels() { + let mut branded = JP2.to_vec(); + branded[12..16].copy_from_slice(&24u32.to_be_bytes()); + branded.splice(32..32, b"jpx ".iter().copied()); + assert_eq!(pixels(&branded), COLORS); +} + +#[test] +fn codestream_component_count_bounds_decode_planes() { + assert_eq!(super::jp2read::decode(RAW, 1000).unwrap().pixels().map(|p| p.0).collect::>(), COLORS); + let mut many = RAW.to_vec(); + let component = many[42..45].to_vec(); + many.splice(42..42, component.repeat(17)); + many[4..6].copy_from_slice(&98u16.to_be_bytes()); // 38 + 3 * 20 + many[40..42].copy_from_slice(&20u16.to_be_bytes()); + assert!(super::jp2read::decode(&many, 1000).unwrap_err().contains("too large to draw")); + many[0..4].copy_from_slice(&[0, 0, 0, 0]); + assert!(super::jp2read::decode(&many, 1000).unwrap_err().contains("can't be read")); +} + +fn with_header_boxes(extra: &[Vec]) -> Vec { + let header = super::jp2read::boxes(JP2).find_map(|(ty, data)| (ty == b"jp2h").then_some(data)).unwrap(); + let mut children = header.to_vec(); + children.extend(extra.concat()); + super::jp2read::boxes(JP2) + .map(|(ty, data)| if ty == b"jp2h" { boxed(b"jp2h", &children) } else { boxed(ty.try_into().unwrap(), data) }) + .collect::>() + .concat() +} + +#[test] +fn channel_definitions_must_cover_every_channel() { + // (channel, type, association): color channels 1 to 3, an opacity channel (type 1). + let cdef = |defs: &[[u16; 3]]| { + let mut data = (defs.len() as u16).to_be_bytes().to_vec(); + data.extend(defs.iter().flatten().flat_map(|v| v.to_be_bytes())); + boxed(b"cdef", &data) + }; + assert_eq!(pixels(&with_header_boxes(&[cdef(&[[0, 0, 1], [1, 0, 2], [2, 0, 3]])])), COLORS); + // Channel 0 as green and channel 1 as red swap the two. + let swapped = [[0, 255, 0, 255], [255, 0, 0, 255], [0, 0, 255, 255], [255; 4]]; + assert_eq!(pixels(&with_header_boxes(&[cdef(&[[0, 0, 2], [1, 0, 1], [2, 0, 3]])])), swapped); + for defs in [ + vec![[0, 0, 1]], + vec![[0, 0, 1], [1, 0, 2], [2, 1, 0]], + vec![[0, 0, 1], [1, 0, 2], [2, 0, 3], [3, 1, 0]], + // Opacity first, a premultiplied opacity, a repeated color, an unassociated color. + vec![[0, 1, 0], [1, 0, 1], [2, 0, 2]], + vec![[0, 0, 1], [1, 0, 2], [2, 2, 0]], + vec![[0, 0, 1], [1, 0, 1], [2, 0, 3]], + vec![[0, 0, 1], [1, 0, 2], [2, 0, 0]], + ] { + assert!(load::raster_image(&with_header_boxes(&[cdef(&defs)])).err().unwrap().to_string().contains("can't be read"), "{defs:?}"); + } + // A second definition box, and a second header, are refused. + let full = cdef(&[[0, 0, 1], [1, 0, 2], [2, 0, 3]]); + assert!(load::raster_image(&with_header_boxes(&[full.clone(), cdef(&[[0, 0, 1]])])).err().unwrap().to_string().contains("can't be read")); + let mut two_headers = JP2.to_vec(); + let header = super::jp2read::boxes(JP2).find_map(|(ty, data)| (ty == b"jp2h").then_some(data)).unwrap(); + two_headers.extend(boxed(b"jp2h", header)); + assert!(load::raster_image(&two_headers).err().unwrap().to_string().contains("can't be read")); +} + +#[test] +fn palette_needs_one_mapping_per_column() { + // 256 entries of three 8-bit columns, entry i being (i, i, i). + let mut palette = vec![1, 0, 3, 7, 7, 7]; + palette.extend((0..=255u8).flat_map(|i| [i; 3])); + let pclr = boxed(b"pclr", &palette); + let mut deep = vec![1, 0, 3, 7, 7, 31]; + deep.extend((0..=255u8).flat_map(|i| [i, i, 0, 0, 0, i])); + let deep = boxed(b"pclr", &deep); + let cmap = |entries: &[[u8; 4]]| boxed(b"cmap", &entries.concat()); + // Each component through its own column gives the same pixels. + let mapped = with_header_boxes(&[pclr.clone(), cmap(&[[0, 0, 1, 0], [0, 1, 1, 1], [0, 2, 0, 0]])]); + assert_eq!(pixels(&mapped), COLORS); + // 256 rows of three 8-byte values and a 24-byte list each (12,288 bytes) pass a 12,000-byte allowance. + assert!(super::jp2read::decode(&mapped, 12_000).unwrap_err().contains("too large to draw")); + for extra in [ + vec![pclr.clone(), cmap(&[[0, 0, 1, 0], [0, 1, 1, 1], [0, 2, 1, 2], [0, 0, 1, 0]])], + vec![pclr.clone(), cmap(&[[0, 0, 1, 0], [0, 1, 1, 1], [0, 9, 1, 2]])], + vec![pclr.clone(), cmap(&[[0, 0, 1, 0], [0, 1, 1, 1], [0, 2, 1, 3]])], + vec![pclr.clone(), cmap(&[[0, 0, 1, 0], [0, 1, 1, 1]])], + // A 32-bit column. + vec![deep.clone(), cmap(&[[0, 0, 1, 0], [0, 1, 1, 1], [0, 2, 1, 2]])], + ] { + assert!(load::raster_image(&with_header_boxes(&extra)).err().unwrap().to_string().contains("can't be read")); + } +} + +#[test] +fn tile_grid_is_bounded_before_decoding() { + let tiled = |size: u32, tile: u32| { + let mut file = RAW.to_vec(); + file[8..12].copy_from_slice(&size.to_be_bytes()); + file[12..16].copy_from_slice(&size.to_be_bytes()); + file[24..28].copy_from_slice(&tile.to_be_bytes()); + file[28..32].copy_from_slice(&tile.to_be_bytes()); + file + }; + // 1,000 × 1,000 one-pixel tiles are more than a codestream can number. + assert!(super::jp2read::decode(&tiled(1000, 1), MAX_RASTER_ALLOC).unwrap_err().contains("can't be read")); + assert!(super::jp2read::decode(&tiled(2, 0), MAX_RASTER_ALLOC).unwrap_err().contains("can't be read")); + // 255 × 255 one-pixel tiles of three components need more than this allowance before decoding. + assert!(super::jp2read::decode(&tiled(255, 1), 1 << 20).unwrap_err().contains("too large to draw")); +} + +#[test] +fn cielab_images_are_refused() { + let header = super::jp2read::boxes(JP2).find_map(|(ty, data)| (ty == b"jp2h").then_some(data)).unwrap(); + // Enumerated color space 14, CIELab, with its default range and offset. + let children = super::jp2read::boxes(header) + .map(|(ty, data)| if ty == b"colr" { boxed(b"colr", &[1, 0, 0, 0, 0, 0, 14]) } else { boxed(ty.try_into().unwrap(), data) }) + .collect::>(); + let file = super::jp2read::boxes(JP2) + .map(|(ty, data)| if ty == b"jp2h" { boxed(b"jp2h", &children.concat()) } else { boxed(ty.try_into().unwrap(), data) }) + .collect::>() + .concat(); + assert!(load::raster_image(&file).err().unwrap().to_string().contains("unsupported color space")); +} + +#[test] +fn embedded_srgb_profile_keeps_checker_pixels() { + let icc = vectorcraft_color::cms::icc_bytes(vectorcraft_color::cms::SRGB).unwrap(); + let mut colr_data = vec![2, 0, 0]; + colr_data.extend_from_slice(&icc); + let header = super::jp2read::boxes(JP2).find_map(|(ty, data)| (ty == b"jp2h").then_some(data)).unwrap(); + let children = super::jp2read::boxes(header) + .map(|(ty, data)| if ty == b"colr" { boxed(b"colr", &colr_data) } else { boxed(ty.try_into().unwrap(), data) }) + .collect::>(); + let file = super::jp2read::boxes(JP2) + .map(|(ty, data)| if ty == b"jp2h" { boxed(b"jp2h", &children.concat()) } else { boxed(ty.try_into().unwrap(), data) }) + .collect::>() + .concat(); + assert_eq!(pixels(&file), COLORS); + // A profile past 4 MiB is refused before the decoder reads the file. + let mut big = vec![2, 0, 0]; + big.resize(3 + 4 * 1024 * 1024 + 1, 0); + assert!(super::jp2read::decode(&with_header_boxes(&[boxed(b"colr", &big)]), MAX_RASTER_ALLOC).unwrap_err().contains("unsupported color space")); +} diff --git a/crates/engine/tests/fixtures/jpeg2000/checker-300ppi.jp2 b/crates/engine/tests/fixtures/jpeg2000/checker-300ppi.jp2 new file mode 100644 index 000000000..60f2c2423 Binary files /dev/null and b/crates/engine/tests/fixtures/jpeg2000/checker-300ppi.jp2 differ diff --git a/crates/engine/tests/fixtures/jpeg2000/checker-raw.j2k b/crates/engine/tests/fixtures/jpeg2000/checker-raw.j2k new file mode 100644 index 000000000..0e2196656 Binary files /dev/null and b/crates/engine/tests/fixtures/jpeg2000/checker-raw.j2k differ diff --git a/crates/engine/tests/fixtures/jpeg2000/checker.jp2 b/crates/engine/tests/fixtures/jpeg2000/checker.jp2 new file mode 100644 index 000000000..787ca865d Binary files /dev/null and b/crates/engine/tests/fixtures/jpeg2000/checker.jp2 differ diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 12eaf13d6..10109e1ec 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -1781,6 +1781,31 @@ proptest! { // ---------- SVG saved with hidden layers and editing data ---------- +proptest! { + #![proptest_config(config())] + + /// Mutated JP2 boxes and raw codestream markers must return errors rather than unwind. + #[test] + fn mutated_jpeg2000_files_do_not_panic(raw in any::(), cut in 0usize..300, edits in prop::collection::vec((0usize..300, any::()), 0..8)) { + let (name, mut bytes) = if raw { + ("a.j2k", include_bytes!("fixtures/jpeg2000/checker-raw.j2k").to_vec()) + } else { + ("a.jp2", include_bytes!("fixtures/jpeg2000/checker.jp2").to_vec()) + }; + for (at, byte) in edits { + if let Some(slot) = bytes.get_mut(at) { *slot = byte; } + } + bytes.truncate(cut); + let result = catch_quiet(|| { + let mut s = vectorcraft_engine::Session::new(); + let params = json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes)}); + let _ = no_panic(s.execute("document.open", ¶ms)); + let _ = no_panic(s.execute("file.place", ¶ms)); + }); + prop_assert!(result.is_ok(), "{name}: panicked: {:?}", result.err()); + } +} + /// The rich document with a hidden layer, saved as SVG with its editing data. fn saved_svg() -> String { let mut d = rich_doc(); diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 311580ce5..d5f5e6dad 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -4862,6 +4862,8 @@ 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 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) @msg zipped 32-bit PSD image data with prediction can't be read zipovani 32-bitni PSD podaci slike s predviđanjem ne mogu se pročitati @msg {depth}-bit PSD files in this colour mode can't be read {depth}-bitne PSD datoteke u ovom načinu boja ne mogu se pročitati diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 9f8b0a55e..4f2d7c616 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6006,6 +6006,8 @@ breakText Glyphs Glifs Gap 4 Separació 4 Dash 5 Guió 5 Gap 5 Separació 5 +@msg the JPEG 2000 file can't be read no es pot llegir el fitxer JPEG 2000 +@msg the JPEG 2000 file has an unsupported color space or JPX content el fitxer JPEG 2000 té un espai de color o contingut JPX no compatible @msg You must choose a side pattern. Heu de triar un patró lateral. @msg invalid pattern tile choice opció de mosaic de patró no vàlida @msg no pattern named `{choice}` no hi ha cap patró anomenat `{choice}` diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 2a9aeaf99..ab09e818a 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4900,6 +4900,8 @@ 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 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ů) @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 240bb8684..80bd277fd 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4640,6 +4640,8 @@ 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 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) @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 a4057ea1d..bcea580dd 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4581,6 +4581,8 @@ 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 τα αρχεία PCX με εικονοστοιχεία {bits} bit σε {planes} επίπεδα δεν διαβάζονται +@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} εικονοστοιχεία) @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 2a77b567a..e9555f324 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4621,6 +4621,8 @@ 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 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) @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 44af5c9fb..58f8157ba 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4803,6 +4803,8 @@ 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 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ä) @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 abc63175f..818a363e6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4631,6 +4631,8 @@ 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 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) @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 db6960188..6bb3bf986 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5144,6 +5144,8 @@ 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 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) @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 618fce870..ce6b0d70a 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4661,6 +4661,8 @@ 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 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) @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 30005d3c3..69caf7f3a 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4703,6 +4703,8 @@ 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 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} ピクセル) @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 ff40a494d..134cc1808 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4340,6 +4340,8 @@ 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 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} პიქსელი) @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 d2f381afc..6a12ca363 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4631,6 +4631,8 @@ 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 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}픽셀) @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 092e5e944..c15322337 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4727,6 +4727,8 @@ 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 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) @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 d9a1a94f1..4fd8e2bc5 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4164,6 +4164,8 @@ 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 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) @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 d364faf0a..122bf5939 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4659,6 +4659,8 @@ 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 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} пикс.) @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 e01793d23..711f1eab4 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4538,6 +4538,8 @@ 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 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) @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 32bb794bc..5f69d1acb 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4381,6 +4381,8 @@ 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 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) @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 835a75efc..eb9d14016 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4645,6 +4645,8 @@ 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 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} пікс.) @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 6da79b33e..f6b926e07 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4801,6 +4801,8 @@ 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 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) @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 2912cf6d2..96b5fbe0d 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4417,6 +4417,8 @@ 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 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} 像素) @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 b814e2991..147c3b043 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2349,6 +2349,8 @@ 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 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} 像素) @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 c01f0005e..2d687384c 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 | 26 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, JPEG 2000, PICT, Pixar, Word `.doc`, PS | +| Formats opened or placed | ~30 | 27 | missing: FreeHand, CorelDRAW, DWG, HEIC/HEIF, 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 | @@ -105,8 +105,8 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri | PNG / JPEG / GIF / BMP / TIFF / Targa | open, place, export | yes (Targa: `.tga`, `.vda`, `.icb` and `.vst`, raw or run-length encoded, with a 16-bit file's one-bit alpha) | yes; TIFF is also selectable in Export for Screens and Asset Export with its existing options | Targa: `tests_tiffbmp.rs`, `tests/links.rs`; TIFF: grayscale with an alpha channel or other extra channels reads (#1466 in part), and so do files that store each channel as its own plane (PlanarConfiguration 2), CMYK ones with their ink amounts, and 1-bit files compressed with CCITT Group 3 (Compression 2 and 3, one- and two-dimensional rows); the ICC profiles embedded in JPEG and TIFF have 4-byte aligned tags (#1584); PNG export marks its colours as sRGB (`embedIcc`, an `sRGB` chunk; no ICC profile is embedded, #2221) | | 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 | | | 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 last valid `resc` or `resd` value on each axis, or 72 ppi; bounded decoding; `tests_jp2read.rs` | | PICT, Pixar PXR | open, place (legacy) | no | no | rarely used | | FreeHand `.fh*` | open (legacy) | no | no | | | CorelDRAW `.cdr` | open | no | no | | diff --git a/docs/mcp.md b/docs/mcp.md index 155755523..719ac6d40 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, 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. | +| `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, JPEG 2000 (`.jpf`/`.jpx`/`.jp2`/`.j2k`/`.j2c`/`.jpc`), 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. | @@ -3685,7 +3685,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 or PCX file, decoded within the pixel allowance the +`{name?, dataBase64}`: a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD, PSB, PCX 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 449729420..1ec5c221c 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 and PCX, so file managers list VectorCraft under Open With for them. +PSB, `.ps` PostScript, Targa, PCX 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 caa6dbfb3..6f0a426b2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -144,7 +144,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them | | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and Arrange | 9–14 | estimated; document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | -| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), guides lost (#779), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC/JPEG 2000 import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | +| File formats | 6 | 75% | `.ai` files open with a flattened or wrong layer structure in real cases (#951), guides lost (#779), no writing of Illustrator's editing data (#868), FreeHand/CorelDRAW/DWG/HEIC import, 3D export; see [file-format-parity.md](file-format-parity.md); EPS legacy mesh/opacity-mask and raster comparison follow-up verified | 10–20 | estimated (lowered from 85%: open issues); DXF control-point SPLINE import closure regression-tested (#1299 follow-up after #1320); PDF writer metadata, reader streaming and immutable backend checks in `crates/pdf/src/tests.rs`, `crates/pdf/src/tests/reader_streaming.rs` and `xtask/src/pdf_deps.rs`; PDF Place and clipboard paste retain live text (synthetic regressions); EPS ordinary Save retains the page frame and editable document (2026-10-26); `.ai`/EPS editing data checked against a user's files (2026-10-30): case setting (`ate::tests`, `tests_native::type_from_the_text_document_keeps_its_case_setting`), placed-file placements (`tests_native::a_small_placed_file_goes_where_it_draws_closer_to_the_page`), pages cropped to the art (`tests_native::type_on_the_copies_of_a_page_cropped_to_the_art_is_read_once`), mesh corner weights and state (`ai::mesh::tests::corners_with_handle_weights_of_zero_are_read`, `ai::tests::a_gradient_mesh_takes_the_transparency_written_after_it`), type on a path spacing (`tests_native::type_round_a_circle_is_spaced_as_the_file_lays_it_out`), blend state (`ai::tests::a_blends_transparency_follows_its_record`); scores and estimates unchanged. | | View & navigation | 3 | 70% | multiple windows and Arrange | 9–14 | estimated; document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged) | | View & navigation | 3 | 70% | multiple windows and Arrange | 9–14 | estimated | | Guides, grids, smart guides, snapping, rulers | 3 | 80% | "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658), distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved); new ruler-guide Alt orientation implemented; video rulers (View › Rulers › Show Video Rulers, #2849) done, score unchanged | @@ -536,7 +536,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 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. +- **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. - **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:** @@ -1059,6 +1059,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - 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`). + - JPEG 2000 JP2 and raw codestream images open and place from `.jpf`, `.jpx`, `.jp2`, `.j2k`, `.j2c` and `.jpc`. The last valid `resc` or `resd` resolution value wins on each axis; 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`). - 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`). @@ -1169,7 +1170,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 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. +- **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. - **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).