diff --git a/Cargo.lock b/Cargo.lock index 00fe4ca454..476af0dc89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6386,6 +6386,7 @@ dependencies = [ "pollster 1.0.1", "raw-window-handle", "rfd", + "rustix 1.1.6", "serde_json", "socket2", "tempfile", diff --git a/apps/vectorcraft/Cargo.toml b/apps/vectorcraft/Cargo.toml index 2da5f0083d..9a1b11e350 100644 --- a/apps/vectorcraft/Cargo.toml +++ b/apps/vectorcraft/Cargo.toml @@ -110,6 +110,10 @@ wayland-protocols = { version = "0.32", features = ["client", "unstable"] } # wayland-protocols' `unstable` set) made on the window's own connection. MIT, already in the build # through winit and rfd. wayland-backend = { version = "0.3", features = ["client_system"] } +# The Wayland clipboard's pipes (src/clipboard.rs): `poll` and the nonblocking flag give up a read +# or send when the other app moves no data for a while. Apache-2.0 WITH LLVM-exception OR +# Apache-2.0 OR MIT, already in the build through the workspace. +rustix = { workspace = true, features = ["std", "event", "fs"] } # The save dialog's file type (src/portal_save.rs, #2679): the desktop portal over D-Bus, on the # systems where rfd shows the portal's dialog. MIT, already in the build through accesskit. Pinned diff --git a/apps/vectorcraft/src/clipboard.rs b/apps/vectorcraft/src/clipboard.rs index 77a96442c4..17b9327956 100644 --- a/apps/vectorcraft/src/clipboard.rs +++ b/apps/vectorcraft/src/clipboard.rs @@ -1790,15 +1790,17 @@ mod portable { /// starts a thread for each paste of a Copy's data by another app ([`MAX_SENDS`] at once). Reads /// run on another worker and take [`MAX_BYTES`] at most; text waits [`LOAD_WAIT`] at most, SVG and /// PNG as long as a paste's bitmap from arboard. A read that gets no answer in its wait is kept for -/// a paste to wait for off the UI thread, as arboard's are. +/// a paste to wait for off the UI thread, as arboard's are. A read or send whose pipe moves no data +/// for [`IDLE_WAIT`] fails and closes the pipe, which frees its worker or thread. #[cfg(target_os = "linux")] mod wayland { - use std::io::{Read, Write}; - use std::os::fd::AsFd as _; + use std::io::{ErrorKind, Read, Write}; + use std::os::fd::{AsFd, BorrowedFd}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError, mpsc}; use std::time::{Duration, Instant}; + use rustix::event::{PollFd, PollFlags, Timespec}; use vectorcraft_engine::cmd::clipboard::names_a_picture; use vectorcraft_ui_egui::ClipboardRead; use wayland_client::backend::ObjectData; @@ -1827,6 +1829,11 @@ mod wayland { const MAX_SENDS: usize = 8; /// The most types of an offer that are kept (untrusted data). const MAX_TYPES: usize = 256; + /// How long a read or send waits for the other app to move a byte through the pipe before it + /// fails, VectorCraft's own limit; the wait starts again after each byte moved. + const IDLE_WAIT: Duration = LOAD_WAIT; + /// The most bytes a read takes from the pipe at a time. + const CHUNK: usize = 64 << 10; /// What a read gives: the bytes, or why there are none. type Got = Result, String>; @@ -1898,37 +1905,117 @@ mod wayland { Some(first) } - /// `from` read to its end, `cap` bytes at most: more is an error, as is a failed read. - fn read_capped(from: impl Read, cap: usize) -> Got { - let mut data = vec![]; - from.take(cap as u64 + 1).read_to_end(&mut data).map_err(|e| format!("couldn't read the clipboard's data: {e}"))?; - if data.len() > cap { - return Err(format!("the clipboard's data is larger than {} MiB", cap >> 20)); + /// The time `idle` from now; None when that is past the clock's range (no limit). + fn idle_until(idle: Duration) -> Option { + Instant::now().checked_add(idle) + } + + /// Makes `fd` nonblocking, its other flags kept. + fn nonblocking(fd: BorrowedFd<'_>) -> std::io::Result<()> { + let flags = rustix::fs::fcntl_getfl(fd)?; + rustix::fs::fcntl_setfl(fd, flags | rustix::fs::OFlags::NONBLOCK)?; + Ok(()) + } + + /// Waits until `fd` is ready for `events`, closed or failed (true), or `until` passes (false); + /// None waits without a limit. A wait a signal cuts short goes on until `until`. + fn ready(fd: BorrowedFd<'_>, events: PollFlags, until: Option) -> std::io::Result { + loop { + let left = until.map(|t| t.saturating_duration_since(Instant::now())); + let timeout = left.and_then(|d| Timespec::try_from(d).ok()); + let mut fds = [PollFd::new(&fd, events)]; + match rustix::event::poll(&mut fds, timeout.as_ref()) { + Ok(n) => return Ok(n > 0), + Err(rustix::io::Errno::INTR) => {} + Err(e) => return Err(e.into()), + } } - Ok(data) + } + + /// `from` read to its end, `cap` bytes at most: more is an error, as is a failed read and a + /// pipe that is left open with no byte coming for `idle`. A read that fails gives none of the + /// bytes it took, and closes `from`. + fn read_capped(mut from: impl Read + AsFd, cap: usize, idle: Duration) -> Got { + let failed = |e: std::io::Error| format!("couldn't read the clipboard's data: {e}"); + nonblocking(from.as_fd()).map_err(failed)?; + let (mut data, mut chunk) = (vec![], vec![0u8; CHUNK]); + let mut until = idle_until(idle); + loop { + match from.read(&mut chunk) { + Ok(0) => return Ok(data), + Ok(n) => { + let got = chunk.get(..n).ok_or_else(|| failed(ErrorKind::InvalidData.into()))?; + if data.len() + got.len() > cap { + return Err(format!("the clipboard's data is larger than {} MiB", cap >> 20)); + } + data.extend_from_slice(got); + until = idle_until(idle); + } + Err(e) if e.kind() == ErrorKind::Interrupted => {} + Err(e) if e.kind() == ErrorKind::WouldBlock => { + if !ready(from.as_fd(), PollFlags::IN, until).map_err(failed)? { + return Err("the app that copied stopped sending the clipboard's data".to_string()); + } + } + Err(e) => return Err(failed(e)), + } + } + } + + /// `data` written to `to`: an error when the app that pastes closes its end (the process + /// ignores `SIGPIPE`) or takes no byte for `idle`. + fn send(mut to: impl Write + AsFd, data: &[u8], idle: Duration) -> std::io::Result<()> { + nonblocking(to.as_fd())?; + let (mut sent, mut until) = (0, idle_until(idle)); + while let Some(rest) = data.get(sent..).filter(|rest| !rest.is_empty()) { + match to.write(rest) { + Ok(0) => return Err(ErrorKind::WriteZero.into()), + Ok(n) => { + sent += n; + until = idle_until(idle); + } + Err(e) if e.kind() == ErrorKind::Interrupted => {} + Err(e) if e.kind() == ErrorKind::WouldBlock => { + if !ready(to.as_fd(), PollFlags::OUT, until)? { + return Err(ErrorKind::TimedOut.into()); + } + } + Err(e) => return Err(e), + } + } + Ok(()) } /// The threads that send a Copy's data to the apps that paste it, counted. - #[derive(Clone, Default)] - struct Sends(Arc); + #[derive(Clone)] + struct Sends { + running: Arc, + /// How long a send waits for the app that pastes to take a byte. + idle: Duration, + } + + impl Default for Sends { + fn default() -> Self { + Self { running: Arc::default(), idle: IDLE_WAIT } + } + } impl Sends { - /// `data` written to `to` on a thread of its own, which then closes `to`; an app that - /// closes its end early ends the write (the process ignores `SIGPIPE`). While - /// [`MAX_SENDS`] are being written `to` is closed at once with nothing written → false. - fn start(&self, mut to: impl Write + Send + 'static, data: Arc<[u8]>) -> bool { - if self.0.fetch_add(1, Ordering::SeqCst) >= MAX_SENDS { - self.0.fetch_sub(1, Ordering::SeqCst); + /// `data` written to `to` on a thread of its own ([`send`]), which then closes `to`; an app + /// that closes its end early or takes no byte for the send's idle wait ends the write. + /// While [`MAX_SENDS`] are being written `to` is closed at once with nothing written → false. + fn start(&self, to: impl Write + AsFd + Send + 'static, data: Arc<[u8]>) -> bool { + if self.running.fetch_add(1, Ordering::SeqCst) >= MAX_SENDS { + self.running.fetch_sub(1, Ordering::SeqCst); return false; } - let count = self.0.clone(); - let send = move || { - let _ = to.write_all(&data); - drop(to); + let (count, idle) = (self.running.clone(), self.idle); + let work = move || { + let _ = send(to, &data, idle); count.fetch_sub(1, Ordering::SeqCst); }; - if std::thread::Builder::new().name("wayland-clipboard-send".into()).spawn(send).is_err() { - self.0.fetch_sub(1, Ordering::SeqCst); + if std::thread::Builder::new().name("wayland-clipboard-send".into()).spawn(work).is_err() { + self.running.fetch_sub(1, Ordering::SeqCst); return false; } true @@ -1937,7 +2024,7 @@ mod wayland { /// How many are being written. #[cfg(test)] fn running(&self) -> usize { - self.0.load(Ordering::SeqCst) + self.running.load(Ordering::SeqCst) } } @@ -2298,7 +2385,7 @@ mod wayland { impl Receiver { /// The clipboard's data of type `wire`, read until the app that copied closes the pipe, - /// [`MAX_BYTES`] at most. + /// [`MAX_BYTES`] at most; an error when that app sends no byte for [`IDLE_WAIT`]. fn receive(&self, wire: String) -> Got { let offer = lock(&self.objects).selection.clone().ok_or("the clipboard is empty")?; let (from, to) = std::io::pipe().map_err(|e| format!("couldn't open a pipe for the clipboard's data: {e}"))?; @@ -2307,7 +2394,7 @@ mod wayland { // holds the only one, and the read ends when that app closes it. drop(to); self.conn.flush().map_err(|e| format!("couldn't reach the compositor: {e}"))?; - read_capped(from, MAX_BYTES) + read_capped(from, MAX_BYTES, IDLE_WAIT) } } @@ -2557,31 +2644,86 @@ mod wayland { assert_eq!(keyboard_serial(&modifiers(9), None), None); } + /// A wait no test reaches: the other end of each pipe below either moves data or closes. + const LONG: Duration = Duration::from_secs(600); + + /// A pipe holding `data`, its write end closed when `close`. + fn pipe_with(data: &[u8], close: bool) -> (std::io::PipeReader, Option) { + let (from, mut to) = std::io::pipe().unwrap(); + to.write_all(data).unwrap(); + (from, (!close).then_some(to)) + } + #[test] fn a_read_stops_at_its_limit() { assert_eq!(MAX_BYTES, 64 * 1024 * 1024); - assert_eq!(read_capped(&b"12345678"[..], 8), Ok(b"12345678".to_vec())); - assert_eq!(read_capped(&b""[..], 8), Ok(vec![])); - assert_eq!(read_capped(&[0u8; 9][..], 8), Err("the clipboard's data is larger than 0 MiB".to_string())); - assert_eq!(read_capped(&vec![0u8; (2 << 20) + 1][..], 2 << 20), Err("the clipboard's data is larger than 2 MiB".to_string())); - /// A pipe that breaks. - struct Broken; - impl Read for Broken { - fn read(&mut self, _: &mut [u8]) -> std::io::Result { - Err(std::io::ErrorKind::BrokenPipe.into()) - } + assert_eq!(IDLE_WAIT, LOAD_WAIT); + assert_eq!(read_capped(pipe_with(b"12345678", true).0, 8, Duration::ZERO), Ok(b"12345678".to_vec())); + assert_eq!(read_capped(pipe_with(b"", true).0, 8, Duration::ZERO), Ok(vec![])); + assert_eq!(read_capped(pipe_with(&[0u8; 9], true).0, 8, Duration::ZERO), Err("the clipboard's data is larger than 0 MiB".to_string())); + // More than a pipe holds, from an app that is still sending. + let (from, to) = std::io::pipe().unwrap(); + assert!(Sends::default().start(to, vec![0u8; (2 << 20) + 1].into())); + assert_eq!(read_capped(from, 2 << 20, LONG), Err("the clipboard's data is larger than 2 MiB".to_string())); + // A pipe that can't be read: the write end. + let (_, to) = std::io::pipe().unwrap(); + let unreadable = std::fs::File::from(std::os::fd::OwnedFd::from(to)); + assert!(read_capped(unreadable, 8, LONG).is_err_and(|e| e.starts_with("couldn't read the clipboard's data"))); + } + + #[test] + fn a_read_from_an_app_that_stops_sending_fails_and_closes_the_pipe() { + let stopped = Err("the app that copied stopped sending the clipboard's data".to_string()); + // Some bytes and then nothing, the pipe left open: none of them are pasted. + let (from, to) = pipe_with(b"part", false); + assert_eq!(read_capped(from, MAX_BYTES, Duration::ZERO), stopped); + // The read end is closed, so that app's next write fails. + let mut to = to.unwrap(); + assert_eq!(to.write(b"x").map_err(|e| e.kind()), Err(ErrorKind::BrokenPipe)); + // Nothing at all, and data that fills the limit exactly with more never coming. + let (from, _held) = pipe_with(b"", false); + assert_eq!(read_capped(from, MAX_BYTES, Duration::ZERO), stopped); + let (from, _held) = pipe_with(b"12345678", false); + assert_eq!(read_capped(from, 8, Duration::ZERO), stopped); + // The next read gets its data. + assert_eq!(read_capped(pipe_with(b"words", true).0, MAX_BYTES, Duration::ZERO), Ok(b"words".to_vec())); + } + + #[test] + fn a_send_to_an_app_that_stops_reading_fails_and_frees_its_thread() { + let data: Arc<[u8]> = vec![3u8; 300_000].into(); + // More than a pipe holds, to an app that never reads. + let (_held, to) = std::io::pipe().unwrap(); + assert_eq!(send(to, &data, Duration::ZERO).map_err(|e| e.kind()), Err(ErrorKind::TimedOut)); + // An app that closes its end before reading. + let (from, to) = std::io::pipe().unwrap(); + drop(from); + assert_eq!(send(to, &data, LONG).map_err(|e| e.kind()), Err(ErrorKind::BrokenPipe)); + // A send thread to an app that does not read until the send gives up, its end still open. + let sends = Sends { idle: Duration::ZERO, ..Sends::default() }; + let (from, to) = std::io::pipe().unwrap(); + assert!(sends.start(to, data.clone())); + while sends.running() > 0 { + std::thread::yield_now(); } - assert!(read_capped(Broken, 8).is_err_and(|e| e.starts_with("couldn't read the clipboard's data"))); + // What the pipe held when the send gave up, then the end of the data: the send closed it. + let got = read_capped(from, MAX_BYTES, Duration::ZERO).map(|d| d.len()); + assert!(got.as_ref().is_ok_and(|&n| n > 0 && n < data.len()), "{got:?}"); + // The thread is free for the next paste. + let (from, to) = std::io::pipe().unwrap(); + assert!(sends.start(to, b"words"[..].into())); + assert_eq!(read_capped(from, MAX_BYTES, LONG), Ok(b"words".to_vec())); } #[test] fn a_copys_data_is_sent_through_the_pipe_and_closed() { let sends = Sends::default(); + assert_eq!(sends.idle, IDLE_WAIT); let (from, to) = std::io::pipe().unwrap(); let data: Arc<[u8]> = vec![7u8; 300_000].into(); assert!(sends.start(to, data.clone())); // More than a pipe holds: the write ends once all of it is read, then the pipe closes. - assert_eq!(read_capped(from, MAX_BYTES).as_deref(), Ok(&data[..])); + assert_eq!(read_capped(from, MAX_BYTES, LONG).as_deref(), Ok(&data[..])); // An app that closes its end before reading ends the send. let (from, to) = std::io::pipe().unwrap(); drop(from); @@ -2593,9 +2735,9 @@ mod wayland { #[test] fn pastes_that_read_nothing_hold_a_few_sends_at_most() { - let sends = Sends::default(); + let sends = Sends { idle: LONG, ..Sends::default() }; let data: Arc<[u8]> = vec![1u8; 300_000].into(); - // Apps that ask for the data and never read it. + // Apps that ask for the data and never read it, within the sends' idle wait. let stalled: Vec<_> = (0..MAX_SENDS) .map(|_| { let (from, to) = std::io::pipe().unwrap(); @@ -2606,7 +2748,7 @@ mod wayland { // The next paste gets its pipe closed at once, with nothing written. let (from, to) = std::io::pipe().unwrap(); assert!(!sends.start(to, data.clone())); - assert_eq!(read_capped(from, MAX_BYTES), Ok(vec![])); + assert_eq!(read_capped(from, MAX_BYTES, Duration::ZERO), Ok(vec![])); // Once those apps close their pipes, the sends end and others are sent again. drop(stalled); while sends.running() > 0 { @@ -2614,7 +2756,7 @@ mod wayland { } let (from, to) = std::io::pipe().unwrap(); assert!(sends.start(to, data.clone())); - assert_eq!(read_capped(from, MAX_BYTES).map(|d| d.len()), Ok(data.len())); + assert_eq!(read_capped(from, MAX_BYTES, LONG).map(|d| d.len()), Ok(data.len())); } /// What a clipboard holds, by type. diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 90a0d85ec8..2cffad6581 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -29,7 +29,7 @@ use crate::group::{has_own_paint, paints}; use crate::reshape::{as_art, geometry_effects, glyph_outlines, outline_nested_text, reshape_leaves}; use crate::{ GeomContext, adjust_in_document, apply_geometry_carrying, crop_marks_art, evaluate_container, has_adjustment, has_crop_marks, has_geometry, - has_item_geometry, has_pathfinder, is_pathfinder, merged_params, needs_outline, outline_text, reshape, + has_instance_row_raster, has_item_geometry, has_pathfinder, is_pathfinder, merged_params, needs_outline, outline_text, reshape, }; fn item_effects(item: &AppearanceItem) -> &[vectorcraft_doc::Effect] { @@ -58,6 +58,7 @@ pub fn needs_bake(n: &Node) -> bool { || has_geometry(&n.appearance.effects) || n.projection().is_some() || n.appearance.items.iter().any(|i| has_geometry(item_effects(i))) + || has_instance_row_raster(n) || n.children().is_some_and(|ch| ch.iter().any(|c| needs_bake(c))) } @@ -142,9 +143,11 @@ fn bake_node(d: &mut Document, n: &Node) -> Option { } // Type, images, symbol instances and live objects: reshaped through their outlines, and so are // type whose own fills or strokes have geometry effects and images whose own fills or strokes - // paint a shape ([`has_item_geometry`]). Type that Transform effects only move and copy stays - // type, moved to each place. - if needs_outline(n) && (has_geometry(&n.appearance.effects) || n.projection().is_some() || has_item_geometry(n)) { + // paint a shape ([`has_item_geometry`]), and symbol instances whose active fill or stroke has a + // raster effect, which the fills and strokes of their art then carry + // ([`has_instance_row_raster`]). Type that Transform effects only move and copy stays type, + // moved to each place. + if needs_outline(n) && (has_geometry(&n.appearance.effects) || n.projection().is_some() || has_item_geometry(n) || has_instance_row_raster(n)) { if let Some(m) = moved_type_group(n) { return Some(bake_pieces(d, m)); } diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 6316a7a273..c18a31d842 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -80,8 +80,9 @@ pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks}; pub use pixel::{PIXEL_EFFECTS, PixelFx, PixelSpace}; pub use raster::{RASTERIZE, RasterFx, Rasterize, color_param, follow_items, item_effects_in, outset, raster_effects, without_geometry}; pub use reshape::{ - SYMBOL_ART_MAX_NODES, expand_outlined, follows_image_pixels, gives_image_shape, has_gradient_copies, has_item_geometry, has_painted_effects, - has_transform_art, has_transform_copies, has_transform_options, image_paint, needs_outline, outline_art, outline_text, reshape, + SYMBOL_ART_MAX_NODES, expand_outlined, follows_image_pixels, gives_image_shape, has_gradient_copies, has_instance_row_raster, has_item_geometry, + has_painted_effects, has_transform_art, has_transform_copies, has_transform_options, image_paint, needs_outline, outline_art, outline_text, + reshape, }; pub use revolve::{ CLASSIC_REVOLVE, REVOLVE, has_revolve, is_revolve, revolve_art, revolve_art_in, revolve_options, validate_revolve, validate_revolve_in, diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index 8c829d9e0b..83ff68431f 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -162,15 +162,26 @@ fn active_stroke(n: &Node) -> Option<&vectorcraft_doc::StrokeLayer> { n.appearance.stroke().filter(|s| s.visible && s.opacity > 0.0) } -/// The visible geometry effects in `fx`, in order. -fn geometry_of(fx: &[Effect]) -> Vec { - fx.iter().filter(|e| e.visible && is_geometry(&e.id)).cloned().collect() +/// Is `n` a symbol instance whose active fill or stroke has a visible raster effect (a shadow, glow, +/// blur, feather or other painted effect, [`crate::is_raster`])? Its art takes that effect on each +/// fill or stroke in it that paints ([`add_instance_row_effects`]), and draws it there. +pub fn has_instance_row_raster(n: &Node) -> bool { + let raster = |fx: &[Effect]| fx.iter().any(|e| e.visible && crate::is_raster(&e.id)); + matches!(n.kind, NodeKind::SymbolInstance { .. }) + && (active_fill(n).is_some_and(|f| raster(&f.effects)) || active_stroke(n).is_some_and(|s| raster(&s.effects))) } -/// `art`, the art symbol instance `n` places, with the visible geometry effects of `n`'s active -/// fill added after the effects of every fill in it that paints, and those of its active stroke -/// after the effects of every stroke in it that paints ([`active_fill`], [`active_stroke`]). Each -/// fill or stroke keeps its paint, and the others keep their shapes. Live objects in it that are +/// The visible geometry and raster effects in `fx`, in order: those a symbol instance's active fill +/// or stroke passes on to its art. +fn row_effects_of(fx: &[Effect]) -> Vec { + fx.iter().filter(|e| e.visible && (is_geometry(&e.id) || crate::is_raster(&e.id))).cloned().collect() +} + +/// `art`, the art symbol instance `n` places, with the visible geometry and raster effects of +/// `n`'s active fill added after the effects of every fill in it that paints, and those of its +/// active stroke after the effects of every stroke in it that paints ([`active_fill`], +/// [`active_stroke`]): a Drop Shadow on the instance's fill shadows each filled shape in the art. +/// Each fill or stroke keeps its paint, and the others keep their shapes. Live objects in it that are /// reached are evaluated first (with the symbols of `doc`), so the effects reshape what they draw. /// Clip groups' clipping paths, guides and the art of opacity masks keep their shapes. A symbol /// instance in the art takes the effects on its own active fill or stroke, or on a new fill or @@ -180,8 +191,8 @@ fn geometry_of(fx: &[Effect]) -> Vec { /// instance in the art takes them. fn add_instance_row_effects(doc: Option<&Document>, n: &Node, art: &mut Node) { let NodeKind::SymbolInstance { symbol, .. } = &n.kind else { return }; - let fill = active_fill(n).map_or(vec![], |f| geometry_of(&f.effects)); - let stroke = active_stroke(n).map_or(vec![], |s| geometry_of(&s.effects)); + let fill = active_fill(n).map_or(vec![], |f| row_effects_of(&f.effects)); + let stroke = active_stroke(n).map_or(vec![], |s| row_effects_of(&s.effects)); if !fill.is_empty() || !stroke.is_empty() { add_row_effects(doc, symbol, art, &fill, &stroke); } @@ -526,8 +537,8 @@ fn with_id(art: &Node, id: NodeId) -> Node { /// a symbol instance as `symbol_art` (the symbol's art, stained as the caller draws it) placed by /// the instance's transform (through the symbol's 9-slice guides in `doc`, when it has them), as /// the art its members draw when the instance has geometry effects of its own -/// ([`crate::group::members_as_drawn`]), its fills and strokes carrying the geometry effects of the -/// instance's active fill and stroke ([`add_instance_row_effects`]). +/// ([`crate::group::members_as_drawn`]), its fills and strokes carrying the geometry and raster +/// effects of the instance's active fill and stroke ([`add_instance_row_effects`]). /// `None` for other kinds (images, paths, containers), and for an instance whose art placed /// through the guides could hold more than [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects. A Repeat counts the Repeats in the art of /// `doc`'s symbols that its instances show toward its instance budget. @@ -709,12 +720,14 @@ pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping /// reshaped with it, and keep their effects from their first Convert to Shape or Outline Object /// on. The result is a group with `n`'s id, name, visibility, transparency, opacity mask /// and remaining (raster) effects; `None` when `n` has no visible geometry effect, no perspective -/// and is not type or an image with a fill or stroke of its own that [`has_item_geometry`] counts, -/// or has no art (a symbol instance without `symbol_art`). A path with [`has_transform_art`] is -/// always reshaped. The fills and strokes in the art whose own Transform effects reach their -/// appearance become objects of their own ([`transform_items`]). +/// and is not type or an image with a fill or stroke of its own that [`has_item_geometry`] counts +/// or a symbol instance whose active fill or stroke has a raster effect +/// ([`has_instance_row_raster`]), or has no art (a symbol instance without `symbol_art`). A path +/// with [`has_transform_art`] is always reshaped. The fills and strokes in the art whose own +/// Transform effects reach their appearance become objects of their own ([`transform_items`]). pub fn reshape(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { - if !has_transform_art(n) && ((geometry_effects(n).is_empty() && n.projection().is_none() && !has_item_geometry(n)) || !needs_outline(n)) { + let own = !geometry_effects(n).is_empty() || n.projection().is_some() || has_item_geometry(n) || has_instance_row_raster(n); + if !has_transform_art(n) && (!own || !needs_outline(n)) { return None; } as_art(doc, n, symbol_art) diff --git a/crates/engine/src/cmd/addons/shapes.rs b/crates/engine/src/cmd/addons/shapes.rs index 87549e3694..7fc935546e 100644 --- a/crates/engine/src/cmd/addons/shapes.rs +++ b/crates/engine/src/cmd/addons/shapes.rs @@ -9,6 +9,8 @@ #[cfg(test)] mod tests; +use std::ops::RangeInclusive; + use serde_json::{Value, json}; use vectorcraft_doc::{LiveShape, NodeId, NodeKind}; use vectorcraft_geom::{Affine, FillRule, Point, Rect, shapes}; @@ -16,9 +18,25 @@ use vectorcraft_tools::addons::box3d::CustomShape; use super::super::create::insert_look; use super::super::{ - CommandSpec, Result, Session, bool_or, check_numbers, check_switches, cmd, f64_or, f64_req, has_doc, has_selection, str_param, targets, + CommandSpec, Result, Session, bad, bool_or, check_numbers, check_switches, cmd, f64_or, f64_req, has_doc, has_selection, number_or, str_param, + targets, }; +/// The sides of a polygon and the points of a star: whole numbers in this range. +const COUNTS: RangeInclusive = 3.0..=1000.0; +/// `rounded` and `randomized` of a polygon or star. +const ROUNDING: RangeInclusive = -10.0..=10.0; +/// A star's inner radius as a fraction of its outer radius. +const SPOKE_RATIO: RangeInclusive = 0.01..=1.0; +/// A spiral's `turns`. +const TURNS: RangeInclusive = 0.01..=1024.0; +/// A spiral's `divergence`. +const DIVERGENCE: RangeInclusive = 0.0..=1000.0; +/// A spiral's `innerRadius`, as a fraction of its radius. +const INNER_RADIUS: RangeInclusive = 0.0..=0.999; +/// The `arcMode` names; `slice` and `open` are also read as `pie` and `arc`. +const ARC_MODES: [&str; 4] = ["whole", "pie", "chord", "arc"]; + pub fn specs() -> Vec { vec![ cmd!( @@ -26,7 +44,7 @@ pub fn specs() -> Vec { "Rectangle", [], None, - "{x, y, width, height, radius?: pt, radii?: [tl,tr,br,bl], startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top-left corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", + "{x, y, width, height: pt (0 or more), radius?: pt (0 or more), radii?: [tl,tr,br,bl] (each 0 or more), startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top-left corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", has_doc, rectangle ), @@ -35,7 +53,7 @@ pub fn specs() -> Vec { "Ellipse", [], None, - "{x, y, width, height, startAngle?, endAngle?: degrees (counter-clockwise from 3 o'clock, as object.setLiveShape's pieStart and pieEnd; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", + "{x, y, width, height: pt (0 or more), startAngle?, endAngle?: degrees (counter-clockwise from 3 o'clock, as object.setLiveShape's pieStart and pieEnd; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise about the centre (0: upright)} → {id}", has_doc, ellipse ), @@ -44,7 +62,7 @@ pub fn specs() -> Vec { "Polygon", [], None, - "{cx, cy, radius, sides=6, rounded=0, randomized=0, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a corner straight up)} → {id}", + "{cx, cy, radius: pt (0 or more), sides=6: a whole number 3 to 1000, rounded=0, randomized=0: -10 to 10, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top corner; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a corner straight up)} → {id}", has_doc, polygon ), @@ -53,7 +71,7 @@ pub fn specs() -> Vec { "Star", [], None, - "{cx, cy, radius1, radius2?, points=5, rounded=0, randomized=0, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top point; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a point straight up)} → {id}", + "{cx, cy, radius1: pt (0 or more), radius2?: pt (0.01 to 1 times radius1; default half of it), points=5: a whole number 3 to 1000, rounded=0, randomized=0: -10 to 10, startAngle?, endAngle?: degrees (the part of the outline kept, clockwise from its first anchor, the top point; each side an equal share of 360; default 0 and 360), arcMode?: whole|pie|chord|arc, rotation?: deg clockwise (0: a point straight up)} → {id}", has_doc, star ), @@ -62,7 +80,7 @@ pub fn specs() -> Vec { "Spiral", [], None, - "{cx, cy, radius, turns=3, divergence=1, innerRadius=0, clockwise=true} → {id}", + "{cx, cy, radius: pt (0 or more), turns=3: 0.01 to 1024, divergence=1: 0 to 1000, innerRadius=0: 0 to 0.999 of radius, clockwise=true} → {id}", has_doc, spiral ), @@ -71,7 +89,7 @@ pub fn specs() -> Vec { "3D Box", [], None, - "{x, y, width, height, angleX=30 (degrees about the level axis: positive tips the top away from you), angleY=30 (degrees about the upright axis: positive turns the right side away from you), angleZ=0 (degrees clockwise in the page), parallelX=false, parallelY=true, parallelZ=false} → {id}", + "{x, y, width, height: pt (0 or more), angleX=30 (degrees about the level axis: positive tips the top away from you), angleY=30 (degrees about the upright axis: positive turns the right side away from you), angleZ=0 (degrees clockwise in the page), parallelX=false, parallelY=true, parallelZ=false} → {id}", has_doc, box3d ), @@ -80,33 +98,96 @@ pub fn specs() -> Vec { "Configure Shape", [], None, - "{ids?, radius?, radii?: [tl,tr,br,bl], startAngle?, endAngle?: degrees (as addon.shapes.rectangle, ellipse, polygon and star take them: an ellipse's counter-clockwise from 3 o'clock, the others' clockwise along the outline), arcMode?: whole|pie|chord|arc, sides?, isStar?, spokeRatio?, rounded?, randomized?, turns?, divergence?, innerRadius?, angleX?, angleY?: degrees (as addon.shapes.box3d: positive tips the top, or turns the right side, away from you), angleZ?: degrees clockwise in the page, parallelX?, parallelY?, parallelZ?, reset?: bool} → {updated}", + "{ids?, radius?: pt (0 or more; fitted to half each rectangle's shorter side), radii?: [tl,tr,br,bl] (each 0 or more), startAngle?, endAngle?: degrees (as addon.shapes.rectangle, ellipse, polygon and star take them: an ellipse's counter-clockwise from 3 o'clock, the others' clockwise along the outline), arcMode?: whole|pie|chord|arc, sides?: a whole number 3 to 1000, isStar?, spokeRatio?: 0.01 to 1, rounded?, randomized?: -10 to 10, turns?: 0.01 to 1024, divergence?: 0 to 1000, innerRadius?: 0 to 0.999, angleX?, angleY?: degrees (as addon.shapes.box3d: positive tips the top, or turns the right side, away from you), angleZ?: degrees clockwise in the page, parallelX?, parallelY?, parallelZ?, reset?: bool} an omitted or null value keeps each shape's setting → {updated}", has_selection, configure ), ] } -fn parse_arc_params(p: &Value) -> ((f64, f64), shapes::EllipseMode) { +/// The number `p` gives for `key`, or `default` when `p` leaves it out or gives null. A number +/// outside `range` is an error that names `key` and the range. +fn ranged(p: &Value, key: &str, default: f64, range: &RangeInclusive, cmd: &str) -> Result { + let x = number_or(p, key, default, cmd)?; + if !range.contains(&x) { + let (min, max) = (range.start(), range.end()); + return Err(bad(cmd, format!("`{key}` must be between {min} and {max} (got {x})"))); + } + Ok(x) +} + +/// The number of sides or points `p` gives for `key`, or `default` when `p` leaves it out or gives +/// null. A fraction or a number outside [`COUNTS`] is an error. +fn count(p: &Value, key: &str, default: u32, cmd: &str) -> Result { + let x = number_or(p, key, f64::from(default), cmd)?; + if x.fract() != 0.0 || !COUNTS.contains(&x) { + let (min, max) = (COUNTS.start(), COUNTS.end()); + return Err(bad(cmd, format!("`{key}` must be a whole number between {min} and {max} (got {x})"))); + } + Ok(x as u32) +} + +/// The size `p` gives for `key`: a number that is not negative. +fn size(p: &Value, key: &str, cmd: &str) -> Result { + not_negative(f64_req(p, key, cmd)?, key, cmd) +} + +/// The size `p` gives for `key`, or `default` when `p` leaves it out or gives null. +fn size_or(p: &Value, key: &str, default: f64, cmd: &str) -> Result { + not_negative(number_or(p, key, default, cmd)?, key, cmd) +} + +fn not_negative(x: f64, key: &str, cmd: &str) -> Result { + if x < 0.0 { + return Err(bad(cmd, format!("`{key}` must be a number not below 0"))); + } + Ok(x) +} + +/// The four corner radii `p` gives as `radii` ([tl, tr, br, bl]), or `None` when `p` leaves it out +/// or gives null. Anything but four numbers that are not negative is an error. +fn radii_param(p: &Value, cmd: &str) -> Result> { + let key = "radii"; + match p.get(key) { + None | Some(Value::Null) => Ok(None), + Some(v) => { + let four = v.as_array().filter(|a| a.len() == 4).and_then(|a| { + let mut out = [0.0; 4]; + for (slot, item) in out.iter_mut().zip(a) { + *slot = item.as_f64().filter(|r| *r >= 0.0)?; + } + Some(out) + }); + four.map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be four numbers not below 0: [tl, tr, br, bl]"))) + } + } +} + +/// The mode `p` gives as `arcMode`, or `None` when `p` leaves it out or gives null. Any other +/// value than one of [`ARC_MODES`] (or `slice` or `open`) is an error. +fn arc_mode(p: &Value, cmd: &str) -> Result> { + let key = "arcMode"; + match p.get(key) { + None | Some(Value::Null) => Ok(None), + Some(v) => v.as_str().and_then(parse_mode).map(Some).ok_or_else(|| bad(cmd, format!("`{key}` must be one of {}", ARC_MODES.join(", ")))), + } +} + +fn parse_arc_params(p: &Value, cmd: &str) -> Result<((f64, f64), shapes::EllipseMode)> { let start = f64_or(p, "startAngle", 0.0); let end = f64_or(p, "endAngle", 360.0); - let mode = match str_param(p, "arcMode") { - Some("pie" | "slice") => shapes::EllipseMode::Slice, - Some("chord") => shapes::EllipseMode::Chord, - Some("arc" | "open") => shapes::EllipseMode::Arc, - _ => shapes::EllipseMode::Whole, - }; + let mode = arc_mode(p, cmd)?.unwrap_or(shapes::EllipseMode::Whole); let span = (end - start).rem_euclid(360.0); let pie = if mode == shapes::EllipseMode::Whole && (span < 1e-6 || (span - 360.0).abs() < 1e-6) { (0.0, 360.0) } else { (start, end) }; - (pie, mode) + Ok((pie, mode)) } fn rect_of(p: &Value, c: &str) -> Result { let x = f64_req(p, "x", c)?; let y = f64_req(p, "y", c)?; - let w = f64_req(p, "width", c)?; - let h = f64_req(p, "height", c)?; - Ok(Rect::new(x, y, x + w.abs(), y + h.abs()).abs()) + let w = size(p, "width", c)?; + let h = size(p, "height", c)?; + Ok(Rect::new(x, y, x + w, y + h)) } fn rect_xf(r: Rect, rot: f64) -> Affine { @@ -141,18 +222,9 @@ fn rectangle(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.rectangle"; check_numbers(p, &["radius", "startAngle", "endAngle", "rotation"], c)?; let r = rect_of(p, c)?; - let radius = f64_or(p, "radius", 0.0).max(0.0); - let mut radii = [radius; 4]; - if let Some(arr) = p.get("radii").and_then(Value::as_array) - && arr.len() == 4 - { - for (i, item) in arr.iter().enumerate() { - if let Some(val) = item.as_f64().filter(|n| n.is_finite()) { - radii[i] = val.max(0.0); - } - } - } - let (pie, arc_mode) = parse_arc_params(p); + let radius = size_or(p, "radius", 0.0, c)?; + let radii = radii_param(p, c)?.unwrap_or([radius; 4]); + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = rect_xf(r, f64_or(p, "rotation", 0.0)); let live = LiveShape::Rectangle { w: r.width(), h: r.height(), radii, kinds: Default::default(), radii_relative: false, xf }; let cs = CustomShape::Rectangle { w: r.width(), h: r.height(), radii, kinds: Default::default(), radii_relative: false, xf, pie, arc_mode }; @@ -164,7 +236,7 @@ fn ellipse(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.ellipse"; check_numbers(p, &["startAngle", "endAngle", "rotation"], c)?; let r = rect_of(p, c)?; - let (pie, arc_mode) = parse_arc_params(p); + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = rect_xf(r, f64_or(p, "rotation", 0.0)); let live = LiveShape::Ellipse { w: r.width(), h: r.height(), pie, xf }; let cs = CustomShape::Ellipse { w: r.width(), h: r.height(), pie, arc_mode, xf }; @@ -174,13 +246,13 @@ fn ellipse(s: &mut Session, p: &Value) -> Result { fn polygon(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.polygon"; let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?); - let r = f64_req(p, "radius", c)?.abs(); + let r = size(p, "radius", c)?; check_numbers(p, &["sides", "rotation", "rounded", "randomized", "startAngle", "endAngle"], c)?; - let sides = (f64_or(p, "sides", 6.0).round() as i64).clamp(3, 1000) as u32; + let sides = count(p, "sides", 6, c)?; let rot = f64_or(p, "rotation", 0.0); - let rounded = f64_or(p, "rounded", 0.0).clamp(-10.0, 10.0); - let randomized = f64_or(p, "randomized", 0.0).clamp(-10.0, 10.0); - let (pie, arc_mode) = parse_arc_params(p); + let rounded = ranged(p, "rounded", 0.0, &ROUNDING, c)?; + let randomized = ranged(p, "randomized", 0.0, &ROUNDING, c)?; + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = Affine::translate(center.to_vec2()) * vectorcraft_geom::rotate_deg(rot); let live = LiveShape::Polygon { radius: r, sides, xf, radii: vec![], kinds: vec![], radii_relative: false }; let cs = CustomShape::Polygon { @@ -202,16 +274,17 @@ fn polygon(s: &mut Session, p: &Value) -> Result { fn star(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.star"; let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?); - let r1 = f64_req(p, "radius1", c)?.abs(); + let r1 = size(p, "radius1", c)?; check_numbers(p, &["radius2", "points", "rotation", "rounded", "randomized", "startAngle", "endAngle"], c)?; - let r2 = f64_or(p, "radius2", r1 / 2.0).abs(); - let points = (f64_or(p, "points", 5.0).round() as i64).clamp(3, 1000) as u32; + // The inner radius is a fraction of the outer one, in `SPOKE_RATIO`. + let r2 = ranged(p, "radius2", r1 / 2.0, &(r1 * SPOKE_RATIO.start()..=r1 * SPOKE_RATIO.end()), c)?; + let points = count(p, "points", 5, c)?; let rot = f64_or(p, "rotation", 0.0); - let rounded = f64_or(p, "rounded", 0.0).clamp(-10.0, 10.0); - let randomized = f64_or(p, "randomized", 0.0).clamp(-10.0, 10.0); - let (pie, arc_mode) = parse_arc_params(p); + let rounded = ranged(p, "rounded", 0.0, &ROUNDING, c)?; + let randomized = ranged(p, "randomized", 0.0, &ROUNDING, c)?; + let (pie, arc_mode) = parse_arc_params(p, c)?; let xf = Affine::translate(center.to_vec2()) * vectorcraft_geom::rotate_deg(rot); - let ratio = if r1 > 1e-9 { (r2 / r1).clamp(0.01, 1.0) } else { 0.5 }; + let ratio = if r1 > 1e-9 { (r2 / r1).clamp(*SPOKE_RATIO.start(), *SPOKE_RATIO.end()) } else { 0.5 }; let cs = CustomShape::Polygon { radius: r1, sides: points, @@ -231,12 +304,12 @@ fn star(s: &mut Session, p: &Value) -> Result { fn spiral(s: &mut Session, p: &Value) -> Result { let c = "addon.shapes.spiral"; let center = Point::new(f64_req(p, "cx", c)?, f64_req(p, "cy", c)?); - let r = f64_req(p, "radius", c)?.abs(); + let r = size(p, "radius", c)?; check_numbers(p, &["turns", "divergence", "innerRadius"], c)?; check_switches(p, &["clockwise"], c)?; - let turns = f64_or(p, "turns", 3.0).clamp(0.01, 1024.0); - let divergence = f64_or(p, "divergence", 1.0).clamp(0.0, 1000.0); - let inner_radius = f64_or(p, "innerRadius", 0.0).clamp(0.0, 0.999); + let turns = ranged(p, "turns", 3.0, &TURNS, c)?; + let divergence = ranged(p, "divergence", 1.0, &DIVERGENCE, c)?; + let inner_radius = ranged(p, "innerRadius", 0.0, &INNER_RADIUS, c)?; let clockwise = bool_or(p, "clockwise", true); let cs = CustomShape::Spiral { center, radius: r, turns, divergence, inner_radius, clockwise }; insert_custom(s, "Spiral", None, cs) @@ -299,24 +372,54 @@ fn configure(s: &mut Session, p: &Value) -> Result { c, )?; check_switches(p, &["isStar", "parallelX", "parallelY", "parallelZ", "reset"], c)?; + check_configure(p, c)?; let ids = targets(s, p)?; let reset = bool_or(p, "reset", false); let spiral_hint = p.get("turns").is_some() || p.get("divergence").is_some() || p.get("innerRadius").is_some(); + // The shapes the params change, worked out first so that a call that changes nothing records + // no undo step. + let changes: Vec<(NodeId, CustomShape)> = { + let doc = &s.doc()?.doc; + ids.into_iter() + .filter_map(|id| { + let n = doc.node(id)?; + let before = CustomShape::read_for_update(n, spiral_hint)?; + let mut cs = before.clone(); + let stored = CustomShape::read(n).is_some(); + (apply_shape_params(&mut cs, p, reset) && (cs != before || !stored)).then_some((id, cs)) + }) + .collect() + }; + if changes.is_empty() { + return Ok(json!({ "updated": 0 })); + } let updated = s.edit("Live Shape", |d, _| { let mut count = 0usize; - for id in ids { - let Some(n) = d.node_mut(id) else { continue }; - let Some(mut cs) = CustomShape::read_for_update(n, spiral_hint) else { continue }; - if apply_shape_params(&mut cs, p, reset) { - cs.write_to_node(n); - count += 1; - } + for (id, cs) in &changes { + let Some(n) = d.node_mut(*id) else { continue }; + cs.write_to_node(n); + count += 1; } Ok(count) })?; Ok(json!({ "updated": updated })) } +/// Error unless every bounded value `p` gives is in its range, before any shape changes. +fn check_configure(p: &Value, c: &str) -> Result<()> { + size_or(p, "radius", 0.0, c)?; + radii_param(p, c)?; + arc_mode(p, c)?; + count(p, "sides", 6, c)?; + ranged(p, "spokeRatio", 0.5, &SPOKE_RATIO, c)?; + ranged(p, "rounded", 0.0, &ROUNDING, c)?; + ranged(p, "randomized", 0.0, &ROUNDING, c)?; + ranged(p, "turns", 3.0, &TURNS, c)?; + ranged(p, "divergence", 1.0, &DIVERGENCE, c)?; + ranged(p, "innerRadius", 0.0, &INNER_RADIUS, c)?; + Ok(()) +} + fn num_param(p: &Value, k: &str) -> Option { p.get(k).and_then(Value::as_f64).filter(|v| v.is_finite()) } @@ -331,8 +434,8 @@ fn apply_arc_params(p: &Value, pie: &mut (f64, f64), arc_mode: &mut shapes::Elli pie.1 = a1.rem_euclid(360.0); changed = true; } - if let Some(m) = str_param(p, "arcMode") { - *arc_mode = parse_mode(m); + if let Some(m) = str_param(p, "arcMode").and_then(parse_mode) { + *arc_mode = m; if *arc_mode == shapes::EllipseMode::Whole { *pie = (0.0, 360.0); } else if (pie.1 - pie.0).abs() < 1e-6 || ((pie.1 - pie.0).abs() - 360.0).abs() < 1e-6 { @@ -351,19 +454,14 @@ fn apply_rect_params(p: &Value, reset: bool, max_r: f64, radii: &mut [f64; 4], p *arc_mode = shapes::EllipseMode::Whole; changed = true; } + // A radius larger than half the rectangle's shorter side is fitted to that size. if let Some(r) = num_param(p, "radius") { *radii = [r.clamp(0.0, max_r); 4]; changed = true; } - if let Some(arr) = p.get("radii").and_then(Value::as_array) - && arr.len() == 4 - { - for (i, v) in arr.iter().enumerate() { - if let Some(r) = v.as_f64().filter(|x| x.is_finite()) { - radii[i] = r.clamp(0.0, max_r); - changed = true; - } - } + if let Ok(Some(four)) = radii_param(p, "addon.shapes.configure") { + *radii = four.map(|r| r.clamp(0.0, max_r)); + changed = true; } changed | apply_arc_params(p, pie, arc_mode) } @@ -378,8 +476,9 @@ fn apply_poly_params(p: &Value, reset: bool, sides: &mut u32, spoke_ratio: &mut } changed = true; } - if let Some(n_sides) = p.get("sides").and_then(Value::as_u64) { - *sides = (n_sides as u32).clamp(3, 1000); + // `check_configure` has refused values outside their ranges. + if let Some(n_sides) = num_param(p, "sides") { + *sides = n_sides as u32; changed = true; } if let Some(star) = p.get("isStar").and_then(Value::as_bool) { @@ -387,15 +486,15 @@ fn apply_poly_params(p: &Value, reset: bool, sides: &mut u32, spoke_ratio: &mut changed = true; } if let Some(sr) = num_param(p, "spokeRatio") { - *spoke_ratio = Some(sr.clamp(0.01, 1.0)); + *spoke_ratio = Some(sr); changed = true; } if let Some(rnd) = num_param(p, "rounded") { - *rounded = rnd.clamp(-10.0, 10.0); + *rounded = rnd; changed = true; } if let Some(rand) = num_param(p, "randomized") { - *randomized = rand.clamp(-10.0, 10.0); + *randomized = rand; changed = true; } changed @@ -429,15 +528,15 @@ fn apply_shape_params(cs: &mut CustomShape, p: &Value, reset: bool) -> bool { changed = true; } if let Some(t) = num_param(p, "turns") { - *turns = t.clamp(0.01, 1024.0); + *turns = t; changed = true; } if let Some(div) = num_param(p, "divergence") { - *divergence = div.clamp(0.0, 1000.0); + *divergence = div; changed = true; } if let Some(ir) = num_param(p, "innerRadius") { - *inner_radius = ir.clamp(0.0, 0.999); + *inner_radius = ir; changed = true; } changed @@ -470,11 +569,12 @@ fn apply_shape_params(cs: &mut CustomShape, p: &Value, reset: bool) -> bool { } } -fn parse_mode(m: &str) -> shapes::EllipseMode { +fn parse_mode(m: &str) -> Option { match m { - "pie" | "slice" => shapes::EllipseMode::Slice, - "chord" => shapes::EllipseMode::Chord, - "arc" | "open" => shapes::EllipseMode::Arc, - _ => shapes::EllipseMode::Whole, + "whole" => Some(shapes::EllipseMode::Whole), + "pie" | "slice" => Some(shapes::EllipseMode::Slice), + "chord" => Some(shapes::EllipseMode::Chord), + "arc" | "open" => Some(shapes::EllipseMode::Arc), + _ => None, } } diff --git a/crates/engine/src/cmd/addons/shapes/tests.rs b/crates/engine/src/cmd/addons/shapes/tests.rs index 793ae6074c..bc5f1359e2 100644 --- a/crates/engine/src/cmd/addons/shapes/tests.rs +++ b/crates/engine/src/cmd/addons/shapes/tests.rs @@ -135,3 +135,169 @@ fn addon_shapes_commands_and_format_roundtrip_preserve_paths() { assert_eq!(CustomShape::read(a), CustomShape::read(b)); } } + +fn id_of(s: &mut Session, cmd: &str, p: serde_json::Value) -> NodeId { + NodeId(s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))["id"].as_u64().unwrap()) +} + +fn shape_of(s: &Session, id: NodeId) -> CustomShape { + CustomShape::read(s.doc().unwrap().doc.node(id).unwrap()).unwrap() +} + +/// Runs `cmd` with `p`, which must fail with `msg` and leave the document and its history as they +/// were. +fn refused(s: &mut Session, cmd: &str, p: serde_json::Value, msg: &str) { + let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false); + let undo = s.doc().unwrap().history.undo.len(); + let e = s.execute(cmd, &p).map(|v| v.to_string()).unwrap_err().to_string(); + assert!(e.contains(msg), "{cmd} {p}: {e}"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{cmd} {p}"); + assert_eq!(vectorcraft_format::save(&s.doc().unwrap().doc, false), saved, "{cmd} {p}"); +} + +#[test] +fn polygon_and_star_refuse_counts_that_are_not_whole_numbers_from_3_to_1000() { + let mut s = new_session(); + for n in [json!(2.5), json!(2), json!(0), json!(-6), json!(1000.5), json!(5000), json!(4_294_967_299u64)] { + let x = n.as_f64().unwrap(); + let p = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": n}); + refused(&mut s, "addon.shapes.polygon", p, &format!("`sides` must be a whole number between 3 and 1000 (got {x})")); + let p = json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": n}); + refused(&mut s, "addon.shapes.star", p, &format!("`points` must be a whole number between 3 and 1000 (got {x})")); + } + refused(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": "6"}), "`sides` must be a number"); + refused(&mut s, "addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": [5]}), "`points` must be a number"); + + // The ends of the range are taken as given, and an omitted or null count is the default. + for (n, want) in [(json!(3), 3), (json!(1000), 1000), (json!(7.0), 7), (json!(null), 6)] { + let id = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0, "sides": n})); + assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides, spoke_ratio: None, .. } if sides == want), "{n}"); + } + let id = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": 10.0})); + assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides: 6, .. })); + for (n, want) in [(json!(3), 3), (json!(1000), 1000), (json!(null), 5)] { + let id = id_of(&mut s, "addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0, "points": n})); + assert!(matches!(shape_of(&s, id), CustomShape::Polygon { sides, spoke_ratio: Some(_), .. } if sides == want), "{n}"); + } +} + +#[test] +fn shapes_refuse_numbers_outside_their_ranges() { + let mut s = new_session(); + let poly = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0}); + let star = json!({"cx": 0.0, "cy": 0.0, "radius1": 10.0}); + let spiral = json!({"cx": 0.0, "cy": 0.0, "radius": 10.0}); + let with = |base: &serde_json::Value, key: &str, v: f64| { + let mut p = base.clone(); + p[key] = json!(v); + p + }; + for (cmd, base, key, bad, good) in [ + ("addon.shapes.polygon", &poly, "rounded", [-10.5, 10.5], [-10.0, 10.0]), + ("addon.shapes.polygon", &poly, "randomized", [-11.0, 10.01], [-10.0, 10.0]), + ("addon.shapes.star", &star, "rounded", [-10.5, 10.5], [-10.0, 10.0]), + ("addon.shapes.star", &star, "randomized", [-11.0, 10.01], [-10.0, 10.0]), + ("addon.shapes.star", &star, "radius2", [0.05, 11.0], [0.1, 10.0]), + ("addon.shapes.spiral", &spiral, "turns", [0.0, 1024.5], [0.01, 1024.0]), + ("addon.shapes.spiral", &spiral, "divergence", [-0.1, 1000.5], [0.0, 1000.0]), + ("addon.shapes.spiral", &spiral, "innerRadius", [-0.1, 1.0], [0.0, 0.999]), + ] { + let (min, max) = (good[0], good[1]); + for x in bad { + refused(&mut s, cmd, with(base, key, x), &format!("`{key}` must be between {min} and {max} (got {x})")); + } + for x in good { + s.execute(cmd, &with(base, key, x)).unwrap_or_else(|e| panic!("{cmd} {key} {x}: {e}")); + } + } + + // Sizes are not negative. + for (cmd, p, key) in [ + ("addon.shapes.polygon", json!({"cx": 0.0, "cy": 0.0, "radius": -10.0}), "radius"), + ("addon.shapes.star", json!({"cx": 0.0, "cy": 0.0, "radius1": -10.0}), "radius1"), + ("addon.shapes.spiral", json!({"cx": 0.0, "cy": 0.0, "radius": -10.0}), "radius"), + ("addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": -10.0, "height": 10.0}), "width"), + ("addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": 10.0, "height": 10.0, "radius": -1.0}), "radius"), + ("addon.shapes.ellipse", json!({"x": 0.0, "y": 0.0, "width": 10.0, "height": -10.0}), "height"), + ("addon.shapes.box3d", json!({"x": 0.0, "y": 0.0, "width": -10.0, "height": 10.0}), "width"), + ] { + refused(&mut s, cmd, p, &format!("`{key}` must be a number not below 0")); + } + + // Corner radii are four numbers that are not negative. + for radii in [json!([1.0, 2.0, 3.0]), json!([1.0, 2.0, 3.0, -4.0]), json!([1.0, 2.0, 3.0, "4"]), json!(4.0)] { + let p = json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "radii": radii}); + refused(&mut s, "addon.shapes.rectangle", p, "`radii` must be four numbers not below 0: [tl, tr, br, bl]"); + } + + // An arc mode is one of the names the commands take. + for mode in [json!("slices"), json!(""), json!(3)] { + let p = json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "arcMode": mode}); + refused(&mut s, "addon.shapes.rectangle", p.clone(), "`arcMode` must be one of whole, pie, chord, arc"); + refused(&mut s, "addon.shapes.ellipse", p, "`arcMode` must be one of whole, pie, chord, arc"); + } + for mode in ["whole", "pie", "slice", "chord", "arc", "open"] { + s.execute("addon.shapes.ellipse", &json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "arcMode": mode})).unwrap(); + } +} + +#[test] +fn configure_refuses_values_outside_their_ranges_before_changing_any_shape() { + let mut s = new_session(); + let rect = id_of(&mut s, "addon.shapes.rectangle", json!({"x": 0.0, "y": 0.0, "width": 100.0, "height": 80.0, "radius": 4.0})); + let poly = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 50.0, "cy": 50.0, "radius": 40.0, "sides": 5})); + let spiral = id_of(&mut s, "addon.shapes.spiral", json!({"cx": 60.0, "cy": 60.0, "radius": 45.0})); + let ids = json!([rect.0, poly.0, spiral.0]); + let cfg = |key: &str, v: serde_json::Value| json!({"ids": ids.clone(), key: v}); + let c = "addon.shapes.configure"; + for n in [json!(2.5), json!(2), json!(-1), json!(5000), json!(4_294_967_299u64)] { + let x = n.as_f64().unwrap(); + refused(&mut s, c, cfg("sides", n), &format!("`sides` must be a whole number between 3 and 1000 (got {x})")); + } + for (key, bad, min, max) in [ + ("spokeRatio", [0.0, 1.5], 0.01, 1.0), + ("rounded", [-10.5, 11.0], -10.0, 10.0), + ("randomized", [-11.0, 10.5], -10.0, 10.0), + ("turns", [0.0, 2000.0], 0.01, 1024.0), + ("divergence", [-1.0, 1000.5], 0.0, 1000.0), + ("innerRadius", [-0.5, 1.0], 0.0, 0.999), + ] { + for x in bad { + refused(&mut s, c, cfg(key, json!(x)), &format!("`{key}` must be between {min} and {max} (got {x})")); + } + } + refused(&mut s, c, cfg("radius", json!(-1.0)), "`radius` must be a number not below 0"); + for radii in [json!([1.0, 2.0]), json!([1.0, 2.0, 3.0, -1.0]), json!("4")] { + refused(&mut s, c, cfg("radii", radii), "`radii` must be four numbers not below 0: [tl, tr, br, bl]"); + } + refused(&mut s, c, cfg("arcMode", json!("bogus")), "`arcMode` must be one of whole, pie, chord, arc"); + // A valid value next to a refused one changes no shape either. + refused(&mut s, c, json!({"ids": ids.clone(), "sides": 8, "turns": 0.0}), "`turns` must be between 0.01 and 1024 (got 0)"); + + // In range, the values are taken as given; a radius larger than the rectangle takes is fitted to it. + s.execute(c, &json!({"ids": [poly.0], "sides": 1000, "rounded": -10.0, "randomized": 10.0})).unwrap(); + assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 1000, rounded, randomized, .. } if rounded == -10.0 && randomized == 10.0)); + s.execute(c, &json!({"ids": [poly.0], "sides": 3})).unwrap(); + assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 3, .. })); + s.execute(c, &json!({"ids": [spiral.0], "turns": 1024.0, "divergence": 0.0, "innerRadius": 0.999})).unwrap(); + assert!( + matches!(shape_of(&s, spiral), CustomShape::Spiral { turns, divergence, inner_radius, .. } if turns == 1024.0 && divergence == 0.0 && inner_radius == 0.999) + ); + s.execute(c, &json!({"ids": [rect.0], "radius": 1000.0})).unwrap(); + assert!(matches!(shape_of(&s, rect), CustomShape::Rectangle { radii, .. } if radii == [40.0; 4])); +} + +#[test] +fn configure_records_no_undo_step_when_nothing_changes() { + let mut s = new_session(); + let poly = id_of(&mut s, "addon.shapes.polygon", json!({"cx": 50.0, "cy": 50.0, "radius": 40.0, "sides": 5, "rounded": 0.5})); + let undo = s.doc().unwrap().history.undo.len(); + let r = s.execute("addon.shapes.configure", &json!({"ids": [poly.0], "sides": 5, "rounded": 0.5})).unwrap(); + assert_eq!(r["updated"], 0); + assert_eq!(s.doc().unwrap().history.undo.len(), undo); + + let r = s.execute("addon.shapes.configure", &json!({"ids": [poly.0], "sides": 6})).unwrap(); + assert_eq!(r["updated"], 1); + assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1); + assert!(matches!(shape_of(&s, poly), CustomShape::Polygon { sides: 6, .. })); +} diff --git a/crates/engine/src/cmd/clipboard/resources.rs b/crates/engine/src/cmd/clipboard/resources.rs index bdb238ce83..f457712415 100644 --- a/crates/engine/src/cmd/clipboard/resources.rs +++ b/crates/engine/src/cmd/clipboard/resources.rs @@ -127,6 +127,17 @@ fn node_refs(n: &Node, f: Visit) { if let Some(cm) = Document::color_mask_of(m) { f(Ref::Name(Res::Image, &cm.key)); } + // A graph keeps paints and style names for its series and labels, which a redraw gives them. + if let Some(g) = m.extras().graph.as_deref() { + vectorcraft_doc::swatches::graph_paints(m).for_each(|p| paint_refs(p, f)); + let l = &g.label_styles; + for s in [&l.values, &l.categories, &l.legend].into_iter().flatten().filter_map(|st| st.style_name.as_ref()) { + f(Ref::Name(Res::CharStyle, s)); + } + for s in [&l.values_para, &l.categories_para, &l.legend_para].into_iter().flatten().filter_map(|pa| pa.style_name.as_ref()) { + f(Ref::Name(Res::ParaStyle, s)); + } + } match &m.kind { NodeKind::Text(t) => { for r in &t.runs { @@ -671,6 +682,27 @@ impl Relink { self.name(Res::Image, &mut cm.key); self.name(Res::Image, &mut cm.from); } + if let Some(g) = n.extras_if_any().and_then(|e| e.graph.as_deref_mut()) { + for s in &mut g.series_paints { + s.fill.iter_mut().chain(s.stroke.iter_mut()).for_each(|p| self.paint(p)); + } + let l = &mut g.label_styles; + for st in [&mut l.values, &mut l.categories, &mut l.legend].into_iter().flatten() { + self.paint(&mut st.fill); + self.paint(&mut st.stroke); + if let Some(s) = &mut st.style_name { + self.name(Res::CharStyle, s); + } + } + for pa in [&mut l.values_para, &mut l.categories_para, &mut l.legend_para].into_iter().flatten() { + if let Some(s) = &mut pa.style_name { + self.name(Res::ParaStyle, s); + } + } + for r in [&mut l.values_rows, &mut l.categories_rows, &mut l.legend_rows].into_iter().flatten() { + r.paint.fill.iter_mut().chain(r.paint.stroke.iter_mut()).for_each(|p| self.paint(p)); + } + } match &mut n.kind { NodeKind::Text(t) => { for r in &mut t.runs { diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 6ee3905d95..65672f8b6b 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -865,8 +865,8 @@ fn load_texture(params: &mut Value, cmd: &str) -> Result<()> { /// Has `n` an appearance Expand Appearance turns into objects: a path that isn't basic (several /// fills or strokes, effects, transparency of a fill or stroke, a brush), a group or layer with /// fills, strokes or effects of its own or such members, other objects with effects, type with -/// fills or strokes of its own, or a symbol instance whose active fill or stroke has a geometry -/// effect ([`effects::has_item_geometry`])? +/// fills or strokes of its own, or a symbol instance whose active fill or stroke has a geometry or +/// raster effect ([`effects::has_item_geometry`], [`effects::has_instance_row_raster`])? pub(crate) fn expandable(n: &Node) -> bool { expandable_except(n, &[]) } @@ -883,7 +883,9 @@ pub(crate) fn expandable_except(n: &Node, skip: &[NodeId]) -> bool { !n.appearance.items.is_empty() || !n.appearance.effects.is_empty() || children.iter().any(|c| expandable_except(c, skip)) } NodeKind::Text(_) => !n.appearance.items.is_empty() || !n.appearance.effects.is_empty(), - NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => effects::has_item_geometry(n) || !n.appearance.effects.is_empty(), + NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => { + effects::has_item_geometry(n) || effects::has_instance_row_raster(n) || !n.appearance.effects.is_empty() + } _ => !n.appearance.effects.is_empty(), } } diff --git a/crates/engine/src/cmd/fileio/tests_pdf.rs b/crates/engine/src/cmd/fileio/tests_pdf.rs index bd52fe71c7..f57e02fc2d 100644 --- a/crates/engine/src/cmd/fileio/tests_pdf.rs +++ b/crates/engine/src/cmd/fileio/tests_pdf.rs @@ -304,11 +304,22 @@ fn options_not_applied_yet_and_document_features_warn() { let frame = s.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap(); s.execute("paint.setStroke", &json!({"ids": [frame], "swatch": "Dots"})).unwrap(); assert_eq!(warnings(&export(&mut s, json!({}))).len(), 0); - // A knockout group is approximated: its warning comes back from every PDF path. + // A knockout group is written as a PDF knockout group: nothing to report. s.execute("select.set", &json!({"ids": [frame]})).unwrap(); s.execute("object.group", &json!({})).unwrap(); s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); let knockout = |w: Vec| w.iter().any(|w| w.contains("knockout groups")); + assert!(!knockout(warnings(&export(&mut s, json!({}))))); + // One whose soft masks would draw a brushed stroke is approximated: its warning comes back + // from every PDF path. + let rect = + |s: &mut Session, x: i32| s.execute("shape.rectangle", &json!({"x": x, "y": 45, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap(); + let (lower, upper) = (rect(&mut s, 20), rect(&mut s, 40)); + s.execute("brush.apply", &json!({"ids": [upper], "name": "3 pt. Round"})).unwrap(); + s.execute("object.setProps", &json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap(); + s.execute("select.set", &json!({"ids": [lower, upper]})).unwrap(); + s.execute("object.group", &json!({})).unwrap(); + s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); assert!(knockout(warnings(&export(&mut s, json!({}))))); assert!(knockout(warnings(&s.execute("document.export", &as_export(&json!({}))).unwrap()))); // Other formats have none. @@ -432,6 +443,17 @@ fn pdf_settings_summarise_changes_and_warnings() { s.execute("effect.apply", &json!({"effect": "stylize.dropShadow", "ids": [g]})).unwrap(); assert_eq!(s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap()["warnings"], json!([])); s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); + // A knockout group is written as a PDF knockout group: nothing to report. + assert_eq!(s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap()["warnings"], json!([])); + // One whose soft masks would draw a brushed stroke is approximated. + let rect = + |s: &mut Session, x: i32| s.execute("shape.rectangle", &json!({"x": x, "y": 45, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap(); + let (lower, upper) = (rect(&mut s, 20), rect(&mut s, 40)); + s.execute("brush.apply", &json!({"ids": [upper], "name": "3 pt. Round"})).unwrap(); + s.execute("object.setProps", &json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap(); + s.execute("select.set", &json!({"ids": [lower, upper]})).unwrap(); + s.execute("object.group", &json!({})).unwrap(); + s.execute("transparency.set", &json!({"knockout": "on"})).unwrap(); assert_eq!(s.execute("document.pdfSettings", &json!({})).unwrap()["warnings"], json!([])); let full = s.execute("document.pdfSettings", &json!({"includeDocument": true})).unwrap(); assert!(warnings(&full).iter().any(|w| w.contains("knockout groups")), "{}", full["warnings"]); diff --git a/crates/engine/src/cmd/scripts.rs b/crates/engine/src/cmd/scripts.rs index f5edf1f91d..385fd071c4 100644 --- a/crates/engine/src/cmd/scripts.rs +++ b/crates/engine/src/cmd/scripts.rs @@ -32,7 +32,7 @@ pub fn specs() -> Vec { "Run Script", [], None, - "{path?: a .js or .jsx file | source?: the script's text (exactly one of the two), name?: the name errors and the report use (default: the script's #script line, else the path's file name, or \"script\"), args?: {…} (at most 1 MiB as JSON; vc.args in the script), answers?: [{kind: alert|confirm|prompt, value: \"ok\" (alert) | true or false (confirm) | text or null (prompt)}] (for a run with nobody to ask: the answers to its dialogs, in order; at most 10000 and 1 MiB as JSON; refused in the app, where the person answers; an empty object is an empty list), maxFuel?: whole number from 1 to 100000000000 (instructions in all; default 1000000000)} run a JavaScript file against the open documents, through the vc object: vc.run(id, params) runs a command and returns its result, a failed command throws an error the script can catch and puts back the documents and the session state it changed, while any file it wrote stays written and is named; vc.document is a snapshot of the active document or null; vc.commands lists the commands a script may run; vc.log(value) adds to the report and the Script Console (script.console); $.write(...values) writes the values to the Script Console as String(value) gives them, and $.writeln(...values) does the same and ends the line; $.evalFile(path, timeout?) runs a file with the same globals and returns its last value (timeout, in milliseconds from 0, is taken and not waited on), $.fileName is the file being run (empty for text), $.includePath the #includepath list and $.engineName the engine's name (VectorCraft without #targetengine); alert(message[, title, errorIcon]), confirm(message[, noAsDflt, title]) and prompt(message, preset[, title]) open a dialog with a Stop button (default title \"Script Alert\"; message and preset as String(value) gives them; errorIcon and noAsDflt as Boolean(value) gives them; No is the default with noAsDflt): alert returns undefined, confirm true for Yes and false for No, prompt the field's text for OK and null for Cancel; in the app the run waits at the call for the person's answer and Stop ends it as script.stop does, a dialog cannot open inside a callback of a built-in, a getter, a conversion or a file $.evalFile runs and throws there (an #include file's code is the script's own and can), and a run with nobody to ask takes the next of answers, and ends when none is left or the next is for another kind of dialog, or when the script ends with answers unused; vc.args is args. Before anything runs, the script's preprocessor lines (# or //@ at the start of a line, outside strings and comments) are read: #include \"file\" and //@include put that file's text in place of the line, looked for beside the file that includes it, then in each #includepath folder in turn (separated by ;, relative to that file's folder; a later #includepath replaces the list), then in each JSINCLUDE folder; #script names the run; #target takes any value and changes nothing; #strict on makes an assignment to a read-only or getter-only property, or a new property on an object that is not extensible (preventExtensions, seal, freeze, vc, $), throw a TypeError, without the rest of ES5 strict mode, and #strict off turns that off (the last #strict line of the script and its includes sets the run; off without one; a $.evalFile file's own line holds for its code); #targetengine name runs the script in the named engine, whose globals and functions stay for the next run that names it until script.engine.drop drops it or the session ends (script.engines lists them; a name of letters, digits and _ not starting with a digit, or a quoted one of 1 to 64 bytes without control characters, matched as written; only in the script itself, once, before its code and #include lines; main, and a script without the line, get a new engine each run; a session keeps 8 named engines, a ninth name is refused, each has 64 MiB of heap, and an uncaught error, a limit or script.stop keeps the globals the run set while the documents go back; host functions kept in a global act for the run that calls them; an error in code an earlier run compiled is at that run's file, line and column, within 8 MiB of source maps kept for each engine, the oldest going first; not on the web); #engine is refused as unsupported, any other #name as unknown, and another //@name is a comment. A script run from a file reads its includes and $.evalFile's files only in that file's folder and the folders below it, refusing a path with a .. name or a link that leads out (a JSINCLUDE folder outside is passed over); a script given as text reads none. File(path) and Folder(path) name files and folders and read them in the same folders, and inside the automation read roots when they are set: exists, length, created, modified, getFiles(mask or function; at most 2048 entries), resolve, and open('r') with read, readln, readch, seek, tell and close; a path outside, a network or device path and a link that leads out give false or null with error \"Script file access denied\", a file is read whole at most 8 MiB (checked from its size first) and a run reads at most 32 MiB, and writing, the file dialogs and execute return false or null with error set. An error in another file is at that file's line and column; vc.api.version and vc.api.has(feature) describe the API. The run is one transaction: on success each changed document gets one undo step, Run Script, and each preference it changed keeps its value, and on an uncaught error, a limit or script.stop every open document and the session go back as they were; files a command wrote before that stay written and are named. While it runs, other commands that change anything end with a busy error. Commands that change anything outside the documents (script.*, except script.check, plugin.*, prefs.*, actionSet.*, shortcutSet.*, help.*, print.*, library.*, importMatches.*, edit.undoGroup.*, file.recovery.*, stroke.widthProfile.*, saving, importing or deleting a preset, text.addFontFiles, text.findFontFiles, text.rescanFonts, links.findFiles, file.place.queue, color.loadProfile, color.removeProfile, file.print, view.proofSetup, view.proofColors, view.overprintPreview, view.separationsPreview) throw. VectorCraft's own limits: 8 MiB of source a file and 32 MiB in all, 64 included or evaluated files 16 deep, 1024 paths looked up for them, 256 MiB of heap (128 MiB on the web), 2000000 host calls, 200000000 instructions without a pause, 64 levels, 16 MiB and 1048576 entries for a value crossing to or from a command, 10000 log entries and 4 MiB of log, 16 KiB of a dialog's message, 256 bytes of its title and 16 KiB of a prompt's preset (past these a RangeError), and 100000 objects and 512 MiB of new document data, past which a person is asked to continue or stop and a run with nobody to ask ends → {ok: true, name, changed: [document names], written: [paths], log: [values], fuelUsed, hostCalls, runTimeMs (the time the script ran, its commands included, waits between frames and for a person's answer left out; 0 without a clock), timingAvailable (false on the web), hostCallCounts: {vcRun, documentReads, commandListReads, vcLog, consoleWrites, fileCalls, fileExecutes}, slowestCommands: [{id, calls, totalMs, maxMs}] (at most 5, the longest in all first, then by id), console: {lines: [text], droppedLines, truncatedLines} (the run's own output, vc.log lines and error), dialogs: [{kind, message, title, errorIcon (alert), noAsDefault (confirm), preset (prompt), answer: \"ok\" | true | false | text | null | \"stop\" | \"missing\"}] (in the order they opened, at most 10000 and 1 MiB), dialogCount, dialogsTruncated}, or, in the app, {running: true, name} and the report later in ui.inspect's scriptReport; a run that fails ends with its error, and its report there also has kind, message, line, column, file (when the error is in another file than the script, or in code an earlier run of its engine compiled), fuelLeft, fuelUsed, hostCalls and the fields above", + "{path?: a .js or .jsx file | source?: the script's text (exactly one of the two), name?: the name errors and the report use (default: the script's #script line, else the path's file name, or \"script\"), args?: {…} (at most 1 MiB as JSON; vc.args in the script), answers?: [{kind: alert|confirm|prompt, value: \"ok\" (alert) | true or false (confirm) | text or null (prompt)}] (for a run with nobody to ask: the answers to its dialogs, in order; at most 10000 and 1 MiB as JSON; refused in the app, where the person answers; an empty object is an empty list), maxFuel?: whole number from 1 to 100000000000 (instructions in all; default 1000000000)} run a JavaScript file against the open documents, through the vc object: vc.run(id, params) runs a command and returns its result, a failed command throws an error the script can catch and puts back the documents and the session state it changed, while any file it wrote stays written and is named; vc.document is a snapshot of the active document or null; vc.commands lists the commands a script may run; vc.log(value) adds to the report and the Script Console (script.console); $.write(...values) writes the values to the Script Console as String(value) gives them, and $.writeln(...values) does the same and ends the line; $.evalFile(path, timeout?) runs a file with the same globals and returns its last value (timeout, in milliseconds from 0, is taken and not waited on), $.fileName is the file of the code that reads it (the file being run for code that eval or Function compiled; empty for code compiled from a script given as text), $.includePath the #includepath list and $.engineName the engine's name (VectorCraft without #targetengine); alert(message[, title, errorIcon]), confirm(message[, noAsDflt, title]) and prompt(message, preset[, title]) open a dialog with a Stop button (default title \"Script Alert\"; message and preset as String(value) gives them; errorIcon and noAsDflt as Boolean(value) gives them; No is the default with noAsDflt): alert returns undefined, confirm true for Yes and false for No, prompt the field's text for OK and null for Cancel; in the app the run waits at the call for the person's answer and Stop ends it as script.stop does, a dialog cannot open inside a callback of a built-in, a getter, a conversion or a file $.evalFile runs and throws there (an #include file's code is the script's own and can), and a run with nobody to ask takes the next of answers, and ends when none is left or the next is for another kind of dialog, or when the script ends with answers unused; vc.args is args. Before anything runs, the script's preprocessor lines (# or //@ at the start of a line, outside strings and comments) are read: #include \"file\" and //@include put that file's text in place of the line, looked for beside the file that includes it, then in each #includepath folder in turn (separated by ;, relative to that file's folder; a later #includepath replaces the list), then in each JSINCLUDE folder; #script names the run; #target takes any value and changes nothing; #strict on makes an assignment to a read-only or getter-only property, or a new property on an object that is not extensible (preventExtensions, seal, freeze, vc, $), throw a TypeError, without the rest of ES5 strict mode, and #strict off turns that off (the last #strict line of the script and its includes sets the run; off without one; a $.evalFile file's own line holds for its code); #targetengine name runs the script in the named engine, whose globals and functions stay for the next run that names it until script.engine.drop drops it or the session ends (script.engines lists them; a name of letters, digits and _ not starting with a digit, or a quoted one of 1 to 64 bytes without control characters, matched as written; only in the script itself, once, before its code and #include lines; main, and a script without the line, get a new engine each run; a session keeps 8 named engines, a ninth name is refused, each has 64 MiB of heap, and an uncaught error, a limit or script.stop keeps the globals the run set while the documents go back; host functions kept in a global act for the run that calls them; an error in code an earlier run compiled is at that run's file, line and column, within 8 MiB of source maps kept for each engine, the oldest going first; not on the web); #engine is refused as unsupported, any other #name as unknown, and another //@name is a comment. A script run from a file reads its includes and $.evalFile's files only in that file's folder and the folders below it, refusing a path with a .. name or a link that leads out (a JSINCLUDE folder outside is passed over); a script given as text reads none. File(path) and Folder(path) name files and folders and read them in the same folders, and inside the automation read roots when they are set: exists, length, created, modified, getFiles(mask or function; at most 2048 entries), resolve, and open('r') with read, readln, readch, seek, tell and close; a path outside, a network or device path and a link that leads out give false or null with error \"Script file access denied\", a file is read whole at most 8 MiB (checked from its size first) and a run reads at most 32 MiB, and writing, the file dialogs and execute return false or null with error set. An error in another file is at that file's line and column; vc.api.version and vc.api.has(feature) describe the API. The run is one transaction: on success each changed document gets one undo step, Run Script, and each preference it changed keeps its value, and on an uncaught error, a limit or script.stop every open document and the session go back as they were; files a command wrote before that stay written and are named. While it runs, other commands that change anything end with a busy error. Commands that change anything outside the documents (script.*, except script.check, plugin.*, prefs.*, actionSet.*, shortcutSet.*, help.*, print.*, library.*, importMatches.*, edit.undoGroup.*, file.recovery.*, stroke.widthProfile.*, saving, importing or deleting a preset, text.addFontFiles, text.findFontFiles, text.rescanFonts, links.findFiles, file.place.queue, color.loadProfile, color.removeProfile, file.print, view.proofSetup, view.proofColors, view.overprintPreview, view.separationsPreview) throw. VectorCraft's own limits: 8 MiB of source a file and 32 MiB in all, 64 included or evaluated files 16 deep, 1024 paths looked up for them, 256 MiB of heap (128 MiB on the web), 2000000 host calls, 200000000 instructions without a pause, 64 levels, 16 MiB and 1048576 entries for a value crossing to or from a command, 10000 log entries and 4 MiB of log, 16 KiB of a dialog's message, 256 bytes of its title and 16 KiB of a prompt's preset (past these a RangeError), and 100000 objects and 512 MiB of new document data, past which a person is asked to continue or stop and a run with nobody to ask ends → {ok: true, name, changed: [document names], written: [paths], log: [values], fuelUsed, hostCalls, runTimeMs (the time the script ran, its commands included, waits between frames and for a person's answer left out; 0 without a clock), timingAvailable (false on the web), hostCallCounts: {vcRun, documentReads, commandListReads, vcLog, consoleWrites, fileCalls, fileExecutes}, slowestCommands: [{id, calls, totalMs, maxMs}] (at most 5, the longest in all first, then by id), console: {lines: [text], droppedLines, truncatedLines} (the run's own output, vc.log lines and error), dialogs: [{kind, message, title, errorIcon (alert), noAsDefault (confirm), preset (prompt), answer: \"ok\" | true | false | text | null | \"stop\" | \"missing\"}] (in the order they opened, at most 10000 and 1 MiB), dialogCount, dialogsTruncated}, or, in the app, {running: true, name} and the report later in ui.inspect's scriptReport; a run that fails ends with its error, and its report there also has kind, message, line, column, file (when the error is in another file than the script, or in code an earlier run of its engine compiled), fuelLeft, fuelUsed, hostCalls and the fields above", always, run ), diff --git a/crates/engine/src/cmd/typemenu.rs b/crates/engine/src/cmd/typemenu.rs index 87c16bb9b9..c140ba6572 100644 --- a/crates/engine/src/cmd/typemenu.rs +++ b/crates/engine/src/cmd/typemenu.rs @@ -138,7 +138,7 @@ pub fn specs() -> Vec { "Paste without Formatting", ["Edit"], Some("Cmd+Alt+V"), - "{center?, dx?, dy?, swatchConflict?} paste as edit.paste; pasted text takes the default character style (one run) and leaves its character and paragraph styles behind → {ids, added, merged, renamed}", + "{center?, dx?, dy?, swatchConflict?} paste as edit.paste; pasted text takes the default character style (one run) and leaves its character and paragraph styles behind, as do the labels of pasted graphs → {ids, added, merged, renamed}", has_clipboard, paste_plain ), @@ -1460,6 +1460,16 @@ fn strip_formatting(n: &mut vectorcraft_doc::Node) { t.set_all_paras(Default::default()); refresh_bounds(t); } + // A graph keeps the names of its labels' character and paragraph styles for each redraw. + if let Some(g) = n.extras_if_any().and_then(|e| e.graph.as_deref_mut()) { + let l = &mut g.label_styles; + for st in [&mut l.values, &mut l.categories, &mut l.legend].into_iter().flatten() { + st.style_name = None; + } + for pa in [&mut l.values_para, &mut l.categories_para, &mut l.legend_para].into_iter().flatten() { + pa.style_name = None; + } + } if let Some(ch) = n.children_mut() { for c in ch.iter_mut() { strip_formatting(std::sync::Arc::make_mut(c)); diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index 1edeb84e93..1b95b5fe2a 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -26,7 +26,8 @@ //! Before a run starts, its preprocessor lines are read ([`include`]): included files take the //! place of their lines, and an error in one is reported at its own file, line and column. //! `$.evalFile(path)` runs another file in the same global environment and gives its last value, -//! `$.fileName` is the file being run and `$.includePath` the `#includepath` list. +//! `$.fileName` is the file of the code that reads it (the file being run for code that `eval` or +//! `Function` compiled) and `$.includePath` the `#includepath` list. //! //! A run writes to the Script Console with `$.write` and `$.writeln`, and `vc.log` adds a line //! there too. The session's console gets a start line, the run's output, its uncaught error, an end diff --git a/crates/engine/src/tests/clipboard.rs b/crates/engine/src/tests/clipboard.rs index d0e003ec49..6cbfd909a9 100644 --- a/crates/engine/src/tests/clipboard.rs +++ b/crates/engine/src/tests/clipboard.rs @@ -468,6 +468,72 @@ fn styles_symbols_and_images_of_the_same_name_are_renamed() { assert_eq!(key(&s, img2), src_key); } +/// A graph whose series fill, value labels and legend rows paint with global swatch "Brand" and +/// whose value labels are set in character style "Emphasis" and paragraph style "Body", as a +/// later redraw would give them to its marks and labels. +fn styled_graph(s: &mut Session) -> NodeId { + s.execute("swatch.new", &json!({"name": "Brand", "color": "#ff0000", "global": true})).unwrap(); + s.execute("charStyle.new", &json!({"name": "Emphasis", "attrs": {"size": 24.0}})).unwrap(); + s.execute("paraStyle.new", &json!({"name": "Body", "attrs": {"space_after": 8.0}})).unwrap(); + let v = s.execute("graph.create", &json!({"type": "column", "x": 200, "y": 200, "width": 60, "height": 40, "csv": ",2024\nQ1,3\nQ2,5"})); + let id = id_of(&v.unwrap()); + let brand = linked(hex("#ff0000"), "Brand", 1.0); + let mut extras = doc_mut(s).node_mut(id).unwrap().extras_mut(); + let g = extras.graph.as_mut().unwrap(); + g.series_paints = vec![vectorcraft_doc::SeriesPaint { fill: Some(brand.clone()), ..Default::default() }]; + let l = &mut g.label_styles; + l.values = Some(vectorcraft_doc::CharStyle { fill: brand.clone(), style_name: Some("Emphasis".into()), ..Default::default() }); + l.values_para = Some(vectorcraft_doc::ParaStyle { style_name: Some("Body".into()), ..Default::default() }); + let rows = vectorcraft_doc::SeriesPaint { stroke: Some(brand), ..Default::default() }; + l.legend_rows = Some(vectorcraft_doc::graph::LabelRows { paint: rows, ..Default::default() }); + id +} + +/// The swatch names a graph's stored paints link to, and its value labels' character and +/// paragraph style names. +fn graph_links(s: &Session, id: NodeId) -> (Vec, Option, Option) { + let n = node(s, id); + let swatches = vectorcraft_doc::swatches::graph_paints(&n).filter_map(|p| match p { + Paint::Solid { swatch: Some(w), .. } => Some(w.clone()), + _ => None, + }); + let l = &n.extras().graph.as_deref().unwrap().label_styles; + let values = l.values.as_ref().and_then(|st| st.style_name.clone()); + (swatches.collect(), values, l.values_para.as_ref().and_then(|pa| pa.style_name.clone())) +} + +/// A pasted graph brings the swatches and text styles its series and labels keep, and follows +/// them when they come in under new names. +#[test] +fn a_pasted_graph_brings_and_follows_its_label_styles_and_paints() { + let mut s = session(); + let g = styled_graph(&mut s); + copy(&mut s, &[g]); + + new_doc(&mut s); + let v = s.execute("edit.pasteInPlace", &json!({})).unwrap(); + let d = doc(&s); + assert!(d.swatch("Brand").is_some(), "{v}"); + assert!(d.char_styles.iter().any(|x| x.name == "Emphasis") && d.para_styles.iter().any(|x| x.name == "Body"), "{v}"); + let brand = vec!["Brand".to_string(); 3]; + assert_eq!(graph_links(&s, ids_of(&v)[0]), (brand.clone(), Some("Emphasis".into()), Some("Body".into()))); + + // Into a document that gives each name something else: they come in renamed. + new_doc(&mut s); + s.execute("swatch.new", &json!({"name": "Brand", "color": "#0000ff", "global": true})).unwrap(); + s.execute("charStyle.new", &json!({"name": "Emphasis", "attrs": {"size": 8.0}})).unwrap(); + s.execute("paraStyle.new", &json!({"name": "Body", "attrs": {"space_after": 2.0}})).unwrap(); + let v = s.execute("edit.pasteInPlace", &json!({"swatchConflict": "add"})).unwrap(); + let brand2 = vec!["Brand 2".to_string(); 3]; + assert_eq!(graph_links(&s, ids_of(&v)[0]), (brand2, Some("Emphasis 2".into()), Some("Body 2".into())), "{v}"); + + // Without formatting: the labels' text styles stay behind. + new_doc(&mut s); + let v = s.execute("edit.pasteWithoutFormatting", &json!({})).unwrap(); + assert!(doc(&s).char_styles.is_empty() && doc(&s).para_styles.is_empty(), "{v}"); + assert_eq!(graph_links(&s, ids_of(&v)[0]), (brand, None, None)); +} + #[test] fn pasting_again_reuses_what_an_earlier_paste_added() { let mut s = session(); diff --git a/crates/engine/src/tests/expand.rs b/crates/engine/src/tests/expand.rs index d853b91110..e4bfa26b2f 100644 --- a/crates/engine/src/tests/expand.rs +++ b/crates/engine/src/tests/expand.rs @@ -422,6 +422,122 @@ fn a_compound_clipping_path_in_the_art_keeps_its_shape() { assert_eq!(seen, (true, true)); } +/// A Drop Shadow 20 pt to the right on row `row` of instance `id`, solid black with no blur. +fn shadow_row(s: &mut Session, id: NodeId, row: usize) { + let params = json!({"x": 20, "y": 0, "blur": 0, "opacity": 100, "mode": "normal"}); + run(s, "effect.apply", json!({"ids": [id.0], "item": row, "effect": "stylize.dropShadow", "params": params})); +} + +/// Is `p` dark: a shadow, not the white page or the red fill? +fn dark(p: [u8; 4]) -> bool { + p[..3].iter().all(|c| *c < 100) +} + +/// A raster effect on a symbol instance's fill draws on the filled shapes of its art: the shadow +/// falls right of the square, the bounds take it in, a hidden fill draws none, and Expand +/// Appearance keeps the look in one undo step. +#[test] +fn a_symbol_instance_fill_shadow_shadows_its_filled_art() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", true); + assert!(near(render(&s).pixel(160, 100), WHITE)); + shadow_row(&mut s, id, row); + let before = render(&s); + assert!(dark(before.pixel(165, 100)), "the fill's shadow falls right of the square: {:?}", before.pixel(165, 100)); + assert!(near(before.pixel(100, 100), RED), "the art draws over its shadow: {:?}", before.pixel(100, 100)); + let doc = s.doc().unwrap().doc.clone(); + let b = vectorcraft_render::painted_bounds(&doc, doc.node(id).unwrap()).expect("the instance has bounds"); + assert!(b.x1 >= 169.0, "the bounds take in the shadow: {b:?}"); + // A view that shows only the shadow, right of the square: the instance is still drawn. + let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; + let right = vectorcraft_render::Renderer::new().render(&doc, 200, 200, vectorcraft_geom::Affine::translate((-155.0, 0.0)), &opts); + assert!(dark(right.pixel(10, 100)), "the shadow past the square's box is drawn: {:?}", right.pixel(10, 100)); + + assert!(expandable(&s), "the fill's shadow can be expanded"); + run(&mut s, "effect.expandAppearance", json!({})); + let ch = node(&s, id).children().cloned().expect("the instance becomes a group"); + assert!(matches!(ch[0].kind, NodeKind::Image(_)), "the shadow is an image under the art: {}", ch[0].kind_label()); + assert!(matches!(ch[1].kind, NodeKind::SymbolInstance { .. }), "the art stays an instance: {}", ch[1].kind_label()); + no_effects_left(&ch[1]); + let after = render(&s); + assert!(differing(&before, &after, 24) < 400, "expanding changed {} pixels", differing(&before, &after, 24)); + run(&mut s, "edit.undo", json!({})); + let n = node(&s, id); + assert!(matches!(n.kind, NodeKind::SymbolInstance { .. }), "one undo gives the instance back"); + assert!(n.appearance.items[row].effects().iter().any(|e| e.id == "stylize.dropShadow")); + + run(&mut s, "appearance.setItem", json!({"ids": [id.0], "index": row, "visible": false})); + assert!(near(render(&s).pixel(165, 100), WHITE), "a hidden fill draws no shadow"); +} + +/// A raster effect on a symbol instance's stroke draws on the stroked shapes of its art only: the +/// stroke's shadow falls right of the stroke, and none falls where only the fill would cast one. +#[test] +fn a_symbol_instance_stroke_shadow_shadows_only_its_stroked_art() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", false); + shadow_row(&mut s, id, row); + let r = render(&s); + assert!(dark(r.pixel(170, 100)), "the stroke's shadow falls right of the stroke: {:?}", r.pixel(170, 100)); + assert!(near(r.pixel(160, 100), WHITE), "the fill casts no shadow: {:?}", r.pixel(160, 100)); +} + +/// Geometry and raster effects on one fill of an instance: the shadow falls from the grown fill. +#[test] +fn a_symbol_instance_fill_shadow_follows_the_fill_offset_before_it() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", true); + offset_row(&mut s, id, row, 10.0); + shadow_row(&mut s, id, row); + let r = render(&s); + assert!(near(r.pixel(44, 100), RED), "the fill grows by the offset: {:?}", r.pixel(44, 100)); + assert!(dark(r.pixel(175, 100)), "the shadow falls from the grown fill: {:?}", r.pixel(175, 100)); +} + +/// An instance inside a symbol's art passes the outer instance's fill shadow on to its own art. +#[test] +fn a_nested_symbol_instance_takes_the_outer_fill_shadow() { + let mut s = session(); + square(&mut s); + run(&mut s, "symbol.new", json!({"name": "Inner"})); + let id = NodeId(run(&mut s, "symbol.new", json!({"name": "Outer"}))["id"].as_u64().unwrap()); + run(&mut s, "appearance.addFill", json!({"ids": [id.0]})); + let row = node(&s, id).appearance.items.len() - 1; + run(&mut s, "appearance.setItem", json!({"ids": [id.0], "index": row, "none": true})); + shadow_row(&mut s, id, row); + assert!(dark(render(&s).pixel(165, 100)), "the inner art casts the shadow: {:?}", render(&s).pixel(165, 100)); +} + +/// Export draws a symbol instance's fill shadow on the filled shapes of its art: baking leaves the +/// shadow on the art's fill, not its stroke, and PDF export makes an image of it. +#[test] +fn export_draws_a_symbol_instance_fill_shadow() { + let mut s = session(); + let (id, row) = instance_with_row(&mut s, "Square", true); + shadow_row(&mut s, id, row); + let doc = s.doc().unwrap().doc.clone(); + let baked = vectorcraft_render::effects::bake_document(&doc).expect("the instance is baked"); + let n = baked.node(id).expect("the baked art keeps the instance's id").clone(); + assert!(!matches!(n.kind, NodeKind::SymbolInstance { .. })); + let shadowed = |i: &vectorcraft_doc::AppearanceItem| i.effects().iter().any(|e| e.id == "stylize.dropShadow"); + let (mut fills, mut strokes) = (0, 0); + n.walk(&mut |part| { + fills += part.appearance.items.iter().filter(|i| i.is_fill() && shadowed(i)).count(); + strokes += part.appearance.items.iter().filter(|i| !i.is_fill() && shadowed(i)).count(); + }); + assert_eq!((fills, strokes), (1, 0), "the art's fill carries the shadow"); + + let flat = crate::flatten_raster_effects(&doc).expect("the shadow becomes an image"); + let mut image = None; + flat.node(id).expect("the flattened art keeps the instance's id").walk(&mut |part| { + if matches!(part.kind, NodeKind::Image(_)) { + image = part.geometric_bounds(); + } + }); + let image = image.expect("the shadow is an image"); + assert!(image.x1 >= 165.0, "the image holds the shadow right of the square: {image:?}"); +} + #[test] fn two_fills_and_an_offset_stroke_become_three_paths_that_look_the_same() { let mut s = session(); diff --git a/crates/engine/src/tests/knockout.rs b/crates/engine/src/tests/knockout.rs index 723ef66dd2..9f2139fd47 100644 --- a/crates/engine/src/tests/knockout.rs +++ b/crates/engine/src/tests/knockout.rs @@ -138,7 +138,7 @@ fn knockout_settings_round_trip_and_old_bools_load() { } #[test] -fn pdf_and_svg_write_knockout_groups_as_masks() { +fn pdf_writes_knockout_groups_and_svg_writes_them_as_masks() { let mut s = session(); let g = knockout_group(&mut s); let plain = s.doc().unwrap().doc.clone(); @@ -149,10 +149,11 @@ fn pdf_and_svg_write_knockout_groups_as_masks() { let r = pdf(&plain); assert!(!String::from_utf8_lossy(&r.bytes).contains("/SMask") && r.warnings.is_empty()); let r = pdf(&ko); - let text = String::from_utf8_lossy(&r.bytes); - // The lower rectangle is drawn through a luminosity mask made of the upper one's alpha. - assert!(text.contains("/Luminosity") && text.contains("/Alpha"), "soft masks"); - assert!(r.warnings.iter().any(|w| w.contains("knockout")), "{:?}", r.warnings); + let text = String::from_utf8_lossy(&r.bytes).replace(' ', ""); + // The rectangles knock out the area they paint: a PDF knockout group, without soft masks. + assert_eq!(text.matches("/Ktrue").count(), 1, "a knockout group"); + assert!(!text.contains("/Luminosity"), "no soft masks"); + assert!(r.warnings.is_empty(), "{:?}", r.warnings); let svg = |d: &Document| vectorcraft_svg::export(d, &Default::default()); assert!(!svg(&plain).contains("knockout")); @@ -173,5 +174,5 @@ fn pdf_and_svg_write_knockout_groups_as_masks() { assert!(svg(&page).contains("isolation")); page.page_knockout = true; assert_eq!(svg(&page).matches("mask=\"url(#knockout-").count(), 1, "the layer is neutral: its objects knock each other out"); - assert!(String::from_utf8_lossy(&pdf(&page).bytes).contains("/Luminosity")); + assert!(String::from_utf8_lossy(&pdf(&page).bytes).replace(' ', "").contains("/Ktrue")); } diff --git a/crates/eps/src/scene.rs b/crates/eps/src/scene.rs index 75a7437e1d..023dbdcbe1 100644 --- a/crates/eps/src/scene.rs +++ b/crates/eps/src/scene.rs @@ -92,6 +92,19 @@ struct ImageCode { } impl ImageCode { + /// A hash of everything that is written for the image: equal images have equal keys. + fn content_key(&self) -> u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + (&self.prelude, &self.dict, self.filter, self.op, &self.ascii).hash(&mut h); + h.finish() + } + + /// Is everything written for `other` the same as for this image? + fn same_image(&self, other: &ImageCode) -> bool { + self.prelude == other.prelude && self.dict == other.dict && self.filter == other.filter && self.op == other.op && self.ascii == other.ascii + } + /// The operators and data that draw the image where it is placed. fn push_inline(&self, s: &mut String) { s.push_str(&self.prelude); @@ -253,6 +266,9 @@ pub(crate) struct Scene<'a> { images: HashMap>, /// The source key and solid paint together identify a colorized encoding. colorized_images: HashMap<(String, String), Option>, + /// Freeform gradients' images by [`ImageCode::content_key`] → their indices in `codes`: a field + /// encoded the same as one already kept draws from it. + fields: HashMap>, /// The encoded images. codes: Vec, /// Where in `body` each placement of an encoded image goes (in order) and the index of its code. @@ -281,6 +297,7 @@ impl<'a> Scene<'a> { brushes: None, images: HashMap::new(), colorized_images: HashMap::new(), + fields: HashMap::new(), codes: vec![], placements: vec![], pending: 0, @@ -478,7 +495,7 @@ impl<'a> Scene<'a> { _ => {} } self.note_losses(n); - let (was, placed) = (self.body.len(), self.placements.len()); + let (was, placed, codes) = (self.body.len(), self.placements.len(), self.codes.len()); match &n.kind { NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => { children.iter().for_each(|c| self.node(c, false)); @@ -536,6 +553,7 @@ impl<'a> Scene<'a> { if self.size() > self.max_body { self.body.truncate(was); self.unplace(placed); + self.forget_fields(codes); self.warn(TOO_LARGE); } } @@ -1364,6 +1382,14 @@ impl<'a> Scene<'a> { if !(long > 1e-9 && long.is_finite()) { return; } + let place = Affine::translate(area.origin().to_vec2()) * Affine::scale_non_uniform(area.width(), area.height()); + if !invertible(place) { + return; + } + if self.size().saturating_add(64) > self.max_body { + self.warn(TOO_LARGE); + return; + } let k = (self.doc.raster_effects_ppi / 72.0).min(MAX_FIELD_PX / long); let along = |len: f64| (len * k).ceil().clamp(1.0, MAX_FIELD_PX) as u16; let (cols, rows) = (along(area.width()), along(area.height())); @@ -1383,9 +1409,43 @@ impl<'a> Scene<'a> { let rgba: Vec = field.display_grid(area, cols, rows).flat_map(|(c, a)| [q(c[0]), q(c[1]), q(c[2]), q(a)]).collect(); self.encode_image(&rgba, w, h) }; - let place = Affine::translate(area.origin().to_vec2()) * Affine::scale_non_uniform(area.width(), area.height()); - if let Some(image) = image { - self.place_inline(&image, place); + let Some(image) = image else { return }; + let key = image.content_key(); + let found = + self.fields.get(&key).and_then(|bucket| bucket.iter().copied().find(|&i| self.codes.get(i).is_some_and(|c| c.same_image(&image)))); + let i = match found { + Some(i) => i, + None => { + // A field that can't be placed isn't kept. + if self.size().saturating_add(image.inline_len).saturating_add(64) > self.max_body { + self.warn(TOO_LARGE); + return; + } + let i = self.add_code(image); + self.fields.entry(key).or_default().push(i); + i + } + }; + self.place_image(i, place); + } + + /// Forget the field images after the first `n` codes that no placement draws any longer, last + /// first, up to the first code that is drawn or isn't a field image. + fn forget_fields(&mut self, n: usize) { + while self.codes.len() > n { + let i = self.codes.len() - 1; + let Some(code) = self.codes.get(i) else { break }; + if code.uses > 0 { + break; + } + let key = code.content_key(); + let Some(bucket) = self.fields.get_mut(&key) else { break }; + let Some(at) = bucket.iter().position(|&j| j == i) else { break }; + bucket.swap_remove(at); + if bucket.is_empty() { + self.fields.remove(&key); + } + self.codes.pop(); } } @@ -1595,21 +1655,6 @@ impl<'a> Scene<'a> { self.line("Q"); } - /// Draw `code`, an image drawn nowhere else, on the unit square mapped by `place`. - fn place_inline(&mut self, code: &ImageCode, place: Affine) { - if !invertible(place) { - return; - } - if self.size().saturating_add(code.inline_len).saturating_add(64) > self.max_body { - self.warn(TOO_LARGE); - return; - } - self.line("q"); - let _ = writeln!(self.body, "{} cm", ps::matrix(place)); - code.push_inline(&mut self.body); - self.line("Q"); - } - /// Straight RGBA pixels (`w` × `h`, rows from the top) as the operators and data that draw /// them on the unit square: in the process colour space, transparent pixels masked out at /// Level 3 (a key colour no opaque pixel has), white at Level 2; partly transparent ones over diff --git a/crates/eps/src/tests.rs b/crates/eps/src/tests.rs index 2ab96d3555..e9d6f79521 100644 --- a/crates/eps/src/tests.rs +++ b/crates/eps/src/tests.rs @@ -1264,6 +1264,104 @@ fn the_page_limit_counts_a_repeated_images_samples_once() { assert!(page.body.len() + page.setup.len() <= inline + 1000 + 400, "{}", page.body.len()); } +/// A freeform gradient of `points` (x, y and color). +fn freeform(points: &[(f64, f64, Color)]) -> Paint { + let mut g = GradientPaint::new(Gradient { kind: GradientKind::Freeform, ..Default::default() }); + g.freeform = Some(Freeform { points: points.iter().map(|&(x, y, c)| FreeformPoint::new(Point::new(x, y), c)).collect(), ..Default::default() }); + Paint::Gradient(Box::new(g)) +} + +/// A square at (`x`, `y`) of side `side`, filled with a freeform gradient of `a` and `b` at a +/// fifth and four fifths of its width. +fn freeform_square(d: &mut Document, x: f64, y: f64, side: f64, a: Color, b: Color) -> Node { + let mid = y + side / 2.0; + let paint = freeform(&[(x + side / 5.0, mid, a), (x + side * 0.8, mid, b)]); + path(d, shapes::rectangle(Rect::new(x, y, x + side, y + side)), paint, Paint::None, 0.0) +} + +/// A freeform gradient on a shape and on a moved copy of it is sampled the same for both: its +/// image's samples are written once, at both levels, in RGB and in CMYK. +#[test] +fn a_freeform_gradients_image_drawn_twice_writes_its_samples_once() { + let rgb = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)); + let cmyk = (Color::cmyk(1.0, 0.0, 0.0, 0.0), Color::cmyk(0.0, 0.0, 0.0, 1.0)); + for (a, b) in [rgb, cmyk] { + let single = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, a, b)]); + let twice = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, a, b), freeform_square(d, 64.0, 64.0, 64.0, a, b)]); + for level in Level::ALL { + let one = export(&single, &EpsOptions { level, ..opts(&single) }, None, None).unwrap(); + let one_ps = text(&one); + assert!(!one_ps.contains("VCim"), "a field drawn once is written where it is drawn: {one_ps}"); + let out = export(&twice, &EpsOptions { level, ..opts(&twice) }, None, None).unwrap(); + let ps = text(&out); + checked_page(&ps); + assert_eq!(reuses(&ps, 0, level), 2, "{ps}"); + assert!(!ps.contains("VCim1"), "{ps}"); + let body = &ps[ps.find("%%EndSetup").unwrap()..]; + assert!(!body.contains("currentfile") && !body.contains("~>"), "no samples in the page: {ps}"); + assert_eq!(reused_samples(&ps, level), inline_samples(&one_ps, level), "{level:?}"); + assert_eq!(out.warnings, one.warnings); + } + } +} + +/// Fields sampled differently are each written where they are drawn: different gradients, and one +/// gradient on shapes of different sizes. +#[test] +fn freeform_gradients_sampled_differently_dont_share_an_image() { + let (red, blue, green) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0), Color::rgb(0.0, 1.0, 0.0)); + let colors = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, green)]); + let sizes = doc_with(|d| { + let paint = freeform(&[(20.0, 50.0, red), (80.0, 50.0, blue)]); + vec![ + path(d, shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), paint.clone(), Paint::None, 0.0), + path(d, shapes::rectangle(Rect::new(0.0, 0.0, 50.0, 100.0)), paint, Paint::None, 0.0), + ] + }); + for d in [colors, sizes] { + for level in Level::ALL { + let ps = page_of(&d, &EpsOptions { level, ..opts(&d) }); + assert!(!ps.contains("VCim"), "{ps}"); + assert_eq!(ps.matches("} exec\n").count(), 2, "{ps}"); + } + } +} + +/// A field's second placement that passes the page limit is left out, and the first is written +/// where it is drawn. A field taken back with the shape that passed the limit leaves nothing in the +/// setup. +#[test] +fn a_field_taken_back_at_the_page_limit_leaves_no_image_behind() { + let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)); + let single = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, red, blue)]); + let o = opts(&single); + let inline = crate::scene::Scene::new(&single, &o).run().body; + let twice = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, blue)]); + let page = crate::scene::Scene::new(&twice, &o).with_max_body(inline.len() + 50).run(); + assert!(page.warnings.iter().any(|w| w.contains("too large")), "{:?}", page.warnings); + assert!(page.body.starts_with(&inline), "{}", page.body); + assert_eq!(page.body.matches("} exec\n").count(), 1, "{}", page.body); + assert!(!page.setup.contains("VCim"), "{}", page.setup); + // A many-sided shape whose stroke passes the limit after its fill's field was placed. + let big = |d: &mut Document, stroke: Paint| { + let paint = freeform(&[(60.0, 100.0, Color::rgb(0.0, 1.0, 0.0)), (140.0, 100.0, red)]); + path(d, shapes::polygon(Point::new(100.0, 100.0), 90.0, 2000, 0.0), paint, stroke, 1.0) + }; + let filled = doc_with(|d| vec![big(d, Paint::None)]); + let stroked = doc_with(|d| vec![big(d, Paint::solid(Color::rgb(0.0, 0.0, 0.0)))]); + let fill = crate::scene::Scene::new(&filled, &o).run().body.len(); + let both = crate::scene::Scene::new(&stroked, &o).run().body.len(); + assert!(both > fill + 20_000, "{fill} {both}"); + let d = doc_with(|d| { + let s = big(d, Paint::solid(Color::rgb(0.0, 0.0, 0.0))); + vec![s, freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, blue)] + }); + let page = crate::scene::Scene::new(&d, &o).with_max_body(fill + (both - fill) / 2).run(); + assert!(page.warnings.iter().any(|w| w.contains("too large")), "{:?}", page.warnings); + assert_eq!(reuses(&page.body, 0, Level::Three), 2, "{}", page.body); + assert!(page.setup.contains("/VCim0 VCsrc") && !page.setup.contains("VCim1"), "{}", page.setup); +} + #[test] fn run_length_and_ascii85_round_trip() { let cycle: Vec = (0..=255u8).cycle().take(1000).collect(); diff --git a/crates/pathops/src/sweep_axis.rs b/crates/pathops/src/sweep_axis.rs index fb544ad163..8940515dcc 100644 --- a/crates/pathops/src/sweep_axis.rs +++ b/crates/pathops/src/sweep_axis.rs @@ -259,8 +259,9 @@ mod tests { #[test] fn offset_path_sweeps_a_long_low_path_turned_with_the_result_of_the_path_on_end() { let mut rng = Rng(17); - // Grown past the zigzag's bends, where the stroker leaves loops whose faces are dropped by - // the distance test; grown a little; and inset. + // An open zigzag grown by 1.5, a closed one grown by 0.5 and a closed one inset by 0.3. Each + // is swept turned when low and as it is when stood on end, and the two results cover the + // same region. None of the three leaves a face for the distance test to drop. for (closed, delta) in [(false, 1.5), (true, 0.5), (true, -0.3)] { let what = format!("closed {closed}, offset {delta}"); let low = zigzag(3, 300, 0.4, &mut rng, closed); @@ -274,7 +275,8 @@ mod tests { assert_same_region(&turned, &upright(&on_end), &what); } // A long plate with a row of round holes: growing it by 1 closes the narrow holes, which - // the distance test drops, and keeps the wide ones. + // the distance test drops when it measures each hole's point turned back into the plate's + // own frame, and keeps the wide ones. use kurbo::Shape; let mut plate = kurbo::Rect::new(0.0, 0.0, 4000.0, 20.0).to_path(0.1); for i in 0..200u32 { diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 4ba4aa123d..4ba361d29b 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -46,7 +46,7 @@ use exporter::{MAX_COORD, writable, xf}; #[cfg(test)] use images::{png_8_bit, png_16_bit}; #[cfg(test)] -pub(crate) use nodes::MEASURES; +pub(crate) use nodes::{KNOCKOUT_BRUSHES, KNOCKOUT_SHAPES, MAX_KNOCKOUT_MASK_OBJECTS, MEASURES}; pub(crate) use writer::{Sheet, Writer}; pub(crate) struct Exporter<'a> { @@ -127,6 +127,14 @@ pub(crate) struct Exporter<'a> { /// The most [`Self::symbol_nodes`] may reach: [`vectorcraft_doc::MAX_SYMBOL_NODES`], or less /// while one of the copies on a sheet is drawn ([`Writer::page`]). pub(crate) symbol_limit: u64, + /// Objects the symbol instances in the copies drawn in knockout masks held, together: counted + /// apart from [`Self::symbol_nodes`], within [`vectorcraft_doc::MAX_SYMBOL_NODES`] of their own. + mask_symbol_nodes: u64, + /// Whether a knockout mask is being drawn: [`Self::symbol_nodes`] then counts + /// [`Self::mask_symbol_nodes`]. + in_knockout_mask: bool, + /// Whether a knockout mask left out symbol art because the objects its copies may hold ran out. + mask_art_left_out: bool, } #[cfg(test)] diff --git a/crates/pdf/src/export/exporter.rs b/crates/pdf/src/export/exporter.rs index 22109b3e82..f102e0a0ab 100644 --- a/crates/pdf/src/export/exporter.rs +++ b/crates/pdf/src/export/exporter.rs @@ -54,6 +54,9 @@ impl<'a> Exporter<'a> { paper: Rect::ZERO, symbol_nodes: 0, symbol_limit: vectorcraft_doc::MAX_SYMBOL_NODES, + mask_symbol_nodes: 0, + in_knockout_mask: false, + mask_art_left_out: false, overprint: false, overprinted: false, invisible: false, diff --git a/crates/pdf/src/export/nodes.rs b/crates/pdf/src/export/nodes.rs index 097ae52bba..78930c7500 100644 --- a/crates/pdf/src/export/nodes.rs +++ b/crates/pdf/src/export/nodes.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; + use super::exporter::{UNDECODED, address, blend, color, constant_mask, cover, in_range, norm, rects_overlap, rule, to_path, writable, xf}; use super::{ - Affine, AppearanceItem, BezPath, BlendMode, Exporter, Fill, FillRule, GradientKind, LineCap, LineJoin, Mark, Node, NodeKind, NormalizedF32, - Paint, Path, Rect, Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke, + Affine, AppearanceItem, BezPath, BlendMode, Document, Exporter, Fill, FillRule, GradientKind, LineCap, LineJoin, Mark, Node, NodeKind, + NormalizedF32, Paint, Path, Rect, Shape, Stroke, StrokeAlign, StrokeDash, StrokeLayer, Surface, WrittenShape, krilla, stroke, }; /// An object's visual box for leaving out what is off the page. A plain group or layer also holds @@ -57,88 +59,146 @@ impl Exporter<'_> { /// The children of a group, as the elements of a knockout group when it is one. pub(super) fn children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect) { - self.measured_children(s, children, page, &[]); + self.measured_children(s, children, page, &[], false); } - /// [`Self::children`] with the boxes of the last members in `measured`. - fn measured_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured]) { + /// [`Self::children`] with the boxes of the last members in `measured`, in an isolated group + /// when `isolated`. + fn measured_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured], isolated: bool) { if !self.knockout { + if isolated { + s.push_isolated(); + } let unmeasured = children.len().saturating_sub(measured.len()); for (i, c) in children.iter().enumerate() { self.measured_node(s, c, page, false, i.checked_sub(unmeasured).and_then(|i| measured.get(i))); } + if isolated { + s.pop(); + } return; } - let elements: Vec<_> = Node::knockout_elements(children) + let elements: Vec<(&Node, Measured)> = Node::knockout_elements(children) .into_iter() - .map(|c| (c, self.measure(c))) + .map(|c| (&**c, self.measure(c))) .filter(|(_, m)| m.bounds.is_some_and(|b| rects_overlap(b.inflate(1.0, 1.0), page))) .collect(); - if !elements.is_empty() && elements.iter().all(|(c, _)| c.knockout_outline().is_some()) { - let pieces: Vec<&Node> = elements.iter().map(|(c, _)| &***c).collect(); - if vectorcraft_effects::warp_pieces_translucent(self.doc, &pieces) { - return self.warp_pieces(s, &elements, page); + if elements.is_empty() { + return; + } + if elements.iter().all(|(c, _)| c.knockout_outline().is_some()) { + let pieces: Vec<&Node> = elements.iter().map(|(c, _)| *c).collect(); + if isolated { + s.push_isolated(); } - for (c, m) in &elements { - self.measured_node(s, c, page, false, Some(m)); + if vectorcraft_effects::warp_pieces_translucent(self.doc, &pieces) { + self.warp_pieces(s, &elements, page); + } else { + for (c, m) in &elements { + self.measured_node(s, c, page, false, Some(m)); + } + } + if isolated { + s.pop(); } return; } - // The PDF writer has no knockout groups: each element is drawn through a soft mask of - // where the elements above it don't paint (the same look for Normal blending). The masks - // nest, so element i sits inside the masks of elements i+1…n: open them outermost first. - self.warn("knockout groups are written as soft-masked groups (same look, but not editable as knockout groups)"); - let Some(((first, measured), rest)) = elements.split_first() else { return }; - for (c, m) in rest.iter().rev() { - let mask = self.knockout_mask(s, c, page, m); - s.push_mask(mask); + let several = elements.len() > 1; + // A PDF knockout group (`/K`) knocks out by the area each element paints. Where an element + // above another knocks out by less (its opacity or mask defines its knockout shape, or it + // paints translucently), each element is drawn through one soft mask of where the + // elements above it that overlap it paint, while those masks hold no more than + // [`MAX_KNOCKOUT_MASK_OBJECTS`] objects and no brushed stroke. + if several && !elements.iter().skip(1).all(|(c, _)| shape_is_coverage(c)) { + let doc = self.doc; + let brushes = self.brushes.get_or_insert_with(|| vectorcraft_brush::library(doc)); + match knockout_masks(BrushSearch::new(doc, brushes), &elements) { + Ok(above) => { + // Every mask is made before anything is drawn: when the symbol objects the + // masks may hold run out, one would leave out art its element covers, and + // the group is written as a PDF knockout group instead. + let outer = std::mem::take(&mut self.mask_art_left_out); + let before = *self.mask_counter(); + let mut masks = Vec::with_capacity(above.len()); + for above in above { + let shapes: Vec<_> = above.into_iter().map(|j| &elements[j]).collect(); + masks.push((!shapes.is_empty()).then(|| self.knockout_mask(s, &shapes, page))); + } + let left_out = std::mem::replace(&mut self.mask_art_left_out, outer); + if left_out { + *self.mask_counter() = before; + self.warn(KNOCKOUT_SHAPES); + } else { + if isolated { + s.push_isolated(); + } + for ((c, m), mask) in elements.iter().zip(masks) { + if let Some(mask) = mask { + s.push_mask(mask); + self.measured_node(s, c, page, false, Some(m)); + s.pop(); + } else { + self.measured_node(s, c, page, false, Some(m)); + } + } + if isolated { + s.pop(); + } + return; + } + } + Err(warning) => self.warn(warning), + } } - self.measured_node(s, first, page, false, Some(measured)); - for (c, m) in rest { - s.pop(); + let group = several || isolated; + if group { + s.push_group(krilla::blend::GroupOptions { isolated, knockout: several }); + } + for (c, m) in &elements { + // In a PDF knockout group each painting operation knocks out the ones before it. An + // element drawn with several is one non-isolated group (drawn through a full alpha + // mask), so that its own fills and strokes don't knock each other out; its opacity + // or opacity mask makes it one already. + let unit = several && !one_paint(c) && c.opacity >= 1.0 && c.extras().mask.as_deref().is_none_or(|m| m.disabled); + if unit { + let all = constant_mask(s, page, 1.0); + s.push_mask(all); + } self.measured_node(s, c, page, false, Some(m)); + if unit { + s.pop(); + } + } + if group { + s.pop(); } } /// [`Self::measured_children`] of a plain group or layer. When they are the pieces of a /// mesh-warped image, every piece, on the page or not, takes part in the one size its pixels /// are resampled for ([`Self::warp_image_size`]). - fn group_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured]) { + fn group_children(&mut self, s: &mut Surface, children: &[std::sync::Arc], page: Rect, measured: &[Measured], isolated: bool) { let outer = self.warp_image_size(children).map(|w| self.warp_size.replace(w)); - self.measured_children(s, children, page, measured); + self.measured_children(s, children, page, measured, isolated); if let Some(outer) = outer { self.warp_size = outer; } } - /// A luminosity mask that is 1 − the knockout shape of `c`, whose box is in `measured`: white, - /// then black through an alpha mask of `c` (at full object opacity without its own mask, unless - /// those define its shape). - fn knockout_mask(&mut self, s: &mut Surface, c: &Node, page: Rect, measured: &Measured) -> krilla::mask::Mask { - let shape = if c.knockout_shape { - c.clone() - } else { - let mut s = c.clone().with_mask(None); - s.opacity = 1.0; - s - }; - self.shapes_mask(s, &[(&shape, Some(measured))], page) - } - /// The pieces of a partly transparent mesh-warped image (`elements`, each with its box) as the /// elements of a knockout group ([`vectorcraft_effects::warp_pieces_translucent`]). Each piece /// is drawn through one soft mask of where the pieces above it that reach its box don't cover /// ([`vectorcraft_effects::knockout_silhouette`]): only neighbors overlap, so each mask holds a /// few triangles and no mask nests in another. A piece above that overlaps it only over opaque /// pixels ([`vectorcraft_effects::WarpSeams`]) is left out of its mask. - fn warp_pieces(&mut self, s: &mut Surface, elements: &[(&std::sync::Arc, Measured)], page: Rect) { + fn warp_pieces(&mut self, s: &mut Surface, elements: &[(&Node, Measured)], page: Rect) { // Each piece's triangle and its box: a piece paints inside its triangle only. let silhouette = |c: &Node| { let b = c.knockout_outline()?.geometric_bounds()?; Some((vectorcraft_effects::knockout_silhouette(c, vectorcraft_doc::color::Color::WHITE)?, b)) }; let silhouettes: Vec> = elements.iter().map(|(c, _)| silhouette(c)).collect(); - let pieces: Vec<&Node> = elements.iter().map(|(c, _)| &***c).collect(); + let pieces: Vec<&Node> = elements.iter().map(|(c, _)| *c).collect(); let seams = vectorcraft_effects::WarpSeams::new(self.doc, &pieces); for (i, (c, measured)) in elements.iter().enumerate() { let Some((_, reach)) = &silhouettes[i] else { continue }; @@ -191,6 +251,67 @@ impl Exporter<'_> { krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity) } + /// A luminosity mask that is 1 − the knockout shapes of `shapes` (each with its box): white, + /// then black through an alpha mask of each (at full object opacity without its own mask, + /// unless those define its shape). The symbol instances in the copies count toward + /// [`Exporter::mask_symbol_nodes`], not toward the objects the visible art may draw. A + /// knockout mask drawn inside another one counts toward the same objects as the outer one. + fn knockout_mask(&mut self, s: &mut Surface, shapes: &[&(&Node, Measured)], page: Rect) -> krilla::mask::Mask { + if self.in_knockout_mask { + return self.knockout_mask_art(s, shapes, page); + } + self.in_knockout_mask = true; + let visible = std::mem::replace(&mut self.symbol_nodes, self.mask_symbol_nodes); + let limit = std::mem::replace(&mut self.symbol_limit, vectorcraft_doc::MAX_SYMBOL_NODES); + let mask = self.knockout_mask_art(s, shapes, page); + self.mask_symbol_nodes = std::mem::replace(&mut self.symbol_nodes, visible); + self.symbol_limit = limit; + self.in_knockout_mask = false; + mask + } + + /// The symbol objects the copies drawn in knockout masks held, together: [`Self::symbol_nodes`] + /// while a knockout mask is being drawn, [`Exporter::mask_symbol_nodes`] otherwise. + fn mask_counter(&mut self) -> &mut u64 { + if self.in_knockout_mask { &mut self.symbol_nodes } else { &mut self.mask_symbol_nodes } + } + + /// [`Self::knockout_mask`], drawn within the symbol objects counted at the time. + fn knockout_mask_art(&mut self, s: &mut Surface, shapes: &[&(&Node, Measured)], page: Rect) -> krilla::mask::Mask { + let transform = s.group_transform(); + let mut sb = s.stream_builder(); + let mut ms = Surface::stream(sb.surface()); + ms.push_transform(&transform); + cover(&mut ms, page, 255, 1.0); + for (c, measured) in shapes.iter().map(|e| (e.0, &e.1)) { + let shape = if c.knockout_shape { + c.clone() + } else { + let mut s = c.clone().with_mask(None); + s.opacity = 1.0; + s + }; + let alpha = { + let transform = ms.group_transform(); + let mut ab = ms.stream_builder(); + let mut als = Surface::stream(ab.surface()); + als.push_transform(&transform); + let overprint = std::mem::take(&mut self.overprint); + self.measured_node(&mut als, &shape, page, false, Some(measured)); + self.overprint = overprint; + als.pop(); + als.finish(); + krilla::mask::Mask::new(ab.finish(), krilla::mask::MaskType::Alpha) + }; + ms.push_mask(alpha); + cover(&mut ms, page, 0, 1.0); + ms.pop(); + } + ms.pop(); + ms.finish(); + krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Luminosity) + } + /// The box of `n`, and of the members of a plain group or layer. fn measure(&self, n: &Node) -> Measured { if !matches!(n.kind, NodeKind::Layer { clip: false, .. } | NodeKind::Group { clip: false, .. }) { @@ -242,23 +363,7 @@ impl Exporter<'_> { // Whether this container's children knock each other out (a knockout group is written as a group). let knockout = n.knocks_out(self.knockout); let enclosing = std::mem::replace(&mut self.knockout, knockout); - let mut pushes = 0; - if n.blend != BlendMode::Normal { - s.push_blend_mode(blend(n.blend)); - pushes += 1; - } - if n.opacity < 1.0 || (container && (n.isolate || n.blend != BlendMode::Normal || knockout)) { - if !n.isolate && n.blends_through() { - // Not isolated: blending inside reaches the art below the group. - let alpha = constant_mask(s, page, n.opacity); - s.push_mask(alpha); - } else if n.opacity < 1.0 { - s.push_opacity(norm(n.opacity)); - } else { - s.push_isolated(); - } - pushes += 1; - } + let (mut pushes, isolated_children) = transparency(s, n, page, container, knockout); let mask = n.extras().mask.as_deref().filter(|m| !m.disabled); let alpha = match (&n.kind, mask) { (NodeKind::Image(im), Some(m)) => self.masked_image(im, m, !n.extras().pixelated), @@ -279,7 +384,7 @@ impl Exporter<'_> { } match &n.kind { NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => { - self.group_children(s, children, page, measured.map_or(&[], |m| m.members.as_slice())); + self.group_children(s, children, page, measured.map_or(&[], |m| m.members.as_slice()), isolated_children); } NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => { // The region every output clips to; nothing to clip by hides the clipped art. The @@ -366,8 +471,10 @@ impl Exporter<'_> { (art, count) }) }); - if let Some(art) = placed.flatten() { - self.node(s, &art, page, true); + match placed.flatten() { + Some(art) => self.node(s, &art, page, true), + // A knockout mask that leaves the art out no longer matches what it masks. + None => self.mask_art_left_out |= self.in_knockout_mask, } } } @@ -741,3 +848,379 @@ impl Exporter<'_> { true } } + +/// Opens the blend mode and the transparency group `n` is drawn with (`container` when it is a +/// group or layer whose children `knockout` each other). Returns how many were opened, and +/// whether the children are to be drawn in an isolated group of their own: the knockout group of +/// an unclipped group's elements is its isolated group too. +fn transparency(s: &mut Surface, n: &Node, page: Rect, container: bool, knockout: bool) -> (usize, bool) { + let mut pushes = 0; + if n.blend != BlendMode::Normal { + s.push_blend_mode(blend(n.blend)); + pushes += 1; + } + let mut isolated_children = false; + if n.opacity < 1.0 || (container && (n.isolate || n.blend != BlendMode::Normal || knockout)) { + if !n.isolate && n.blends_through() { + // Not isolated: blending inside reaches the art below the group. + let alpha = constant_mask(s, page, n.opacity); + s.push_mask(alpha); + pushes += 1; + } else if n.opacity < 1.0 { + s.push_opacity(norm(n.opacity)); + pushes += 1; + } else if knockout && matches!(n.kind, NodeKind::Layer { clip: false, .. } | NodeKind::Group { clip: false, .. }) { + isolated_children = true; + } else { + s.push_isolated(); + pushes += 1; + } + } + (pushes, isolated_children) +} + +/// The warning for a knockout group whose elements are too many to draw through soft masks: it is +/// written as a PDF knockout group, where an element knocks out all of the area it paints. +pub(crate) const KNOCKOUT_SHAPES: &str = "knockout groups with too many overlapping translucent elements are written as PDF knockout groups: \ + their translucent elements knock out what they cover fully"; + +/// The most objects the soft masks of one knockout group's elements draw between them: each mask +/// draws the elements above its own element that overlap it. +pub(crate) const MAX_KNOCKOUT_MASK_OBJECTS: usize = 1 << 16; + +/// The most elements of one knockout group drawn through soft masks: finding the elements that +/// overlap compares every pair. +pub(crate) const MAX_MASKED_ELEMENTS: usize = 2048; + +/// The warning for a knockout group in which a soft mask would draw an element with a brushed +/// stroke: it is written as a PDF knockout group, where an element knocks out all of the area it +/// paints. +pub(crate) const KNOCKOUT_BRUSHES: &str = "knockout groups with brushed strokes over other translucent elements are written as PDF knockout groups: \ + their translucent elements knock out what they cover fully"; + +/// For each element of a knockout group (with its box), the indices of the elements above it whose +/// boxes overlap its own. The warning to give instead ([`KNOCKOUT_SHAPES`]) when there are more than +/// [`MAX_MASKED_ELEMENTS`] elements or the masks would draw more than +/// [`MAX_KNOCKOUT_MASK_OBJECTS`] objects, or ([`KNOCKOUT_BRUSHES`]) when a mask would draw a +/// brushed stroke ([`BrushSearch`]), whose brush art the object count leaves out. +fn knockout_masks(mut search: BrushSearch, elements: &[(&Node, Measured)]) -> Result>, &'static str> { + if elements.len() > MAX_MASKED_ELEMENTS { + return Err(KNOCKOUT_SHAPES); + } + let counts: Vec = elements.iter().map(|(c, _)| c.count_with_masks()).collect(); + let mut checked: Vec> = vec![None; elements.len()]; + let mut objects = 0usize; + let mut out = Vec::with_capacity(elements.len()); + for (i, (_, below)) in elements.iter().enumerate() { + let mut above = vec![]; + for (j, (over_node, over)) in elements.iter().enumerate().skip(i + 1) { + if let (Some(a), Some(b)) = (below.bounds, over.bounds) + && rects_overlap(a.inflate(1.0, 1.0), b.inflate(1.0, 1.0)) + { + // The mask draws a copy of the element without its own opacity mask, unless the + // mask defines its knockout shape. + if *checked[j].get_or_insert_with(|| search.node(over_node, over_node.knockout_shape, 0)) { + return Err(if search.exhausted { KNOCKOUT_SHAPES } else { KNOCKOUT_BRUSHES }); + } + objects = objects.checked_add(counts[j]).filter(|&o| o <= MAX_KNOCKOUT_MASK_OBJECTS).ok_or(KNOCKOUT_SHAPES)?; + above.push(j); + } + } + out.push(above); + } + Ok(out) +} + +/// The art a [`BrushSearch`] looks through once: a symbol's or a pattern's, by name, or a +/// symbol's with one instance's appearance changes, by the symbol's name and the changes. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +enum SharedArt { + Symbol, + Pattern, + Dynamic, +} + +/// A search for the brushed strokes drawing an object draws, whose brush art can hold many more +/// objects than the stroked path. +struct BrushSearch<'a> { + doc: &'a Document, + /// The brushes the export draws strokes with ([`Exporter::brush_art`]); a stroke whose brush + /// is not among them is drawn as a plain stroke. + brushes: &'a [vectorcraft_brush::Brush], + /// The answer for each symbol's and pattern's art, and each symbol's art with an instance's + /// appearance changes, once found: false while it is being looked through, so that art that + /// uses itself ends the search. + seen: HashMap<(SharedArt, String), bool>, + /// The objects the search may still visit or copy: [`MAX_KNOCKOUT_MASK_OBJECTS`] at the start. + left: usize, + /// Whether the search stopped because [`Self::left`] ran out; it then answers true. + exhausted: bool, +} + +impl<'a> BrushSearch<'a> { + fn new(doc: &'a Document, brushes: &'a [vectorcraft_brush::Brush]) -> Self { + Self { doc, brushes, seen: HashMap::new(), left: MAX_KNOCKOUT_MASK_OBJECTS, exhausted: false } + } + + /// Takes `n` objects from [`Self::left`]; false, and the search exhausted, when fewer are left. + fn spend(&mut self, n: usize) -> bool { + match self.left.checked_sub(n) { + Some(left) if !self.exhausted => { + self.left = left; + true + } + _ => { + (self.left, self.exhausted) = (0, true); + false + } + } + } + + /// Takes the objects of `n` (its members and opacity-mask art included) from [`Self::left`], + /// counting no further than what is left; false when they don't fit. + fn spend_tree(&mut self, n: &Node) -> bool { + self.spend(1) + && n.extras().mask.as_deref().is_none_or(|m| self.spend_tree(&m.art)) + && n.children().into_iter().flatten().all(|c| self.spend_tree(c)) + } + + /// Whether drawing `n` draws a brushed stroke: on `n` or its visible members (a compound + /// path's members are drawn with its own paint), in the art of the opacity masks that are on + /// (`n`'s own when `own_mask`), of symbol instances and inline graphics, of live objects as + /// evaluated, and of the patterns that fills, strokes and type paint with. `depth` counts the + /// objects and the mask, symbol and pattern art above `n`: past + /// [`vectorcraft_doc::depth::MAX_DRAWN_DEPTH`] the search stops and counts as a brush, and so + /// it does when the objects it may visit or copy ([`Self::left`]) run out. + fn node(&mut self, n: &Node, own_mask: bool, depth: usize) -> bool { + if depth > vectorcraft_doc::depth::MAX_DRAWN_DEPTH || !self.spend(1) { + return true; + } + let depth = depth + 1; + // A fill is drawn when it is visible, and a stroke when it also paints and has a width + // (as `Exporter::shape` draws them). + for item in n.appearance.items.iter().filter(|i| i.visible()) { + let paint = match item { + AppearanceItem::Fill(f) => &f.paint, + AppearanceItem::Stroke(st) if st.paint.is_none() || st.width <= 0.0 => continue, + AppearanceItem::Stroke(st) => { + if st.brush.as_deref().is_some_and(|name| self.brushes.iter().any(|b| b.name == name)) { + return true; + } + &st.paint + } + }; + if self.paint(paint, depth) { + return true; + } + } + if own_mask && n.extras().mask.as_deref().is_some_and(|m| !m.disabled && self.node(&m.art, true, depth)) { + return true; + } + match &n.kind { + NodeKind::SymbolInstance { symbol, .. } => { + // A dynamic symbol's instance draws its art with its own appearance changes: the + // art is copied once for each set of changes, counted before it is copied. + let doc = self.doc; + match doc.symbols.iter().find(|s| &s.name == symbol) { + Some(s) if !n.extras().symbol_overrides.is_empty() => { + let key = format!("{symbol}\u{0}{:?}", n.extras().symbol_overrides); + self.shared(SharedArt::Dynamic, &key, |search| { + !search.spend_tree(&s.art) || search.node(&vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n), true, depth) + }) + } + _ => self.symbol(symbol, depth), + } + } + NodeKind::Text(t) => t.runs.iter().any(|r| { + let stroke = r.style.stroke_width > 0.0 && self.paint(&r.style.stroke, depth); + stroke || self.paint(&r.style.fill, depth) || r.inline.as_ref().is_some_and(|ig| self.symbol(&ig.symbol, depth)) + }), + NodeKind::Compound { .. } => false, + // Live objects are drawn as their evaluated art, as `Exporter::measured_node` draws them. + NodeKind::Blend { .. } + | NodeKind::Envelope { .. } + | NodeKind::Mesh(_) + | NodeKind::Repeat(_) + | NodeKind::PlacedDocument(_) + | NodeKind::Intertwine { .. } => { + // Each live object is reached once in the art searched (shared art is searched + // once); its evaluated art is counted before it is searched. + let g = vectorcraft_effects::expand_live_deep(Some(self.doc), n); + !self.spend_tree(&g) || g.children().into_iter().flatten().filter(|c| c.visible).any(|c| self.node(c, true, depth)) + } + _ => n.children().into_iter().flatten().filter(|c| c.visible).any(|c| self.node(c, true, depth)), + } + } + + /// Whether the art of the symbol named `name` draws a brushed stroke. + fn symbol(&mut self, name: &str, depth: usize) -> bool { + let doc = self.doc; + let Some(s) = doc.symbols.iter().find(|s| s.name == name) else { return false }; + self.shared(SharedArt::Symbol, name, |search| search.node(&s.art, true, depth)) + } + + /// Whether `paint` is a pattern whose tile art draws a brushed stroke. + fn paint(&mut self, paint: &Paint, depth: usize) -> bool { + let Paint::Pattern { pattern, .. } = paint else { return false }; + let doc = self.doc; + let Some(def) = doc.pattern(pattern) else { return false }; + self.shared(SharedArt::Pattern, pattern, |search| def.art.iter().filter(|c| c.visible).any(|c| search.node(c, true, depth))) + } + + /// The answer of `search` for the shared art `kind` named `name`, found once. + fn shared(&mut self, kind: SharedArt, name: &str, search: impl FnOnce(&mut Self) -> bool) -> bool { + let key = (kind, name.to_string()); + if let Some(&b) = self.seen.get(&key) { + return b; + } + self.seen.insert(key.clone(), false); + let b = search(self); + self.seen.insert(key, b); + b + } +} + +/// Whether `n` is drawn with one painting operation at most: a path or compound path with at most +/// one visible fill or stroke that paints, in a solid color, without effects, a brush or +/// arrowheads. +fn one_paint(n: &Node) -> bool { + let mut items = n.appearance.items.iter().filter(|i| i.visible() && !matches!(i.paint(), Paint::None)); + let simple = items.next().is_none_or(|i| { + let plain = match i { + AppearanceItem::Fill(_) => true, + AppearanceItem::Stroke(st) => st.brush.is_none() && st.start_arrow.is_none() && st.end_arrow.is_none(), + }; + plain && i.effects().is_empty() && matches!(i.paint(), Paint::Solid { .. }) + }); + matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) && n.appearance.effects.is_empty() && simple && items.next().is_none() +} + +/// Whether element `c` of a knockout group knocks out all of the area it paints, as an element of +/// a PDF knockout group does: its opacity and opacity mask are full or don't define its knockout +/// shape, and every part of it paints opaquely. +fn shape_is_coverage(c: &Node) -> bool { + let own = !c.knockout_shape || (c.opacity >= 1.0 && c.extras().mask.as_deref().is_none_or(|m| m.disabled)); + own && paints_opaque(c) +} + +/// Whether what `n` paints is opaque wherever it paints, at its own opacity and mask: its visible +/// fills and strokes are solid colors or gradients with opaque stops, without effects or brushes, +/// and so are the members of a group or compound path, at full opacity without an opacity mask. +fn paints_opaque(n: &Node) -> bool { + let paint = |p: &Paint| match p { + Paint::None | Paint::Solid { .. } => true, + Paint::Gradient(g) => g.gradient.stops.iter().all(|s| s.opacity >= 1.0), + _ => false, + }; + let items = n.appearance.items.iter().all(|i| { + let brush = matches!(i, AppearanceItem::Stroke(s) if s.brush.is_some()); + !i.visible() || (i.opacity() >= 1.0 && i.effects().is_empty() && !brush && paint(i.paint())) + }); + let member = + |c: &std::sync::Arc| !c.visible || (c.opacity >= 1.0 && c.extras().mask.as_deref().is_none_or(|m| m.disabled) && paints_opaque(c)); + n.appearance.effects.is_empty() + && items + && match &n.kind { + NodeKind::Path { .. } => true, + NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } | NodeKind::Compound { children, .. } => children.iter().all(member), + NodeKind::Mesh(m) => m.points.iter().all(|p| p.opacity >= 1.0), + _ => false, + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use vectorcraft_doc::{Appearance, NodeId, Symbol, SymbolOverride}; + use vectorcraft_geom::shapes; + + use super::*; + + /// An instance of a dynamic symbol whose art holds an instance of the same symbol with the same + /// appearance changes ends the search for brushed strokes where the art comes round again, + /// after a few objects, instead of recursing until the stack runs out. + #[test] + fn a_dynamic_symbol_holding_itself_ends_the_brush_search() { + let mut d = Document::new(200.0, 200.0); + let instance = || { + let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Loop".into(), xf: Affine::translate((40.0, 0.0)) }); + n.extras_mut().symbol_overrides = vec![SymbolOverride { path: vec![0], opacity: Some(0.5), ..Default::default() }]; + n + }; + let square = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::basic(Paint::None, Paint::None, 0.0)); + let art = Node::group(NodeId(0), vec![Arc::new(square), Arc::new(instance())]); + d.symbols.push(Symbol { dynamic: true, name: "Loop".into(), art: Arc::new(art) }); + let mut search = BrushSearch::new(&d, &[]); + assert!(!search.node(&instance(), true, 0)); + assert!(!search.exhausted); + assert!(MAX_KNOCKOUT_MASK_OBJECTS - search.left < 10, "{} objects", MAX_KNOCKOUT_MASK_OBJECTS - search.left); + } + + /// A group of 1,000 instances of a dynamic symbol whose art holds 100 squares, each instance + /// with appearance changes of its own: copying and looking through every instance's art would + /// take about 203,000 objects, and the search stops when [`MAX_KNOCKOUT_MASK_OBJECTS`] are + /// used, counting as a brush. With the same changes on every instance, the art is copied + /// once. + #[test] + fn the_brush_search_stops_within_its_object_limit() { + let mut d = Document::new(200.0, 200.0); + let square = + Arc::new(Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), Appearance::basic(Paint::None, Paint::None, 0.0))); + d.symbols.push(Symbol { dynamic: true, name: "Dots".into(), art: Arc::new(Node::group(NodeId(0), vec![square; 100])) }); + let group = |distinct: bool| { + let instances = (0..1000u32) + .map(|i| { + let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Dots".into(), xf: Affine::IDENTITY }); + let path = if distinct { vec![0, i] } else { vec![0] }; + n.extras_mut().symbol_overrides = vec![SymbolOverride { path, opacity: Some(0.5), ..Default::default() }]; + Arc::new(n) + }) + .collect(); + Node::group(NodeId(0), instances) + }; + let mut search = BrushSearch::new(&d, &[]); + assert!(search.node(&group(true), true, 0)); + assert!(search.exhausted); + assert_eq!(search.left, 0); + let mut search = BrushSearch::new(&d, &[]); + assert!(!search.node(&group(false), true, 0)); + assert!(!search.exhausted); + // The group, the 1,000 instances, and one copy of the art counted and looked through. + assert_eq!(MAX_KNOCKOUT_MASK_OBJECTS - search.left, 1 + 1000 + 101 + 101); + } + + /// A placed file read as one square with a 3 pt brushed stroke. + fn brushed_art() -> (Document, Node, Rect) { + let mut d = Document::new(40.0, 20.0); + d.layers.clear(); + let mut st = StrokeLayer::new(Paint::solid(vectorcraft_color::Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + let mut square = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 20.0)), Appearance::basic(Paint::None, Paint::None, 0.0)); + square.appearance.items.push(AppearanceItem::Stroke(st)); + let art = Node::group(NodeId(0), vec![Arc::new(square)]); + (d, art, Rect::new(0.0, 0.0, 40.0, 20.0)) + } + + /// A placed document is drawn as the art read from its file, and the brushed strokes in that + /// art count. + #[test] + fn brushed_strokes_in_a_placed_documents_art_count() { + let d = Document::new(200.0, 200.0); + let placed = vectorcraft_doc::PlacedDocument { + link: vectorcraft_doc::LinkInfo::new("/art/placed.vectorcraft"), + key: "placed-1".into(), + bounding: false, + page_box: vectorcraft_doc::placed_document::PageBox::Crop, + width: 40.0, + height: 20.0, + xf: Affine::IDENTITY, + placement: Default::default(), + }; + // The art the export reads for it before drawing (`Document::with_placed_art`). + vectorcraft_doc::placed_document::prime(&placed, Arc::new(vec![]), brushed_art()); + let n = Node::new(NodeId(0), NodeKind::PlacedDocument(Box::new(placed))); + let brushes = vectorcraft_brush::library(&d); + assert!(BrushSearch::new(&d, &brushes).node(&n, true, 0)); + } +} diff --git a/crates/pdf/src/import/builder.rs b/crates/pdf/src/import/builder.rs index 4a979e8742..0b32054c7b 100644 --- a/crates/pdf/src/import/builder.rs +++ b/crates/pdf/src/import/builder.rs @@ -387,6 +387,21 @@ impl<'p> Builder<'p> { only = fold_stop_opacity(&only, &m).unwrap_or_else(|| only.with_mask(Some(Box::new(m)))); } Some(only) + } else if let [only] = children.as_slice() + && matches!(only.kind, NodeKind::Group { clip: false, .. }) + && only.knockout == Knockout::On + && only.opacity >= 0.999 + && only.blend == BlendMode::Normal + && !only.isolate + && only.extras().mask.is_none() + && mask.is_none() + && !knockout + { + // A knockout group drawn in a form that carries its opacity, blend mode or + // isolation: one knockout group with them. + let mut only = (**only).clone(); + (only.opacity, only.blend, only.isolate) = (opacity, blend, isolate); + Some(only) } else { let mut g = Node::group(self.id(), children); (g.opacity, g.blend, g.isolate) = (opacity, blend, isolate); diff --git a/crates/pdf/src/surface.rs b/crates/pdf/src/surface.rs index 4229781cdd..b20b9667ab 100644 --- a/crates/pdf/src/surface.rs +++ b/crates/pdf/src/surface.rs @@ -81,6 +81,11 @@ impl<'a> Surface<'a> { self.group(|s| s.push_isolated()); } + /// A transparency group with `options` (isolated, knockout), as [`Self::push_isolated`]. + pub fn push_group(&mut self, options: krilla::blend::GroupOptions) { + self.group(|s| s.push_group(options)); + } + /// The mask's stream is already in [`Self::group_transform`]'s space, like the group's art. pub fn push_mask(&mut self, mask: Mask) { self.group(|s| s.push_mask(mask)); diff --git a/crates/pdf/src/tests/blend.rs b/crates/pdf/src/tests/blend.rs index 346b4f34d9..c55a0b7504 100644 --- a/crates/pdf/src/tests/blend.rs +++ b/crates/pdf/src/tests/blend.rs @@ -97,3 +97,466 @@ fn a_knockout_group_with_blending_elements_is_non_isolated_unless_isolated() { assert_eq!(text(&d).contains("/Itrue"), isolate, "isolate {isolate}"); } } + +/// A knockout group of `n` red, green and blue squares at 50% opacity that all overlap, over an +/// opaque backdrop. `knockout_shape`: their opacity defines their knockout shape. +fn knockout_squares(n: usize, knockout_shape: bool, isolate: bool) -> Document { + let mut d = Document::new(200.0, 200.0); + add(&mut d, rect(Rect::new(0.0, 0.0, 200.0, 200.0), (0.9, 0.8, 0.2))); + let colors = [(1.0, 0.0, 0.0), (0.0, 0.8, 0.0), (0.0, 0.0, 1.0)]; + let squares = (0..n) + .map(|i| { + let x = (i % 20) as f64 * 2.0; + let mut s = rect(Rect::new(20.0 + x, 50.0, 120.0 + x, 150.0), colors[i % 3]); + (s.id, s.opacity, s.knockout_shape) = (d.alloc_id(), 0.5, knockout_shape); + std::sync::Arc::new(s) + }) + .collect(); + let mut g = Node::group(NodeId(0), squares); + (g.knockout, g.isolate) = (vectorcraft_doc::Knockout::On, isolate); + add(&mut d, g); + d +} + +/// How many objects each knockout group of `d` holds. +fn knockout_groups(d: &Document) -> Vec { + let mut out = vec![]; + d.walk(|n| { + if n.knockout == vectorcraft_doc::Knockout::On { + out.push(n.children().map_or(0, |c| c.len())); + } + }); + out +} + +/// A knockout group whose elements knock out the area they paint is written as one PDF knockout +/// group (`/K true`), whatever the number of elements, and reads back as a knockout group that +/// draws as the original does. +#[test] +fn a_knockout_group_is_written_as_one_pdf_knockout_group() { + for n in [0, 1, 2, 49, 50, 200] { + let d = knockout_squares(n, false, false); + let t = text(&d); + assert_eq!(t.matches("/Ktrue").count(), usize::from(n > 1), "{n}"); + let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap_or_else(|e| panic!("{n}: {e}")); + assert_eq!(knockout_groups(&back), if n > 1 { vec![n] } else { vec![] }, "{n}"); + if n == 200 { + vectorcraft_testkit::raster::assert_similar( + &vectorcraft_testkit::raster::render_artboard(&back), + &vectorcraft_testkit::raster::render_artboard(&d), + 4.0, + 0.01, + ); + } + } +} + +/// The knockout group of an isolated group is isolated too (`/I true` beside `/K true`), and the +/// group reads back as isolated and knocking out. (Each translucent element is a group of its own.) +#[test] +fn an_isolated_knockout_group_is_one_isolated_pdf_knockout_group() { + for isolate in [false, true] { + let mut d = knockout_squares(3, false, isolate); + // Blending inside, which isolation keeps from the backdrop. + let g = d.layers[0].children().unwrap()[1].id; + let top = d.node(g).unwrap().children().unwrap()[2].id; + d.node_mut(top).unwrap().blend = BlendMode::Multiply; + let t = text(&d); + assert_eq!(t.matches("/Ktrue").count(), 1, "isolate {isolate}"); + assert_eq!(t.contains("/Itrue/Ktrue"), isolate, "isolate {isolate}"); + let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap(); + let mut found = vec![]; + back.walk(|n| { + if n.knockout == vectorcraft_doc::Knockout::On { + found.push(n.isolate); + } + }); + assert_eq!(found, [isolate]); + } +} + +/// A translucent knockout group is written as a form with its opacity around the `/K` group, and +/// reads back as one knockout group with that opacity and its isolation. +#[test] +fn a_translucent_knockout_group_reads_back_as_one_group() { + for isolate in [false, true] { + let mut d = knockout_squares(3, false, isolate); + let g = d.layers[0].children().unwrap()[1].id; + d.node_mut(g).unwrap().opacity = 0.5; + let top = d.node(g).unwrap().children().unwrap()[2].id; + d.node_mut(top).unwrap().blend = BlendMode::Multiply; + let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap(); + let mut found = vec![]; + back.walk(|n| { + if matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. }) { + found.push(( + n.knockout == vectorcraft_doc::Knockout::On, + n.isolate, + (n.opacity * 100.0).round(), + n.children().map_or(0, |c| c.len()), + )); + } + }); + assert_eq!(found, [(true, isolate, 50.0, 3)], "isolate {isolate}"); + } +} + +/// Elements whose opacity defines their knockout shape knock out less than a PDF knockout group +/// knocks out: each is drawn through one soft mask of the elements above it, so that 200 of them +/// still read back, every one of them. +#[test] +fn elements_whose_opacity_defines_their_knockout_shape_are_drawn_through_one_mask_each() { + let d = knockout_squares(200, true, false); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert!(!r.warnings.iter().any(|w| w.contains("knockout")), "{:?}", r.warnings); + assert!(!text(&d).contains("/Ktrue")); + let back = import(&r.bytes).unwrap(); + let mut paths = 0; + back.walk(|n| paths += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::Path { .. }))); + // The backdrop and the 200 squares (the masks' copies of them are opacity-mask art). + assert_eq!(paths, 201); +} + +/// When the masks would draw more than [`crate::export::MAX_KNOCKOUT_MASK_OBJECTS`] objects, the +/// group is written as a PDF knockout group, with a warning, and reads back as a knockout group. +#[test] +fn a_knockout_group_too_large_to_mask_is_a_pdf_knockout_group_with_a_warning() { + // 400 elements that all overlap: 79,800 objects in their masks. + const { assert!(400 * 399 / 2 > crate::export::MAX_KNOCKOUT_MASK_OBJECTS) }; + let d = knockout_squares(400, true, false); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert!(r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_SHAPES), "{:?}", r.warnings); + assert_eq!(text(&d).matches("/Ktrue").count(), 1); + assert_eq!(knockout_groups(&import(&r.bytes).unwrap()), [400]); +} + +/// In a PDF knockout group each painting operation knocks out the ones before it: an element with +/// two fills, the second in Multiply, is drawn as one group (through a full alpha mask), so that +/// its second fill blends with its first. An element with one fill needs no group. +#[test] +fn an_element_with_several_paints_is_one_group_in_a_knockout_group() { + for (fills, groups) in [(1, 0), (2, 1)] { + let mut d = Document::new(200.0, 200.0); + let below = rect(Rect::new(0.0, 0.0, 100.0, 100.0), (0.0, 0.0, 1.0)); + let mut top = rect(Rect::new(50.0, 50.0, 150.0, 150.0), (1.0, 0.0, 0.0)); + if fills == 2 { + let mut multiply = vectorcraft_doc::FillLayer::new(Paint::solid(Color::rgb(0.0, 0.0, 1.0))); + multiply.blend = BlendMode::Multiply; + top.appearance.items.push(vectorcraft_doc::AppearanceItem::Fill(multiply)); + } + top.id = d.alloc_id(); + let below = Node { id: d.alloc_id(), ..below }; + let mut g = Node::group(NodeId(0), vec![std::sync::Arc::new(below), std::sync::Arc::new(top)]); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let t = text(&d); + assert_eq!(t.matches("/Ktrue").count(), 1, "{fills}"); + let Some(start) = t.find("/Ktrue") else { panic!("no knockout group") }; + let group = &t[start..]; + let Some(end) = group.find("endstream") else { panic!("no knockout stream") }; + assert_eq!(group[..end].matches("Do").count(), groups, "{fills}: {t}"); + } +} + +/// Elements that don't overlap need no masks of each other. +#[test] +fn knockout_masks_hold_only_the_elements_that_overlap() { + let mut d = Document::new(200.0, 100.0); + let squares = (0..3) + .map(|i| { + let x = f64::from(i) * 60.0; + let mut s = rect(Rect::new(x, 0.0, x + 40.0, 40.0), (0.0, 0.0, 1.0)); + (s.id, s.opacity, s.knockout_shape) = (d.alloc_id(), 0.5, true); + std::sync::Arc::new(s) + }) + .collect(); + let mut g = Node::group(NodeId(0), squares); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let t = text(&d); + assert!(!t.contains("/Luminosity") && !t.contains("/Ktrue"), "{t}"); +} + +/// The copies of symbol instances drawn in knockout masks don't count toward the objects the +/// visible symbol art of an export may hold (`MAX_SYMBOL_NODES`): of two overlapping 50% instances +/// whose art placed through 9-slice guides holds about 6,510,000 objects each (a gradient square +/// in all nine cells, each copy with a disabled opacity mask of 723,351 objects), both are written +/// in their nine cells, although the lower one's mask draws a copy of the upper one first. +#[test] +fn knockout_mask_copies_of_symbol_instances_leave_the_visible_art_its_object_limit() { + let d = panel_instances(850, &[20.0, 40.0]); + // Nine squares of each instance (the mask's copy of the upper one is opacity-mask art). + assert_eq!(painted_paths(&export(&d, &PdfOptions::default()).unwrap()), 18); +} + +/// A knockout group of overlapping 50% instances, at `xs`, of a symbol whose art placed through +/// 9-slice guides holds 9 × (`mask_objects`² + `mask_objects` + 3) objects (a gradient square in +/// all nine cells, each copy with a disabled opacity mask of `mask_objects`² + `mask_objects` + 1 +/// objects). +fn panel_instances(mask_objects: usize, xs: &[f64]) -> Document { + use vectorcraft_color::{Gradient, GradientPaint}; + use vectorcraft_geom::Affine; + let mut d = Document::new(300.0, 200.0); + let leaf = std::sync::Arc::new(rect(Rect::new(0.0, 0.0, 1.0, 1.0), (0.0, 0.0, 0.0))); + let inner = std::sync::Arc::new(Node::group(NodeId(0), vec![leaf; mask_objects])); + let mut mask = vectorcraft_doc::OpacityMask::new(Node::group(NodeId(0), vec![inner; mask_objects]), true); + (mask.linked, mask.disabled) = (false, true); + let gradient = Paint::Gradient(Box::new(GradientPaint::new(Gradient::default()))); + let mut square = Node::path(NodeId(0), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::basic(gradient, Paint::None, 0.0)); + square.extras_mut().mask = Some(Box::new(mask)); + let art = Node::group(NodeId(0), vec![std::sync::Arc::new(square)]); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Panel".into(), art: std::sync::Arc::new(art) }); + d.set_symbol_nine_slice("Panel", Some(vectorcraft_doc::SymbolNineSlice::default())); + let instances = xs + .iter() + .map(|&x| { + let xf = Affine::translate((x, 20.0)) * Affine::scale_non_uniform(2.0, 1.5); + let mut n = Node::new(NodeId(0), vectorcraft_doc::NodeKind::SymbolInstance { symbol: "Panel".into(), xf }); + (n.id, n.opacity, n.knockout_shape) = (d.alloc_id(), 0.5, true); + std::sync::Arc::new(n) + }) + .collect(); + let mut g = Node::group(NodeId(0), instances); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + d +} + +/// The paths of `pdf` read back, the art of opacity masks left out. +fn painted_paths(pdf: &[u8]) -> usize { + let mut paths = 0; + import(pdf).unwrap().walk(|n| paths += usize::from(matches!(n.kind, vectorcraft_doc::NodeKind::Path { .. }))); + paths +} + +/// When the copies the knockout masks draw would hold more symbol objects than they may +/// (`MAX_SYMBOL_NODES` of their own), the group is written as a PDF knockout group, with a +/// warning, instead of masks that leave out art their elements cover: four overlapping instances +/// of about 3,870,000 objects each fit the visible art's limit, and the six copies in their masks +/// do not. +#[test] +fn knockout_masks_whose_symbol_copies_run_out_give_way_to_a_pdf_knockout_group() { + let d = panel_instances(655, &[20.0, 40.0, 60.0, 80.0]); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert!(r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_SHAPES), "{:?}", r.warnings); + assert_eq!(painted_paths(&r.bytes), 36); +} + +/// A brushed stroke draws brush art that the objects counted for the knockout masks leave out: +/// a group in which a mask would draw an element with a brushed stroke is written as a PDF +/// knockout group, with a warning. A brushed element that no mask draws (the lowest one) leaves +/// the masks as they are. +#[test] +fn a_knockout_group_whose_masks_would_draw_a_brushed_stroke_is_a_pdf_knockout_group() { + for (brushed, expected) in [(1, true), (0, false)] { + let mut d = Document::new(200.0, 200.0); + let squares = (0..2) + .map(|i| { + let x = f64::from(i) * 40.0; + let mut s = rect(Rect::new(20.0 + x, 50.0, 120.0 + x, 150.0), (0.0, 0.0, 1.0)); + (s.id, s.opacity, s.knockout_shape) = (d.alloc_id(), 0.5, true); + if i == brushed { + let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + s.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(st)); + } + std::sync::Arc::new(s) + }) + .collect(); + let mut g = Node::group(NodeId(0), squares); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + assert_eq!(r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_BRUSHES), expected, "{brushed}: {:?}", r.warnings); + assert!(!r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_SHAPES), "{brushed}: {:?}", r.warnings); + assert_eq!(text(&d).matches("/Ktrue").count(), usize::from(expected), "{brushed}"); + } +} + +/// The knockout group of `upper` at 50%, whose opacity defines its knockout shape when +/// `knockout_shape`, between two 50% squares whose opacity defines theirs (so that the group is +/// drawn through masks): whether the export gives the brushed-stroke warning. +fn warns_of_brushes(mut d: Document, mut upper: Node, knockout_shape: bool) -> bool { + (upper.id, upper.opacity, upper.knockout_shape) = (d.alloc_id(), 0.5, knockout_shape); + let mut lower = rect(Rect::new(20.0, 50.0, 120.0, 150.0), (1.0, 0.0, 0.0)); + (lower.id, lower.opacity, lower.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut top = rect(Rect::new(100.0, 50.0, 180.0, 150.0), (0.0, 0.8, 0.0)); + (top.id, top.opacity, top.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut g = Node::group(NodeId(0), [lower, upper, top].map(std::sync::Arc::new).to_vec()); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_BRUSHES) +} + +/// A square at (60, 50) with a 3 pt brushed stroke. +fn brushed_square() -> Node { + let mut line = rect(Rect::new(60.0, 50.0, 160.0, 150.0), (1.0, 1.0, 1.0)); + let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + line.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(st)); + line +} + +/// Only brushed strokes a mask draws count: a hidden member's is not drawn, nor is one without +/// paint or width, nor the one in the opacity mask of an element whose mask does not define its +/// knockout shape (its copy in a mask leaves the opacity mask out). +#[test] +fn brushed_strokes_that_a_mask_does_not_draw_leave_the_masks_as_they_are() { + let d = Document::new(200.0, 200.0); + let mut hidden = brushed_square(); + hidden.visible = false; + let plain = rect(Rect::new(60.0, 50.0, 160.0, 150.0), (0.0, 0.0, 1.0)); + let group = Node::group(NodeId(0), vec![std::sync::Arc::new(plain.clone()), std::sync::Arc::new(hidden)]); + assert!(!warns_of_brushes(d.clone(), group, true)); + // A brushed stroke without paint or width draws nothing. + for (paint, width) in [(Paint::None, 3.0), (Paint::solid(Color::BLACK), 0.0)] { + let mut idle = brushed_square(); + if let Some(vectorcraft_doc::AppearanceItem::Stroke(st)) = idle.appearance.items.last_mut() { + (st.paint, st.width) = (paint, width); + } + assert!(!warns_of_brushes(d.clone(), idle, true), "width {width}"); + } + assert!(warns_of_brushes(d.clone(), brushed_square(), true)); + // A group's own brushed stroke is drawn too: the export bakes it into a path among the + // group's members. + let mut stroked_group = Node::group(NodeId(0), vec![std::sync::Arc::new(plain.clone())]); + stroked_group.appearance = brushed_square().appearance; + assert!(warns_of_brushes(d.clone(), stroked_group, true)); + for (knockout_shape, expected) in [(false, false), (true, true)] { + let mut masked = plain.clone(); + let mut mask = vectorcraft_doc::OpacityMask::new(brushed_square(), true); + mask.linked = false; + masked.extras_mut().mask = Some(Box::new(mask)); + assert_eq!(warns_of_brushes(d.clone(), masked, knockout_shape), expected, "knockout shape {knockout_shape}"); + } +} + +/// A compound path's members are drawn with its own paint, so a brushed stroke on a member draws +/// nothing; one on the compound path does. +#[test] +fn a_brushed_stroke_counts_on_a_compound_path_and_not_on_its_members() { + let d = Document::new(200.0, 200.0); + let compound = |members: Vec| { + let children = members.into_iter().map(std::sync::Arc::new).collect(); + let mut c = Node::new(NodeId(0), vectorcraft_doc::NodeKind::Compound { children, rule: Default::default() }); + c.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0).into(); + c + }; + assert!(!warns_of_brushes(d.clone(), compound(vec![brushed_square()]), true)); + let mut stroked = compound(vec![rect(Rect::new(60.0, 50.0, 160.0, 150.0), (1.0, 1.0, 1.0))]); + stroked.appearance = brushed_square().appearance; + assert!(warns_of_brushes(d, stroked, true)); +} + +/// A pattern's tiles draw the brushed strokes in its art: an element filled with such a pattern +/// counts, and one filled with a pattern without them does not. +#[test] +fn a_brushed_stroke_in_a_pattern_counts() { + for (brushed_tile, expected) in [(true, true), (false, false)] { + let mut d = Document::new(200.0, 200.0); + let tile = if brushed_tile { brushed_square() } else { rect(Rect::new(60.0, 50.0, 160.0, 150.0), (1.0, 1.0, 1.0)) }; + d.patterns.push(vectorcraft_doc::pattern::PatternDef::new("Tile", vec![std::sync::Arc::new(tile)])); + let pattern = Paint::Pattern { pattern: "Tile".into(), xf: vectorcraft_geom::Affine::IDENTITY }; + let filled = Node::path(NodeId(0), shapes::rectangle(Rect::new(60.0, 50.0, 160.0, 150.0)), Appearance::basic(pattern, Paint::None, 0.0)); + assert_eq!(warns_of_brushes(d, filled, true), expected, "brushed tile {brushed_tile}"); + } +} + +/// A stroke whose brush the export does not know is drawn as a plain stroke, and a stroke without +/// width draws nothing, not even its pattern's brushed strokes. +#[test] +fn strokes_that_draw_no_brush_art_leave_the_masks_as_they_are() { + let mut unknown = brushed_square(); + if let Some(vectorcraft_doc::AppearanceItem::Stroke(st)) = unknown.appearance.items.last_mut() { + st.brush = Some("No Such Brush".into()); + } + assert!(!warns_of_brushes(Document::new(200.0, 200.0), unknown, true)); + let mut d = Document::new(200.0, 200.0); + d.patterns.push(vectorcraft_doc::pattern::PatternDef::new("Tile", vec![std::sync::Arc::new(brushed_square())])); + let pattern = Paint::Pattern { pattern: "Tile".into(), xf: vectorcraft_geom::Affine::IDENTITY }; + let mut thin = rect(Rect::new(60.0, 50.0, 160.0, 150.0), (0.0, 0.0, 1.0)); + thin.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(vectorcraft_doc::StrokeLayer::new(pattern, 0.0))); + assert!(!warns_of_brushes(d, thin, true)); +} + +/// Type draws the brushed strokes in the patterns its characters are filled with and in the art +/// of its inline graphics. +#[test] +fn brushed_strokes_in_type_patterns_and_inline_graphics_count() { + let text = |d: &mut Document, fill: Paint, inline: bool| { + let style = vectorcraft_doc::CharStyle { size: 60.0, fill, ..Default::default() }; + let mut t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(60.0, 120.0), "Type", style.clone()); + if inline { + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Brushed".into(), art: std::sync::Arc::new(brushed_square()) }); + let mut run = vectorcraft_doc::TextRun::new("\u{FFFC}", style); + run.inline = Some(vectorcraft_doc::InlineArt::new("Brushed")); + t.runs.push(run); + } + Node::new(NodeId(0), vectorcraft_doc::NodeKind::Text(Box::new(t))) + }; + let black = Paint::solid(Color::BLACK); + let mut d = Document::new(200.0, 200.0); + let plain = text(&mut d, black.clone(), false); + assert!(!warns_of_brushes(d.clone(), plain, true)); + d.patterns.push(vectorcraft_doc::pattern::PatternDef::new("Tile", vec![std::sync::Arc::new(brushed_square())])); + let patterned = text(&mut d, Paint::Pattern { pattern: "Tile".into(), xf: vectorcraft_geom::Affine::IDENTITY }, false); + assert!(warns_of_brushes(d, patterned, true)); + let mut d = Document::new(200.0, 200.0); + let inline = text(&mut d, black, true); + assert!(warns_of_brushes(d, inline, true)); +} + +/// Type draws the brushed strokes of its appearance as brush art too. +#[test] +fn a_brushed_stroke_on_type_counts() { + let style = vectorcraft_doc::CharStyle { size: 60.0, ..Default::default() }; + let text = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(60.0, 120.0), "Type", style); + let mut n = Node::new(NodeId(0), vectorcraft_doc::NodeKind::Text(Box::new(text))); + n.appearance = brushed_square().appearance; + assert!(warns_of_brushes(Document::new(200.0, 200.0), n, true)); +} + +/// The brushed strokes a mask would draw are found in the opacity-mask art of a symbol's art too, +/// and a disabled opacity mask, which is not drawn, holds none. +#[test] +fn brushed_strokes_in_mask_art_count_only_where_the_mask_is_on() { + for (in_symbol, disabled, expected) in [(true, false, true), (false, true, false), (false, false, true)] { + let mut d = Document::new(200.0, 200.0); + let mut line = rect(Rect::new(0.0, 0.0, 100.0, 100.0), (1.0, 1.0, 1.0)); + let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 3.0); + st.brush = Some("3 pt. Round".into()); + line.appearance.items.push(vectorcraft_doc::AppearanceItem::Stroke(st)); + let mut mask = vectorcraft_doc::OpacityMask::new(line, true); + (mask.linked, mask.disabled) = (false, disabled); + let mut masked = rect(Rect::new(0.0, 0.0, 100.0, 100.0), (0.0, 0.0, 1.0)); + masked.extras_mut().mask = Some(Box::new(mask)); + let mut upper = if in_symbol { + let art = Node::group(NodeId(0), vec![std::sync::Arc::new(masked)]); + d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Masked".into(), art: std::sync::Arc::new(art) }); + let xf = vectorcraft_geom::Affine::translate((60.0, 50.0)); + Node::new(NodeId(0), vectorcraft_doc::NodeKind::SymbolInstance { symbol: "Masked".into(), xf }) + } else { + masked.transform(vectorcraft_geom::Affine::translate((60.0, 50.0)), false); + masked + }; + (upper.id, upper.opacity, upper.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut lower = rect(Rect::new(20.0, 50.0, 120.0, 150.0), (1.0, 0.0, 0.0)); + (lower.id, lower.opacity, lower.knockout_shape) = (d.alloc_id(), 0.5, true); + let mut g = Node::group(NodeId(0), vec![std::sync::Arc::new(lower), std::sync::Arc::new(upper)]); + g.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, g); + let r = export_with_report(&d, &PdfOptions::default()).unwrap(); + let warned = r.warnings.iter().any(|w| w == crate::export::KNOCKOUT_BRUSHES); + assert_eq!(warned, expected, "symbol {in_symbol}, disabled {disabled}: {:?}", r.warnings); + } +} + +/// PDF/A-2 allows transparency: a knockout group is written as one there too. +#[test] +fn a_pdf_a_2b_file_keeps_a_knockout_group() { + let mut o = PdfOptions::default(); + (o.settings.standard, o.settings.compatibility) = (Standard::PdfA2b, Standard::PdfA2b.version()); + let bytes = export(&knockout_squares(3, false, false), &o).unwrap(); + assert_eq!(knockout_groups(&import(&bytes).unwrap()), [3]); +} diff --git a/crates/pdf/tests/gradient_forms.rs b/crates/pdf/tests/gradient_forms.rs index 9f06c9b844..7373ec8494 100644 --- a/crates/pdf/tests/gradient_forms.rs +++ b/crates/pdf/tests/gradient_forms.rs @@ -91,9 +91,7 @@ fn fixture(form: Form, kind: GradientKind, mode: ColorMode, origin: Point) -> (D /// Round trips preserve gradient placement for the form types the importer supports. #[test] fn gradients_in_forms_keep_their_position_when_read_back() { - // The importer has a pre-existing mismatch on the knockout's synthesized nested masks; - // Quartz below checks its actual PDF output, along with the other forms. - for form in FORMS.into_iter().filter(|f| !matches!(f, Form::Knockout)) { + for form in FORMS { for kind in [GradientKind::Linear, GradientKind::Radial] { for origin in [Point::ORIGIN, Point::new(125.0, -80.0)] { let (d, opts) = fixture(form, kind, ColorMode::Rgb, origin); diff --git a/crates/render/src/fx/bounds.rs b/crates/render/src/fx/bounds.rs index 13a6dad415..ce76713e74 100644 --- a/crates/render/src/fx/bounds.rs +++ b/crates/render/src/fx/bounds.rs @@ -356,7 +356,7 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { }; let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_ref()); let b = match &reshaped { - Some(r) if matches!(n.kind, NodeKind::SymbolInstance { .. }) && effects::has_item_geometry(n) => instance_piece_bounds(symbols, r), + Some(r) if has_instance_row_effects(n) => instance_piece_bounds(symbols, r), Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => { r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, painted_reach(symbols, c))) } @@ -379,9 +379,16 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { } } +/// Is `n` a symbol instance whose active fill or stroke has geometry or raster effects that the +/// fills and strokes of its art take ([`effects::has_item_geometry`], +/// [`effects::has_instance_row_raster`])? +fn has_instance_row_effects(n: &Node) -> bool { + matches!(n.kind, NodeKind::SymbolInstance { .. }) && (effects::has_item_geometry(n) || effects::has_instance_row_raster(n)) +} + /// The bounds of `art`, the art [`effects::reshape`] made of a symbol instance whose active fill or -/// stroke has geometry effects ([`effects::has_item_geometry`]): its paths measured with the -/// geometry effects of their fills and strokes ([`visual_bounds`]), the symbol instances in it that +/// stroke has geometry or raster effects ([`has_instance_row_effects`]): its paths measured with the +/// effects of their fills and strokes, shadows and glows included ([`visual_bounds`]), the symbol instances in it that /// took those effects by their own reshaped art in turn, its groups and layers with their own fills /// and strokes by the art those paint ([`effects::evaluate_container`]), its clip groups by their /// clipping paths and other objects as [`Node::visual_bounds_with`] measures them. Hidden objects are left out. @@ -409,7 +416,7 @@ fn pieces_within(symbols: &SymbolBoxes, art: &Node, depth: usize, budget: &mut u g.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, pieces_within(symbols, c, depth, budget))) } NodeKind::Path { .. } | NodeKind::Compound { .. } if has_fx(art) => visual_bounds(symbols, art), - NodeKind::SymbolInstance { symbol, .. } if depth < vectorcraft_doc::depth::MAX_DEPTH && effects::has_item_geometry(art) => { + NodeKind::SymbolInstance { symbol, .. } if depth < vectorcraft_doc::depth::MAX_DEPTH && has_instance_row_effects(art) => { let doc = symbols.document(); let reshaped = doc.symbols.iter().find(|s| s.name == *symbol).and_then(|s| { let count = s.art.count() as u64; @@ -639,7 +646,7 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool, brush: b }; let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_ref()); let b = match &reshaped { - Some(r) if matches!(n.kind, NodeKind::SymbolInstance { .. }) && effects::has_item_geometry(n) => instance_piece_bounds(symbols, r), + Some(r) if has_instance_row_effects(n) => instance_piece_bounds(symbols, r), Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => { r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c))) } diff --git a/crates/render/src/fx/predicates.rs b/crates/render/src/fx/predicates.rs index 4f14cadfd1..e4e479b2e5 100644 --- a/crates/render/src/fx/predicates.rs +++ b/crates/render/src/fx/predicates.rs @@ -20,12 +20,14 @@ pub(crate) fn has_fx(n: &Node) -> bool { /// Does `n` carry visible object-level effects that only apply through its art (type, images, /// symbol instances, live objects; Crop Marks and colour adjustments on anything), is it type with /// a geometry effect on a fill or stroke of its own or an image with a fill or stroke of its own -/// that paints a shape ([`effects::has_item_geometry`]), is it a path whose Transform effect copies -/// it with a gradient or changes its fills and strokes ([`effects::has_transform_art`]), or is it a -/// group or layer with an appearance of its own (see [`Renderer::draw_object_fx`])? +/// that paints a shape ([`effects::has_item_geometry`]), a symbol instance whose active fill or +/// stroke has a raster effect ([`effects::has_instance_row_raster`]), is it a path whose Transform +/// effect copies it with a gradient or changes its fills and strokes ([`effects::has_transform_art`]), +/// or is it a group or layer with an appearance of its own (see [`Renderer::draw_object_fx`])? pub(crate) fn has_object_fx(n: &Node) -> bool { (effects::needs_outline(n) && (visible(&n.appearance.effects) || n.projection().is_some())) || effects::has_item_geometry(n) + || effects::has_instance_row_raster(n) || effects::has_transform_art(n) || effects::has_container_appearance(n) || effects::has_crop_marks(n) diff --git a/crates/svg/src/export/nodes.rs b/crates/svg/src/export/nodes.rs index a4cdf26271..8716af7367 100644 --- a/crates/svg/src/export/nodes.rs +++ b/crates/svg/src/export/nodes.rs @@ -163,6 +163,18 @@ fn pin_to_page(n: &mut Node, m: Affine) { /// What SVG Tiny 1.2 does without masks. const TINY_MASKS: &str = "SVG Tiny 1.2 has no masks: opacity masks are left out and knockout groups written as plain groups"; + +/// What a knockout group too large to mask is written as. +const KNOCKOUT_SIZE: &str = "knockout groups with too many overlapping elements to mask are written as plain groups"; + +/// The most knockout shapes the masks of one knockout group's elements use between them: each +/// mask uses the shapes of the elements above its own element that overlap it. +const MAX_KNOCKOUT_MASK_SHAPES: usize = 1 << 16; + +/// The most elements of one knockout group drawn through masks: finding the elements that +/// overlap compares every pair. +const MAX_MASKED_ELEMENTS: usize = 2048; + pub(super) const UNDECODED: &str = "an image couldn't be decoded and is missing from the picture"; /// The region of a mask or filter over art that has no box to measure: one covering any artwork. @@ -450,20 +462,39 @@ impl Writer<'_> { region } + /// The box of what each of `nodes` paints, as [`Self::paint_box`] measures it. + fn paint_boxes(&mut self, nodes: &[&Node]) -> Vec>> { + let symbols = self.region_boxes.take().unwrap_or_else(|| self.doc.symbol_boxes()); + let mut art_reach = std::mem::take(&mut self.art_reach); + let boxes = nodes.iter().map(|n| self.paint_box(&symbols, &mut art_reach, n)).collect(); + self.region_boxes = Some(symbols); + self.art_reach = art_reach; + boxes + } + + /// The box of what `n` paints in the current user space, as [`Self::region_attrs`] measures + /// it: `Some(None)` when it has no box, `None` when the box can't be told (type, or nesting + /// deeper than [`MAX_REACH_DEPTH`]). + fn paint_box(&self, symbols: &vectorcraft_doc::SymbolBoxes, art_reach: &mut HashMap, n: &Node) -> Option> { + let Some(v) = n.visual_bounds_with(symbols) else { return Some(None) }; + let r = self.paint_reach(symbols, art_reach, n, v, 0)?; + let g = n.geometric_bounds_with(symbols).unwrap_or(v); + let stroke = [g.x0 - v.x0, g.y0 - v.y0, v.x1 - g.x1, v.y1 - g.y1].into_iter().fold(0.0, f64::max); + let scaled = self.xf.transform_rect_bbox(v.inflate(r, r)); + let unscaled = self.xf.transform_rect_bbox(g).inflate(stroke + r, stroke + r); + Some(Some(scaled.union(unscaled))) + } + /// [`Self::region_attrs`] with the document's symbol boxes and the symbols' art reach so far. fn region_of(&self, symbols: &vectorcraft_doc::SymbolBoxes, art_reach: &mut HashMap, nodes: &[&Node]) -> String { let mut painted: Option = None; for n in nodes { - let Some(v) = n.visual_bounds_with(symbols) else { continue }; - let Some(r) = self.paint_reach(symbols, art_reach, n, v, 0) else { + let Some(b) = self.paint_box(symbols, art_reach, n) else { return BIG.to_string(); }; - let g = n.geometric_bounds_with(symbols).unwrap_or(v); - let stroke = [g.x0 - v.x0, g.y0 - v.y0, v.x1 - g.x1, v.y1 - g.y1].into_iter().fold(0.0, f64::max); - let scaled = self.xf.transform_rect_bbox(v.inflate(r, r)); - let unscaled = self.xf.transform_rect_bbox(g).inflate(stroke + r, stroke + r); - let both = scaled.union(unscaled); - painted = Some(painted.map_or(both, |p| p.union(both))); + if let Some(both) = b { + painted = Some(painted.map_or(both, |p| p.union(both))); + } } let Some(b) = painted.map(|b| b.inflate(2.0, 2.0)).filter(|b| b.is_finite()) else { return BIG.to_string(); @@ -572,8 +603,10 @@ impl Writer<'_> { } /// Children of the group being written. SVG has no knockout groups: in one, each element is - /// drawn through a mask of where the elements above it don't paint (the same look for Normal - /// blending). The masks nest, so element i sits inside the masks of elements i+1…n. + /// drawn through one mask of where the elements above it that overlap it don't paint (the + /// same look for Normal blending). The masks share one copy of each element's knockout shape, + /// and the elements stay siblings however many there are. A group whose masks would hold more + /// than [`MAX_KNOCKOUT_MASK_SHAPES`] shapes is written as a plain group, with a warning. pub(super) fn children(&mut self, children: &[std::sync::Arc]) { if self.knockout && self.tiny() { self.warn(TINY_MASKS); @@ -603,36 +636,124 @@ impl Writer<'_> { } return; } - let Some((first, rest)) = elements.split_first() else { return }; - // The elements are written first, in paint order, so that each mask copies an element + let all: Vec<&Node> = elements.iter().map(|c| &***c).collect(); + let Some(above) = self.knockout_masks(&all) else { + self.warn(KNOCKOUT_SIZE); + for c in elements { + self.node(c); + } + return; + }; + // The elements are written first, in paint order, so that each shape copies an element // with the symbol art count the element itself was written with - // (`Self::symbol_nodes`): an instance left out of an element is left out of its mask. - let (depth, nest) = (self.depth, rest.len()); - let first = self.detached(depth + nest, |w| w.node(first)); - let mut written = Vec::with_capacity(nest); - for (i, c) in rest.iter().enumerate() { + // (`Self::symbol_nodes`): an instance left out of an element is left out of its shape. + let depth = self.depth; + let mut written = Vec::with_capacity(elements.len()); + for (c, above) in elements.iter().zip(&above) { let before = self.symbol_nodes; - written.push((before, self.detached(depth + nest - 1 - i, |w| w.node(c)))); + written.push((before, self.detached(depth + usize::from(!above.is_empty()), |w| w.node(c)))); } let after = self.symbol_nodes; // Each mask covers what the group's elements paint. - let all: Vec<&Node> = elements.iter().map(|c| &***c).collect(); let region = self.region_attrs(&all); - for (c, (before, _)) in rest.iter().zip(&written).rev() { - self.symbol_nodes = *before; - let mid = self.knockout_mask(c, ®ion); - self.line(&format!("")); - self.depth += 1; + let mut shapes: Vec> = vec![None; elements.len()]; + for &j in above.iter().flatten() { + if shapes[j].is_none() { + self.symbol_nodes = written[j].0; + shapes[j] = Some(self.knockout_shape(elements[j])); + } } self.symbol_nodes = after; - self.body.push_str(&first); - for (_, body) in written { - self.depth -= 1; - self.line(""); + for (above, (_, body)) in above.iter().zip(written) { + if above.is_empty() { + self.body.push_str(&body); + continue; + } + let ids: Vec<&str> = above.iter().filter_map(|&j| shapes[j].as_deref()).collect(); + let mid = self.knockout_mask(&ids, ®ion); + self.line(&format!("")); self.body.push_str(&body); + self.line(""); } } + /// For each element of a knockout group, the indices of the elements above it whose painted + /// boxes overlap its own (every one above it when a box can't be told). `None` when there are + /// more than [`MAX_MASKED_ELEMENTS`] elements or the masks would hold more than + /// [`MAX_KNOCKOUT_MASK_SHAPES`] shapes. + fn knockout_masks(&mut self, elements: &[&Node]) -> Option>> { + if elements.len() > MAX_MASKED_ELEMENTS { + return None; + } + let boxes = self.paint_boxes(elements); + let overlap = |a: Option>, b: Option>| match (a, b) { + (Some(None), _) | (_, Some(None)) => false, + (Some(Some(a)), Some(Some(b))) => a.x0 <= b.x1 + 2.0 && b.x0 <= a.x1 + 2.0 && a.y0 <= b.y1 + 2.0 && b.y0 <= a.y1 + 2.0, + _ => true, + }; + let mut shapes = 0usize; + let mut out = Vec::with_capacity(elements.len()); + for (i, &below) in boxes.iter().enumerate() { + let mut above = vec![]; + for (j, &over) in boxes.iter().enumerate().skip(i + 1) { + if overlap(below, over) { + shapes = shapes.checked_add(1).filter(|&s| s <= MAX_KNOCKOUT_MASK_SHAPES)?; + above.push(j); + } + } + out.push(above); + } + Some(out) + } + + /// One copy of the knockout shape of `c` in the definitions: `c` at full object opacity + /// without its own mask, unless those define its shape. Returns its id. + fn knockout_shape(&mut self, c: &Node) -> String { + let shape = if c.knockout_shape { + c.clone() + } else { + let mut s = c.clone().with_mask(None); + s.opacity = 1.0; + s + }; + let anonymous = std::mem::replace(&mut self.anonymous, true); + let art = self.detached(2, |w| w.node(&shape)); + self.anonymous = anonymous; + let id = self.fresh_id("knockout-art"); + self.def(1, &format!("")); + self.defs.push_str(&art); + self.def(1, ""); + id + } + + /// A mask that is 1 − the knockout shapes `ids` ([`Self::knockout_shape`]): white, then the + /// shapes painted black. `region` is the attributes of the mask's region + /// ([`Self::region_attrs`]), which the filter that paints the shapes black shares. Returns + /// the mask id. + fn knockout_mask(&mut self, ids: &[&str], region: &str) -> String { + let filter = match self.knockout_filters.get(region) { + Some(f) => f.clone(), + None => { + let f = self.fresh_id("knockout-shape"); + self.def(1, &format!("")); + self.def(2, ""); + self.def(1, ""); + self.knockout_filters.insert(region.to_string(), f.clone()); + f + } + }; + let mid = self.fresh_id("knockout"); + self.def(1, &format!("")); + self.def(2, &format!("")); + self.def(2, &format!("")); + for id in ids { + self.def(3, &format!("")); + } + self.def(2, ""); + self.def(1, ""); + mid + } + /// The pieces of a partly transparent mesh-warped image (`elements`) as the elements of a /// knockout group ([`vectorcraft_effects::warp_pieces_translucent`]). Each piece is drawn /// through one mask of where the pieces above it that reach it don't cover @@ -683,42 +804,6 @@ impl Writer<'_> { } } - /// A mask that is 1 − the knockout shape of `c`: white, then `c` painted black (at full object - /// opacity without its own mask, unless those define its shape). `region` is the attributes of - /// the mask's region ([`Self::region_attrs`]), which the filter that paints `c` black shares. - /// Returns the mask id. - fn knockout_mask(&mut self, c: &Node, region: &str) -> String { - let filter = match self.knockout_filters.get(region) { - Some(f) => f.clone(), - None => { - let f = self.fresh_id("knockout-shape"); - self.def(1, &format!("")); - self.def(2, ""); - self.def(1, ""); - self.knockout_filters.insert(region.to_string(), f.clone()); - f - } - }; - let shape = if c.knockout_shape { - c.clone() - } else { - let mut s = c.clone().with_mask(None); - s.opacity = 1.0; - s - }; - let anonymous = std::mem::replace(&mut self.anonymous, true); - let art = self.detached(3, |w| w.node(&shape)); - self.anonymous = anonymous; - let mid = self.fresh_id("knockout"); - self.def(1, &format!("")); - self.def(2, &format!("")); - self.def(2, &format!("")); - self.defs.push_str(&art); - self.def(2, ""); - self.def(1, ""); - mid - } - /// An object, inside `` when it links to a URL (Attributes panel). pub(super) fn node(&mut self, n: &Node) { // Ruler guides are not written, not even as a link. diff --git a/crates/svg/tests/it/blend_export.rs b/crates/svg/tests/it/blend_export.rs index 3804d30ed2..dd86f318f5 100644 --- a/crates/svg/tests/it/blend_export.rs +++ b/crates/svg/tests/it/blend_export.rs @@ -262,3 +262,93 @@ fn a_mask_region_covers_a_blend_as_written() { let [x, y, w, h] = ["x", "y", "width", "height"].map(|a| mask.attribute(a).and_then(|v| v.parse::().ok()).unwrap_or(f64::NAN)); assert!(x <= 0.0 && y <= 0.0 && x + w >= 95.0 && y + h >= 35.0 && w < 200.0 && h < 200.0, "{x} {y} {w} {h} in {svg}"); } + +/// A knockout group of `n` red, green and blue squares at 50% opacity that all overlap, over an +/// opaque backdrop. `knockout_shape`: their opacity defines their knockout shape. +fn knockout_squares(n: usize, knockout_shape: bool) -> Document { + let mut d = Document::new(200.0, 200.0); + let backdrop = rect(&mut d, Rect::new(0.0, 0.0, 200.0, 200.0), [230, 200, 50]); + add(&mut d, backdrop); + let colors = [[255, 0, 0], [0, 200, 0], [0, 0, 255]]; + let squares = (0..n) + .map(|i| { + let x = (i % 20) as f64 * 2.0; + let mut s = rect(&mut d, Rect::new(20.0 + x, 50.0, 120.0 + x, 150.0), colors[i % 3]); + (s.opacity, s.knockout_shape) = (0.5, knockout_shape); + std::sync::Arc::new(s) + }) + .collect(); + let mut group = Node::group(d.alloc_id(), squares); + group.knockout = vectorcraft_doc::Knockout::On; + add(&mut d, group); + d +} + +/// The deepest an element of `svg` nests, counting the root element as 1. +fn element_depth(svg: &str) -> usize { + let xml = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }).unwrap(); + xml.descendants().filter(|n| n.is_element()).map(|n| n.ancestors().filter(|a| a.is_element()).count()).max().unwrap_or(0) +} + +/// The elements of a knockout group are siblings, each in one mask of the overlapping elements +/// above it, however many there are: 200 translucent elements stay well within the nesting the +/// web build reads (34 levels), and the file reads back with nothing merged. Where the elements' +/// shapes are the area they paint, the file draws as the canvas does. +#[test] +fn a_knockout_group_of_many_translucent_elements_stays_shallow() { + for knockout_shape in [false, true] { + let d = knockout_squares(200, knockout_shape); + let (svg, warnings) = vectorcraft_svg::export_with_report(&d, &ExportOptions::default()); + assert!(warnings.is_empty(), "{warnings:?}"); + assert_eq!(svg.matches(" Tree<'a> { }) .collect() }; - let gradient = |kind, stops: Vec, geom| { - let mut paint = GradientPaint::new(Gradient { kind, stops, ..Gradient::default() }); + let gradient = |kind, stops: Vec, spread, geom| { + // COLR repeats the interval from the first stop to the last, and one run of the + // stops spans the gradient's vector. + let mut paint = GradientPaint::new(Gradient { kind, stops, spread, ..Gradient::default() }); paint.geom = Some(geom); Paint::Gradient(Box::new(paint)) }; @@ -290,22 +292,22 @@ impl<'a> Tree<'a> { Some((Paint::solid(c), a)) } Brush::LinearGradient { p0, p1, color_stops, extend } => { - if *extend != Extend::Pad { - self.unsupported.get_or_insert("a repeating or reflecting gradient"); + let spread = spread(*extend); + let (p0, p1) = (point(*p0), point(*p1)); + let stops = stops(color_stops, self); + // Without a stop, the others would repeat over a different interval. + if !spread.is_pad() && stops.len() < color_stops.len() { + self.unsupported.get_or_insert("a repeating or reflecting gradient with a stop missing from the palette"); return None; } - let (p0, p1) = (point(*p0), point(*p1)); - let (stops, first, last) = normalized(stops(color_stops, self))?; + let (stops, first, last) = normalized(stops)?; // Stops past 0 and 1 move the ends instead. let mut geom = GradientGeom { start: p0.lerp(p1, f64::from(first)), end: p0.lerp(p1, f64::from(last)), aspect: 1.0, focal: None }; geom.transform(xf, GradientKind::Linear); - Some((gradient(GradientKind::Linear, stops, geom), 1.0)) + Some((gradient(GradientKind::Linear, stops, spread, geom), 1.0)) } Brush::RadialGradient { c0, r0, c1, r1, color_stops, extend } => { - if *extend != Extend::Pad { - self.unsupported.get_or_insert("a repeating or reflecting gradient"); - return None; - } + let spread = spread(*extend); let (c0, c1, r0, r1) = (point(*c0), point(*c1), f64::from(*r0), f64::from(*r1)); let concentric = (c0 - c1).hypot() < 1e-6; if (r0 > 1e-6 && !concentric) || r1 <= r0 || (c0 - c1).hypot() >= r1 { @@ -317,13 +319,27 @@ impl<'a> Tree<'a> { self.unsupported.get_or_insert("a radial gradient with stops past its circles"); return None; } + if !spread.is_pad() { + if stops.len() < color_stops.len() { + self.unsupported.get_or_insert("a repeating or reflecting gradient with a stop missing from the palette"); + return None; + } + // A run of the stops is the whole way from the center to the circle: one that + // starts at a circle (a first stop past the center comes as one) repeats over + // less of it. The normalized ends can be a rounding error off 0 and 1. + let (lo, hi) = stops.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), s| (lo.min(s.offset), hi.max(s.offset))); + if r0.abs() > 1e-6 || lo.abs() > 1e-4 || (hi - 1.0).abs() > 1e-4 { + self.unsupported.get_or_insert("a repeating or reflecting radial gradient that doesn't start at its center"); + return None; + } + } // A start circle around the centre starts the stops further out. for s in &mut stops { s.offset = ((r0 + f64::from(s.offset) * (r1 - r0)) / r1) as f32; } let mut geom = GradientGeom { start: c1, end: c1 + Vec2::new(r1, 0.0), aspect: 1.0, focal: (!concentric).then_some(c0) }; geom.transform(xf, GradientKind::Radial); - Some((gradient(GradientKind::Radial, stops, geom), 1.0)) + Some((gradient(GradientKind::Radial, stops, spread, geom), 1.0)) } Brush::SweepGradient { .. } => { self.unsupported.get_or_insert("a sweep gradient"); @@ -345,6 +361,16 @@ impl<'a> Tree<'a> { } } +/// A COLR extend mode as the gradient's spread; an unknown one pads, as the COLR specification +/// says. +fn spread(extend: Extend) -> GradientSpread { + match extend { + Extend::Repeat => GradientSpread::Repeat, + Extend::Reflect => GradientSpread::Reflect, + _ => GradientSpread::Pad, + } +} + /// `stops` sorted, from 0 to 1 (as the gradient's ends then move), and where the first and last /// were; `None` without stops. fn normalized(mut stops: Vec) -> Option<(Vec, f32, f32)> { diff --git a/crates/text/src/layout/shaped.rs b/crates/text/src/layout/shaped.rs index 72f6ed8733..63005364fb 100644 --- a/crates/text/src/layout/shaped.rs +++ b/crates/text/src/layout/shaped.rs @@ -5,6 +5,11 @@ //! line. Each line the composer tries is placed from where the lines above it left the pen, at its //! own tallest metrics, as the line loop in [`super::flow`] places a composed line: the span it //! gets is the room across its whole height. +//! +//! A paragraph the frame cannot hold is still composed whole: a line the frame has no room for, +//! and every line after it, is measured as wide as the span of the last line set in the frame, +//! less its own indents. The line loop sets only the lines that fit, and the rest overflows or +//! goes on in the next threaded frame, which composes it again at its own measures. use std::cell::Cell; use std::collections::HashMap; @@ -63,19 +68,33 @@ struct Start<'r> { /// The paragraph's first line (it takes the first-line indent). first: bool, pen: Pen<'r>, - /// The line placed at each of the metrics it was tried with: its width and the pen after it, - /// or `None` when the frame has no room for it. - placed: Vec<(Metrics, Option<(f64, Pen<'r>)>)>, + /// The width of the span the line above was set in, `None` for the paragraph's first line. + span: Option, + /// The line above was past the frame's end: this line is too, at the width of `span`. + past: bool, + /// The line placed at each of the metrics it was tried with, or `None` when it can't be set. + placed: Vec<(Metrics, Option>)>, /// The metrics of glyphs `pos..to`, measured so far. to: usize, m: Option, } -/// What a line's placement depends on: its first glyph, whether it is the paragraph's first line, -/// and the pen's cell, baselines, pending space, narrowest word without room and further spans. -type Key = (usize, bool, usize, Option, Option, u64, u64, Vec<[u64; 3]>); +/// A line set from a [`Start`]: its width, the pen after it, the width of the span it was set +/// in, and whether it is past the frame's end (measured, but not in the frame). +#[derive(Clone)] +struct Placed<'r> { + width: f64, + pen: Pen<'r>, + span: f64, + past: bool, +} -fn key(pos: usize, first: bool, pen: &Pen<'_>) -> Key { +/// What a line's placement depends on: its first glyph, whether it is the paragraph's first line, +/// the pen's cell, baselines, pending space, narrowest word without room and further spans, the +/// span of the line above and whether the line is past the frame's end. +type Key = (usize, bool, usize, Option, Option, u64, u64, Vec<[u64; 3]>, Option, bool); + +fn key(pos: usize, first: bool, pen: &Pen<'_>, span: Option, past: bool) -> Key { ( pos, first, @@ -85,6 +104,8 @@ fn key(pos: usize, first: bool, pen: &Pen<'_>) -> Key { pen.pending.to_bits(), pen.no_room_for.to_bits(), pen.queued.iter().map(|&(b, x0, x1)| [b.to_bits(), x0.to_bits(), x1.to_bits()]).collect(), + span.map(f64::to_bits), + past, ) } @@ -159,13 +180,14 @@ impl<'a, 'r> Shaped<'a, 'r> { } } - /// The start of a line at glyph `pos` with `pen`: an existing one when its placement is the same. - fn start(&mut self, pos: usize, first: bool, pen: Pen<'r>) -> usize { - let k = key(pos, first, &pen); + /// The start of a line at glyph `pos` with `pen`, after a line set in a span `span` wide (past + /// the frame's end with `past`): an existing one when its placement is the same. + fn start(&mut self, pos: usize, first: bool, pen: Pen<'r>, span: Option, past: bool) -> usize { + let k = key(pos, first, &pen, span, past); if let Some(&id) = self.known.get(&k) { return id; } - self.starts.push(Start { pos, first, pen, placed: vec![], to: pos, m: None }); + self.starts.push(Start { pos, first, pen, span, past, placed: vec![], to: pos, m: None }); let id = self.starts.len() - 1; self.known.insert(k, id); id @@ -177,7 +199,7 @@ impl Measure for Shaped<'_, '_> { fn first(&mut self) -> usize { let pen = self.pen.clone(); - self.start(0, !self.continued, pen) + self.start(0, !self.continued, pen, None, false) } fn line(&mut self, &id: &usize, pos: usize, end: usize) -> Result, Exhausted> { @@ -186,14 +208,14 @@ impl Measure for Shaped<'_, '_> { } self.probes += 1; let Some(start) = self.starts.get_mut(id) else { return Ok(None) }; - let Some((width, pen)) = line(&self.para, start, pos, end)? else { return Ok(None) }; - Ok(Some((width, self.start(end, false, pen)))) + let Some(placed) = line(&self.para, start, pos, end)? else { return Ok(None) }; + Ok(Some((placed.width, self.start(end, false, placed.pen, Some(placed.span), placed.past)))) } /// A longer line is placed as this one is, and gets the same room, unless a glyph after it - /// changes the line's tallest metrics. + /// changes the line's tallest metrics. Past the frame's end every line is as wide. fn may_widen(&mut self, &id: &usize, pos: usize, end: usize) -> bool { - let Some(start) = self.starts.get(id) else { return false }; + let Some(start) = self.starts.get(id).filter(|start| !start.past) else { return false }; let m = if start.pos == pos && start.to == end { start.m } else { self.para.sg.get(pos..end).and_then(Metrics::max) }; match (m, self.rest.get(end).copied().flatten()) { (Some(m), Some(rest)) => m.union(rest) != m, @@ -212,12 +234,14 @@ impl Drop for Shaped<'_, '_> { } } -/// The width of the line of glyphs `pos..end` from `start`, and the pen after it; `None` when -/// the frame has no room for it. -fn line<'r>(p: &Para<'_>, start: &mut Start<'r>, pos: usize, end: usize) -> Result)>, Exhausted> { +/// The line of glyphs `pos..end` from `start`; `None` when it can't be set. +fn line<'r>(p: &Para<'_>, start: &mut Start<'r>, pos: usize, end: usize) -> Result>, Exhausted> { if start.pos != pos || end <= pos { return Ok(None); } + if start.past { + return Ok(past_end(p, start)); + } // The line's tallest metrics, measured on from the glyphs already measured. if end < start.to { start.m = p.sg.get(pos..end).and_then(Metrics::max); @@ -239,9 +263,18 @@ fn line<'r>(p: &Para<'_>, start: &mut Start<'r>, pos: usize, end: usize) -> Resu Ok(placed) } -/// The line from `start` placed with metrics `m`: its width and the pen after it, as the line loop -/// places and settles it; `None` when no frame cell has room for it. -fn place<'r>(p: &Para<'_>, start: &Start<'r>, m: Metrics) -> Result)>, Exhausted> { +/// A line from `start` past the frame's end: as wide as the span of the last line set in the frame, +/// less its indents, and the pen left where the frame ran out. `None` when the paragraph has no line +/// in the frame to measure it by. +fn past_end<'r>(p: &Para<'_>, start: &Start<'r>) -> Option> { + let span = start.span.filter(|span| span.is_finite())?; + let (ind_l, ind_r) = line_indents(p.para, start.first, p.mirror); + Some(Placed { width: span - ind_l - ind_r, pen: start.pen.clone(), span, past: true }) +} + +/// The line from `start` placed with metrics `m`, as the line loop places and settles it; past the +/// frame's end ([`past_end`]) when no frame cell has room for it. +fn place<'r>(p: &Para<'_>, start: &Start<'r>, m: Metrics) -> Result>, Exhausted> { let (ind_l, ind_r) = line_indents(p.para, start.first, p.mirror); let word = word_width(p.sg, start.pos, p.line_end, p.para.kinsoku) - (p.start_credit)(start.pos); let mut pen = start.pen.clone(); @@ -262,12 +295,12 @@ fn place<'r>(p: &Para<'_>, start: &Start<'r>, m: Metrics) -> Result return Ok(None), // Out of steps: the frame may still have room further down. None if p.steps.get() == 0 => return Err(Exhausted), - None => return Ok(None), + None => return Ok(past_end(p, start)), } } } diff --git a/crates/text/src/tests/colr.rs b/crates/text/src/tests/colr.rs index 212882ce7a..c3a0499af6 100644 --- a/crates/text/src/tests/colr.rs +++ b/crates/text/src/tests/colr.rs @@ -326,12 +326,8 @@ fn paints_documents_lack_make_the_glyph_an_image() { let line = |extend| ColorLine::new(extend, 2, vec![ColorStop::new(F2Dot14::from_f32(0.0), 0, one), ColorStop::new(one, 1, one)]); let w = FWord::new; type GlyphPaint<'a> = Box ColrPaint + 'a>; - let cases: [(&str, GlyphPaint<'_>); 3] = [ + let cases: [(&str, GlyphPaint<'_>); 2] = [ ("Colrtest Xor", Box::new(|o| ColrPaint::composite(ColrPaint::glyph(solid(2), o), CompositeMode::Xor, ColrPaint::glyph(solid(0), o)))), - ( - "Colrtest Repeat", - Box::new(move |o| ColrPaint::glyph(ColrPaint::linear_gradient(line(Extend::Repeat), w(0), w(0), w(100), w(0), w(0), w(100)), o)), - ), ( "Colrtest Circles", Box::new(move |o| { @@ -362,6 +358,129 @@ fn a_radial_gradient_from_a_circle_around_its_centre_is_vector_art() { assert_eq!(offsets, [0.25, 1.0], "the stops start a quarter of the way out"); } +/// A COLRv1 color line through `stops` (offset, palette index) that extends by `extend`. +fn color_line(extend: write_fonts::tables::colr::Extend, stops: &[(f32, u16)]) -> write_fonts::tables::colr::ColorLine { + use write_fonts::tables::colr::{ColorLine, ColorStop}; + use write_fonts::types::F2Dot14; + let one = F2Dot14::from_f32(1.0); + let stops: Vec = stops.iter().map(|&(at, index)| ColorStop::new(F2Dot14::from_f32(at), index, one)).collect(); + ColorLine::new(extend, u16::try_from(stops.len()).unwrap(), stops) +} + +/// The gradient filling the glyph's one path. +fn glyph_gradient(font: &[u8]) -> vectorcraft_doc::color::GradientPaint { + let root = art(font, COLOUR_CHAR); + let [only] = children(&root) else { panic!("one path") }; + let Paint::Gradient(g) = fill(only) else { panic!("a gradient") }; + *g +} + +/// A linear gradient that repeats or reflects is vector art. The gradient spreads the same way, +/// and its stops run once from the first stop's place to the last stop's place. +#[test] +fn a_repeating_or_reflecting_linear_gradient_is_vector_art() { + use vectorcraft_doc::color::GradientSpread; + use write_fonts::tables::colr::Extend; + use write_fonts::types::FWord; + let w = FWord::new; + for (family, extend, spread) in + [("Colrtest Linear Repeat", Extend::Repeat, GradientSpread::Repeat), ("Colrtest Linear Reflect", Extend::Reflect, GradientSpread::Reflect)] + { + let line = color_line(extend, &[(0.2, 0), (0.6, 1)]); + let font = + v1_font(family, COLOUR_CHAR, |o| ColrPaint::glyph(ColrPaint::linear_gradient(line.clone(), w(0), w(0), w(100), w(0), w(0), w(100)), o)); + let g = glyph_gradient(&font); + assert_eq!(g.gradient.kind, GradientKind::Linear, "{family}"); + assert_eq!(g.gradient.painted_spread(), (spread, 1), "{family}"); + let offsets: Vec = g.gradient.stops.iter().map(|s| s.offset).collect(); + assert_eq!(offsets, [0.0, 1.0], "{family}"); + let geom = g.geom.unwrap(); + assert!((geom.start.x - 20.0).abs() < 0.01 && (geom.end.x - 60.0).abs() < 0.01, "{family}: {:?} → {:?}", geom.start, geom.end); + } +} + +/// A radial gradient from its center that repeats or reflects is vector art that spreads the same +/// way, also with a focal point. Its circle is at the last stop, and its stops run once from the +/// center out to that circle. +#[test] +fn a_repeating_or_reflecting_radial_gradient_from_its_center_is_vector_art() { + use vectorcraft_doc::color::GradientSpread; + use write_fonts::tables::colr::Extend; + use write_fonts::types::{FWord, UfWord}; + let w = FWord::new; + let cases = [ + ("Colrtest Radial Repeat", Extend::Repeat, GradientSpread::Repeat, 250, 1.0, 400.0), + ("Colrtest Radial Reflect", Extend::Reflect, GradientSpread::Reflect, 300, 1.0, 400.0), + ("Colrtest Radial Half", Extend::Repeat, GradientSpread::Repeat, 300, 0.5, 200.0), + // Normalized, the last stop is a rounding error short of 1. + ("Colrtest Radial Fine", Extend::Repeat, GradientSpread::Repeat, 250, 41.0 / 16384.0, 400.0 * 41.0 / 16384.0), + ]; + for (family, extend, spread, x0, last, radius) in cases { + let line = color_line(extend, &[(0.0, 0), (last / 2.0, 2), (last, 1)]); + let font = v1_font(family, COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(x0), w(300), UfWord::new(0), w(250), w(300), UfWord::new(400)), o) + }); + let g = glyph_gradient(&font); + assert_eq!(g.gradient.kind, GradientKind::Radial, "{family}"); + assert_eq!(g.gradient.painted_spread(), (spread, 1), "{family}"); + let offsets: Vec = g.gradient.stops.iter().map(|s| s.offset).collect(); + assert!(offsets.len() == 3 && offsets.iter().zip([0.0, 0.5, 1.0]).all(|(a, b)| (a - b).abs() < 0.05), "{family}: {offsets:?}"); + let geom = g.geom.unwrap(); + assert!(((geom.end - geom.start).hypot() - radius).abs() < 0.1, "{family}: {geom:?}"); + assert_eq!(geom.focal.is_some(), x0 != 250, "{family}"); + } +} + +/// A radial gradient that repeats from a circle stays an image. The circle is either a start circle +/// around the center or the place a first stop past the center puts the start. +#[test] +fn a_repeating_radial_gradient_from_a_circle_makes_the_glyph_an_image() { + use write_fonts::tables::colr::Extend; + use write_fonts::types::{FWord, UfWord}; + let w = FWord::new; + for (family, r0, first) in [("Colrtest Ring Repeat", 100, 0.0), ("Colrtest Short Repeat", 0, 0.25)] { + let line = color_line(Extend::Repeat, &[(first, 0), (1.0, 1)]); + let font = v1_font(family, COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(250), w(300), UfWord::new(r0), w(250), w(300), UfWord::new(400)), o) + }); + assert!(matches!(content(&font, COLOUR_CHAR), Some(ColourContent::Image(_))), "{family}"); + } +} + +/// A gradient that repeats or reflects with a stop whose palette index is outside the palette stays +/// an image. Without that stop, the other stops would repeat over a different interval. +#[test] +fn a_repeating_gradient_with_a_stop_missing_from_the_palette_makes_the_glyph_an_image() { + use write_fonts::tables::colr::Extend; + use write_fonts::types::{FWord, UfWord}; + let w = FWord::new; + let line = color_line(Extend::Repeat, &[(0.0, 0), (0.5, 1), (1.0, 200)]); + let linear = v1_font("Colrtest Linear Missing", COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::linear_gradient(line.clone(), w(0), w(0), w(100), w(0), w(0), w(100)), o) + }); + let radial = v1_font("Colrtest Radial Missing", COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::radial_gradient(line.clone(), w(250), w(300), UfWord::new(0), w(250), w(300), UfWord::new(400)), o) + }); + for (family, font) in [("linear", linear), ("radial", radial)] { + assert!(matches!(content(&font, COLOUR_CHAR), Some(ColourContent::Image(_))), "{family}"); + } +} + +/// A color line with an extend value the COLR specification doesn't define pads. +#[test] +fn an_unknown_extend_value_pads() { + use vectorcraft_doc::color::GradientSpread; + use write_fonts::tables::colr::Extend; + use write_fonts::types::FWord; + let w = FWord::new; + let line = color_line(Extend::Unknown, &[(0.0, 0), (1.0, 1)]); + let font = v1_font("Colrtest Unknown Extend", COLOUR_CHAR, |o| { + ColrPaint::glyph(ColrPaint::linear_gradient(line.clone(), w(0), w(0), w(100), w(0), w(0), w(100)), o) + }); + let g = glyph_gradient(&font); + assert_eq!(g.gradient.painted_spread(), (GradientSpread::Pad, 1)); +} + /// A glyph whose layers don't use the foreground colour is read once, whatever the type's fill. #[test] fn a_glyph_without_the_foreground_colour_is_read_once_for_every_fill() { diff --git a/crates/text/src/tests/combos.rs b/crates/text/src/tests/combos.rs index 0146871265..4f57aa6972 100644 --- a/crates/text/src/tests/combos.rs +++ b/crates/text/src/tests/combos.rs @@ -465,18 +465,72 @@ fn composer_x_mixed_sizes_in_a_frame_that_widens() { #[test] fn composer_x_mixed_sizes_overflow_a_shaped_frame() { - // A stepped frame too short for the paragraph: the lines that fit are set, in order, each once - // and within its room, and the frame overflows. + // A stepped frame too short for the paragraph: the every-line composer still breaks the lines + // that fit as one paragraph, with the lines past the frame's end measured as wide as the last + // line the frame holds. Here that is the narrow part, so the lines that fit break as in a frame + // tall enough for the whole paragraph, and not as the single-line composer breaks them. They + // are set in order, each once and within its room, and the frame overflows. let w = x_width(); - let mut t = stepped(w, w * 0.5, 40.0, 75.0); - t.runs = with_x(sans(36.0)); - t.para.composer = Composer::EveryLine; - let l = layout(db(), &t); + let set = |bottom: f64, composer: Composer| { + let mut t = stepped(w, w * 0.5, 40.0, bottom); + t.runs = with_x(sans(36.0)); + t.para.composer = composer; + let l = layout(db(), &t); + (line_texts(&t, &l), l, t) + }; + let (every, l, t) = set(75.0, Composer::EveryLine); assert!(l.overflow && l.lines.len() >= 2, "{:?}", l.lines); assert_eq!(l.lines[0].start, 0); assert!(l.lines.windows(2).all(|p| p[0].end == p[1].start && p[0].start < p[0].end), "{:?}", l.lines); assert!(past_their_room(&t, &l).is_empty(), "{:?}", l.lines); assert!(l.lines.iter().all(|li| li.baseline + li.descent <= 75.0 + 0.01), "{:?}", l.lines); + assert_eq!(every, ["Destroy all creatures with toughness", "X or less. Then", "draw a card for each"]); + let (whole, ..) = set(300.0, Composer::EveryLine); + assert_eq!(every, whole[..every.len()]); + let (greedy, ..) = set(75.0, Composer::SingleLine); + assert_ne!(every, greedy); + // A frame with no room for a line sets none. + for composer in [Composer::EveryLine, Composer::SingleLine] { + let mut t = stepped(w, w * 0.5, 4.0, 8.0); + t.runs = with_x(sans(36.0)); + t.para.composer = composer; + let l = layout(db(), &t); + assert!(l.lines.is_empty() && l.overflow, "{composer:?}: {:?}", l.lines); + } +} + +#[test] +fn composer_x_mixed_sizes_overflow_around_text_wrap() { + // A rectangle that wraps around an object beside its lower lines, too short for the + // paragraph: the every-line composer breaks the lines that fit as one paragraph, each span + // measured across its line's whole height, and the frame overflows. No glyph runs into the + // object. + let w = x_width(); + let object = Rect::new(w * 0.4, 40.0, w * 0.6, 70.0); + let set = |composer: Composer| { + let mut runs = with_x(sans(36.0)); + runs[0] = run("Destroy all creatures with toughness\u{a0}", sans(12.0)); + let mut t = area(runs, Rect::new(0.0, 0.0, w, 80.0)); + t.wrap = vec![vectorcraft_doc::WrapShape { path: PathData::from_bezpath(&object.to_path(0.1)), wrap: Default::default() }]; + t.para.composer = composer; + let l = layout(db(), &t); + (line_texts(&t, &l), l, t) + }; + let (every, l, t) = set(Composer::EveryLine); + assert!(l.overflow && l.lines.len() >= 3, "{:?}", l.lines); + assert!(l.lines.windows(2).all(|p| p[0].end == p[1].start && p[0].start < p[0].end), "{:?}", l.lines); + assert!(past_their_room(&t, &l).is_empty(), "{:?}", l.lines); + let text = t.plain_text(); + let hits = l.glyphs.iter().filter(|g| { + let li = &l.lines[g.line]; + let across = li.baseline - li.ascent < object.y1 && li.baseline + li.descent > object.y0; + across && !text[g.byte..].starts_with(char::is_whitespace) && g.origin.x < object.x1 && g.origin.x + g.advance > object.x0 + }); + assert_eq!(hits.count(), 0, "{:?}", l.lines); + // The lines that fit break as in `composer_x_mixed_sizes_around_text_wrap`'s taller frame. + assert_eq!(every, ["Destroy all creatures with", "toughness\u{a0}X", "or less. Then", "draw a card for", "each creature"]); + let (greedy, ..) = set(Composer::SingleLine); + assert_ne!(every, greedy); } #[test] diff --git a/crates/text/src/thread/tests.rs b/crates/text/src/thread/tests.rs index 0ac64f5d27..adbf3b2c70 100644 --- a/crates/text/src/thread/tests.rs +++ b/crates/text/src/thread/tests.rs @@ -397,6 +397,80 @@ fn a_continued_paragraph_fills_the_frame_it_continues_into() { assert_eq!(checked, 16); } +/// Area type in a frame `w` wide down to `y` and `narrow` wide below it, down to `bottom`. +fn stepped_frame(w: f64, narrow: f64, y: f64, bottom: f64) -> TextObject { + let mut t = frame(0.0); + let path = kurbo::BezPath::from_vec(vec![ + kurbo::PathEl::MoveTo((0.0, 0.0).into()), + kurbo::PathEl::LineTo((w, 0.0).into()), + kurbo::PathEl::LineTo((w, y).into()), + kurbo::PathEl::LineTo((narrow, y).into()), + kurbo::PathEl::LineTo((narrow, bottom).into()), + kurbo::PathEl::LineTo((0.0, bottom).into()), + kurbo::PathEl::ClosePath, + ]); + t.kind = TextKind::Area { frame: PathData::from_bezpath(&path) }; + t +} + +/// A paragraph of mixed type sizes that a stepped frame cannot hold goes on in the next frame. The +/// every-line composer breaks the lines the first frame holds as one paragraph, as in a frame tall +/// enough for all of it and not as the single-line composer breaks them, and the first frame +/// takes the text of those lines. The next frame sets the rest at its own width, without the +/// first-line indent, and no character is lost or repeated. The frames take the same shares as +/// when each is measured with the whole rest of the story. +#[test] +fn a_mixed_size_paragraph_continued_from_a_shaped_frame_is_composed_in_both() { + use vectorcraft_doc::Composer; + let db = FontDb::global(); + let small = CharStyle { size: 12.0, ..CharStyle::default() }; + let big = CharStyle { size: 36.0, ..CharStyle::default() }; + let probe = layout(db, &TextObject::point(vectorcraft_geom::Point::ZERO, "Destroy all creatures with toughness X or", small.clone())); + let w = probe.lines[0].x1 - probe.lines[0].x0 + 2.0; + let story = vec![ + TextRun { text: "Destroy all creatures with toughness ".into(), style: small.clone(), inline: None }, + TextRun { text: "X".into(), style: big, inline: None }, + TextRun { text: " or less. Then draw a card for each creature destroyed this way.\nSecond paragraph.".into(), style: small, inline: None }, + ]; + let text: String = story.iter().map(|r| r.text.as_str()).collect(); + let (a, tall) = (stepped_frame(w, w * 0.5, 40.0, 85.0), stepped_frame(w, w * 0.5, 40.0, 300.0)); + let b = sized(200.0, w * 0.75, 300.0); + let texts = |lay: &crate::TextLayout, shown: &str| lay.lines.iter().map(|l| shown[l.start..l.end].trim_end().to_string()).collect::>(); + let shares = |composer: Composer, indent: f64| { + let para = ParaStyle { composer, first_line_indent: indent, ..Default::default() }; + let paras = [para.clone(), para]; + let parts = distribute(db, &[&a, &b], &story, ¶s); + assert_eq!(parts.iter().map(|p| p.continues).collect::>(), [false, true], "{composer:?}"); + let joined: String = parts.iter().flat_map(|p| &p.runs).map(|r| r.text.as_str()).collect(); + assert_eq!(joined, text, "{composer:?}: no character lost or repeated"); + same_as_whole_rest(db, &[&a, &b], &story, ¶s, &format!("{composer:?}, indent {indent}")); + let shown = |p: &Slice| p.runs.iter().map(|r| r.text.as_str()).collect::(); + let lays: Vec<_> = [&a, &b].iter().zip(&parts).map(|(f, p)| laid(db, f, p)).collect(); + assert!(lays.iter().all(|l| !l.overflow), "{composer:?}: each frame holds its share"); + let whole = + laid(db, &tall, &Slice { runs: story.clone(), paras: paras.to_vec(), list_count: 0, list_level_counts: vec![], continues: false }); + let first = texts(&lays[0], &shown(&parts[0])); + // The first frame takes the text of the lines it sets. + assert_eq!(shown(&parts[0]).split_whitespace().collect::>(), first.join(" ").split_whitespace().collect::>()); + (first, texts(&whole, &text), lays, parts) + }; + for indent in [0.0, 20.0] { + let (every, whole, lays, parts) = shares(Composer::EveryLine, indent); + assert!(every.len() >= 2, "{every:?}"); + assert_eq!(every, whole[..every.len()], "indent {indent}"); + if indent == 0.0 { + assert_eq!(every, ["Destroy all creatures with toughness", "X or less. Then", "draw a card for each"]); + let (greedy, ..) = shares(Composer::SingleLine, indent); + assert_eq!(greedy, ["Destroy all creatures with toughness X", "or less. Then draw a", "card for each", "creature destroyed"]); + } + // The next frame starts at its left edge, and its lines keep within it. + assert!((line_start(&lays[1], 0) - 200.0).abs() < 1e-6, "{}", line_start(&lays[1], 0)); + assert!(lays[1].lines.iter().all(|l| l.x1 <= 200.0 + w * 0.75 + 1e-6), "{:?}", lays[1].lines); + assert!((line_start(&lays[0], 0) - indent).abs() < 1e-6, "{}", line_start(&lays[0], 0)); + assert!(parts[1].continues && lays[1].lines.len() >= 2, "{:?}", lays[1].lines); + } +} + /// `frames` holding the runs of `parts`, for [`continued`]. fn filled(frames: &[&TextObject], parts: &[Slice]) -> Vec { frames diff --git a/crates/tools/src/guides/tests.rs b/crates/tools/src/guides/tests.rs index 602242d1cd..16b46f6fb9 100644 --- a/crates/tools/src/guides/tests.rs +++ b/crates/tools/src/guides/tests.rs @@ -1028,6 +1028,124 @@ fn a_drawn_point_lands_on_a_corner_of_a_glyphs_bounds() { } } +/// Snap to Glyph: both ends of a gradient vector drawn on the art with the Gradient tool, a click +/// that applies one, and its dragged start and end handles, land on a glyph's anchor point as a +/// Pen click does, with the guides shown while hovering, and not with Snap to Glyph off. A drawn +/// end also lands on a slanted path's segment, which no bounds line passes along, and on a +/// construction guide through its start, and with Shift on an anchor along its 45° step. With nothing +/// selected, hovering off the art shows no guides. A press that snaps still starts a drag only +/// after the pointer moves 3 px from where it was pressed. +#[test] +fn gradient_vector_ends_land_on_a_glyph_anchor() { + use crate::xform::GradientTool; + use crate::{Action, PointerEvent, PointerKind, Tool}; + use vectorcraft_color::{Gradient, GradientGeom, GradientPaint, Paint}; + use vectorcraft_doc::Appearance; + use vectorcraft_geom::shapes; + let (mut d, t) = glyph_doc("H", 0.0); + let lay = layout_text(FontDb::global(), &t); + let g = lay.glyphs.iter().find(|g| !g.outline.elements().is_empty()).expect("a glyph with an outline"); + // An anchor inside the glyph's bounds, which no glyph bounds line passes through. + let bb = g.outline.bounding_box(); + let inside = |p: Point| p.x > bb.x0 + 1.0 && p.x < bb.x1 - 1.0 && p.y > bb.y0 + 1.0 && p.y < bb.y1 - 1.0; + let anchor = t.xf * g.outline.segments().map(|s| s.start()).find(|p| inside(*p)).expect("an anchor inside the glyph"); + let type_id = d.children(Some(d.layers[0].id)).unwrap()[0].id; + // A rectangle away from the type, with a gradient from (300, 100) to (400, 100). + let (l, rect) = (d.layers[0].id, d.alloc_id()); + d.insert(Some(l), 0, Node::path(rect, shapes::rectangle(Rect::new(300.0, 50.0, 400.0, 150.0)), Appearance::default_art())).unwrap(); + let mut gp = GradientPaint::new(Gradient::default()); + gp.geom = Some(GradientGeom { start: Point::new(300.0, 100.0), end: Point::new(400.0, 100.0), aspect: 1.0, focal: None }); + d.node_mut(rect).unwrap().appearance.set_fill(Paint::Gradient(Box::new(gp))); + // A slanted line, its middle far from its bounds' edges. + let (a0, a1) = (Point::new(200.0, 420.0), Point::new(280.0, 480.0)); + let line = d.alloc_id(); + d.insert(Some(l), 0, Node::path(line, shapes::line(a0, a1), Appearance::default_art())).unwrap(); + let mut on_type = Selection::default(); + on_type.add(type_id); + let mut on_rect = Selection::default(); + on_rect.add(rect); + let p = paint(); + let click = anchor + Vec2::new(1.5, -1.0); + let away = anchor + Vec2::new(-83.3, -61.7); + let ev = |kind, q: Point| PointerEvent::new(kind, q.x, q.y); + let preview = |acts: &[Action]| { + acts.iter().find_map(|a| if let Action::Preview(_, v) = a { Some(v.clone()) } else { None }).unwrap_or_else(|| panic!("{acts:?}")) + }; + let at = |v: &serde_json::Value| Point::new(v[0].as_f64().unwrap(), v[1].as_f64().unwrap()); + for glyphs in [true, false] { + let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &on_type, &p) }; + let lands = |q: Point| q.distance(anchor) < 1e-9; + // The pointer hovering at the click shows the guides of the anchor a vector would start on. + let mut tool = GradientTool::default(); + tool.pointer(&c, &ev(PointerKind::Move, click)); + assert_eq!(!tool.overlays(&c).is_empty(), glyphs, "hover, Snap to Glyph {glyphs}"); + // A vector drawn from the click starts on the anchor. + tool.pointer(&c, &ev(PointerKind::Down, click)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click + Vec2::new(80.0, 60.0)))); + assert_eq!(lands(at(&v["start"])), glyphs, "start, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click + Vec2::new(80.0, 60.0))); + // One drawn to beside the slanted line, a quarter of the way along (15 pt or more from its + // bounds' edges, centre and anchors), ends on the line. + let dir = (a1 - a0) / (a1 - a0).hypot(); + let quarter = a0 + (a1 - a0) * 0.25; + tool.pointer(&c, &ev(PointerKind::Down, away)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, quarter + Vec2::new(-dir.y, dir.x) * 2.0))); + assert!((at(&v["end"]) - a0).cross(dir).abs() < 1e-6, "end on the line: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, quarter)); + // One drawn to the click ends on it. + tool.pointer(&c, &ev(PointerKind::Down, away)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click))); + assert_eq!(lands(at(&v["end"])), glyphs, "end, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click)); + // A click at the click applies a gradient that starts on the anchor. + tool.pointer(&c, &ev(PointerKind::Down, click)); + let acts = tool.pointer(&c, &ev(PointerKind::Up, click)); + let v = acts.iter().find_map(|a| if let Action::Exec(_, v) = a { Some(v.clone()) } else { None }).unwrap_or_else(|| panic!("{acts:?}")); + assert_eq!(lands(at(&v["start"])), glyphs, "click, Snap to Glyph {glyphs}: {v}"); + // One drawn to 2 px below the horizontal through its start, far from any target, ends on + // that construction guide, as a Line Segment's end does. + tool.pointer(&c, &ev(PointerKind::Down, away)); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, away + Vec2::new(150.0, 2.0)))); + assert!((at(&v["end"]).y - away.y).abs() < 1e-9, "end on the construction guide: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, away + Vec2::new(150.0, 2.0))); + // With Shift, one drawn at 45° toward the line's first anchor slides along that step onto it. + let shift = |kind, q: Point| PointerEvent { mods: crate::Mods { shift: true, ..Default::default() }, ..ev(kind, q) }; + tool.pointer(&c, &shift(PointerKind::Down, a0 + Vec2::new(60.0, -60.0))); + let v = preview(&tool.pointer(&c, &shift(PointerKind::Drag, a0 + Vec2::new(-1.5, 0.5)))); + assert!(at(&v["end"]).distance(a0) < 1e-9, "Shift end on the anchor: {v}"); + tool.pointer(&c, &shift(PointerKind::Up, a0)); + // A press 3 px from the anchor, then a move of 2 px away from it: no drag yet. + let near = anchor + Vec2::new(2.5, -1.5); + tool.pointer(&c, &ev(PointerKind::Down, near)); + let acts = tool.pointer(&c, &ev(PointerKind::Drag, near + Vec2::new(1.5, -1.0))); + assert!(acts.is_empty(), "drag start, Snap to Glyph {glyphs}: {acts:?}"); + tool.pointer(&c, &ev(PointerKind::Up, near + Vec2::new(1.5, -1.0))); + // The end handle of the rectangle's gradient dragged to the click lands on the anchor. + let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &on_rect, &p) }; + let mut tool = GradientTool::default(); + // Hovering over the handle (on the rectangle's edge) shows no guides. + tool.pointer(&c, &ev(PointerKind::Move, Point::new(400.0, 100.0))); + assert!(!tool.overlays(&c).iter().any(|o| matches!(o, Overlay::Label { .. })), "hover over the handle"); + tool.pointer(&c, &ev(PointerKind::Down, Point::new(400.0, 100.0))); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click))); + assert_eq!(lands(at(&v["end"])), glyphs, "end handle, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click)); + // The start handle dragged to the click lands on the anchor. + tool.pointer(&c, &ev(PointerKind::Down, Point::new(300.0, 100.0))); + let v = preview(&tool.pointer(&c, &ev(PointerKind::Drag, click))); + assert_eq!(lands(at(&v["start"])), glyphs, "start handle, Snap to Glyph {glyphs}: {v}"); + tool.pointer(&c, &ev(PointerKind::Up, click)); + // With nothing selected, hovering beside the rectangle (off the art and its 1 pt stroke, + // where a press draws nothing, but within the snapping tolerance of its edge) shows no + // guides. + let none = Selection::default(); + let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &none, &p) }; + let mut tool = GradientTool::default(); + tool.pointer(&c, &ev(PointerKind::Move, Point::new(296.2, 75.0))); + assert!(tool.overlays(&c).is_empty(), "hover off the art: {:?}", tool.overlays(&c)); + } +} + /// With Smart Guides off, View → Snap to Point still pulls a drawn point onto an anchor, but /// draws no Smart Guides: no "anchor" label, no lines. #[test] diff --git a/crates/tools/src/xform/gradient.rs b/crates/tools/src/xform/gradient.rs index 9c8881b580..9a61c98591 100644 --- a/crates/tools/src/xform/gradient.rs +++ b/crates/tools/src/xform/gradient.rs @@ -7,8 +7,10 @@ //! //! The annotator is a bar from a round start handle to a square end handle. Drag the start handle //! (or the bar) to move the gradient, the end handle to change its length and angle, and just past -//! the end to rotate it. Dragged handles snap to anchors, edges and smart guides; Shift instead -//! constrains the vector to 45° steps from the Constrain Angle preference. Colour stops sit under the bar: click the bar to add +//! the end to rotate it. A vector drawn on the art, a click that applies one, and the dragged +//! handles snap as drawn points do (the anchors and paths of the art, and with Snap to Glyph the +//! guides of live type); Shift keeps the dragged end or handle at 45° steps from the Constrain Angle +//! preference, snapping along that step. Colour stops sit under the bar: click the bar to add //! one, drag one to move it (Alt drags a copy), drag it off the bar to delete it, and drag a //! diamond above the bar to move a midpoint. Double-clicking a stop opens its popover. The //! selected stop (`gradient.selectStop`) is shared with the Gradient and Color panels: Delete or @@ -43,7 +45,7 @@ use vectorcraft_geom::{Affine, Point, Shape, Vec2, constrain_angle_from}; use super::freeform::{self, Annotator as FreeformAnnotator}; use super::paint_owner; -use crate::guides::snap_draw; +use crate::guides::{DrawSnap, Leave}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; const BAR: [u8; 3] = [0x20, 0x20, 0x20]; @@ -239,6 +241,8 @@ struct Gesture { grab: Grab, /// Where the press happened. at: Point, + /// Where a vector drawn on the art starts: the press, snapped. + start: Point, /// Moved past the drag threshold (an interaction is open). began: bool, /// The vector drawn so far (Art drags). @@ -255,8 +259,9 @@ pub struct GradientTool { free: Option, /// The line Lines mode is drawing on a freeform gradient. lines: freeform::Lines, - /// Smart-guide feedback of the handle being dragged. - guides: Vec, + /// Smart Guides for the vector being drawn or the handle being dragged, and for the pointer + /// hovering over the art. + snap: DrawSnap, } /// Overlays of an annotator: the bar, the handles, the midpoint diamonds and a chip per stop; a @@ -378,18 +383,40 @@ impl GradientTool { Some((a, Part::Aspect)) => Grab::Aspect(GradientGeom { aspect: a.geom.aspect.abs(), ..a.geom }), None => { if cx.selection.is_empty() { - let Some(h) = hit_test(cx.doc, p, cx.hit_options()) else { return out }; + let Some(h) = hit_test(cx.doc, p, cx.hit_options()) else { + self.snap.clear(); + return out; + }; out.push(Action::Exec("select.set".into(), json!({ "ids": [paint_owner(cx.doc, h.leaf).0] }))); } Grab::Art } }; - // A vector drawn on the art starts on the snapped point. - let at = if grab == Grab::Art { snap_draw(cx, p, &[]).0 } else { p }; - self.gesture = Some(Gesture { grab, at, began: false, vector: None, stop: None }); + // The targets are gathered for a drag that snaps. A vector drawn on the art starts on the + // snapped point; the drag threshold still counts from the press. + let snaps = matches!(grab, Grab::Art | Grab::Move { .. } | Grab::End(_)); + let snapped = if snaps { self.snap.press(cx, p, &[], None) } else { p }; + let start = if grab == Grab::Art { snapped } else { p }; + if grab != Grab::Art { + self.snap.clear(); + } + self.gesture = Some(Gesture { grab, at: p, start, began: false, vector: None, stop: None }); out } + /// Snap the pointer hovering where a press would draw a vector (anywhere with art selected, + /// over art with nothing selected), to show where the vector starts; no guides over the + /// annotator, elsewhere with nothing selected, or during a press. + fn hover(&mut self, cx: &ToolContext, p: Point) { + let on_annotator = || Annotator::of(cx).is_some_and(|a| a.hit(cx, p).is_some()); + let off_art = || cx.selection.is_empty() && hit_test(cx.doc, p, cx.hit_options()).is_none(); + if self.gesture.is_some() || self.free.is_some() || FreeformAnnotator::of(cx).is_some() || on_annotator() || off_art() { + self.snap.clear(); + } else { + self.snap.hover(cx, p, &[], None); + } + } + fn drag(&mut self, cx: &ToolContext, p: Point, mods: Mods) -> Vec { let Some(g) = &mut self.gesture else { return vec![] }; let mut out = vec![]; @@ -401,23 +428,16 @@ impl GradientTool { out.push(Action::Begin("Gradient".into())); } let constrain = |v: Vec2| constrain_angle_from(v, 45.0, cx.constrain_angle); - // Where a handle dragged to `to` lands: Shift constrains the vector from `from`, else it - // snaps (keeping the guides to show). - let mut guides = vec![]; - let mut place = |from: Point, to: Point| { - if mods.shift { - from + constrain(to - from) - } else { - let (q, o) = snap_draw(cx, to, &[]); - guides = o; - q - } - }; + // Where a handle dragged to `to` lands: it snaps as a drawn segment's end leaving `from` + // does, Shift keeping it at 45° steps from `from` (keeping the guides to show). + let snap = &mut self.snap; + snap.clear(); + let mut place = |from: Point, to: Point| snap.drag(cx, to, Some(&Leave::segment(cx, from, mods.shift))); let (cmd, params) = match &g.grab { Grab::Art => { - let end = place(g.at, p); - g.vector = Some((g.at, end)); - ("paint.setGradientGeom", Self::geom_params(cx, g.at, end)) + let end = place(g.start, p); + g.vector = Some((g.start, end)); + ("paint.setGradientGeom", Self::geom_params(cx, g.start, end)) } Grab::Move { geom, .. } => { // The start handle snaps; Shift constrains the move. @@ -469,13 +489,12 @@ impl GradientTool { ("paint.editGradient", Self::stops_params(cx, &set_midpoint(stops, *index, m))) } }; - self.guides = guides; out.push(Action::Preview(cmd.into(), params)); out } fn release(&mut self, cx: &ToolContext, p: Point) -> Vec { - self.guides.clear(); + self.snap.clear(); let Some(g) = self.gesture.take() else { return vec![] }; if g.began { let mut out = vec![Action::Commit]; @@ -494,12 +513,12 @@ impl GradientTool { let (stops, i) = insert_stop(&a.gradient, t); vec![Action::Exec("paint.editGradient".into(), Self::stops_params(cx, &stops)), Self::select_stop(i)] } - // A click inside selected art applies the gradient from that point. + // A click inside selected art applies the gradient from that point, snapped. Grab::Art => { let Some(h) = hit_test(cx.doc, p, cx.hit_options()) else { return vec![] }; let id = paint_owner(cx.doc, h.leaf); let Some(v) = cx.selection.objects.contains(&id).then(|| click_vector(cx, id)).flatten() else { return vec![] }; - let mut params = Self::geom_params(cx, p, p + v); + let mut params = Self::geom_params(cx, g.start, g.start + v); // Without `ids`, the engine refuses the gradient while an object of a dynamic symbol // instance's art is selected. if cx.selection.symbol_part.is_some() { @@ -550,6 +569,9 @@ impl Tool for GradientTool { } _ => {} } + if matches!(ev.kind, PointerKind::Move) { + self.hover(cx, ev.pos); + } match ev.kind { PointerKind::Down => self.press(cx, ev.pos, ev.mods), PointerKind::Drag => self.drag(cx, ev.pos, ev.mods), @@ -579,14 +601,19 @@ impl Tool for GradientTool { return self.key(cx, ToolKey::Escape, Mods::default()); } (self.gesture, self.free) = (None, None); - self.guides.clear(); + self.snap.clear(); + vec![] + } + + fn deactivate(&mut self, _cx: &ToolContext) -> Vec { + self.snap.clear(); vec![] } fn key(&mut self, cx: &ToolContext, key: ToolKey, mods: Mods) -> Vec { if key == ToolKey::Escape && self.busy() { self.gesture = None; - self.guides.clear(); + self.snap.clear(); return self.free.take().map_or_else(|| vec![Action::Cancel], |g| freeform::cancel(&g)); } if self.busy() || !self.claims_key(cx, key) { @@ -626,7 +653,7 @@ impl Tool for GradientTool { } _ => vec![], }; - o.extend(self.guides.iter().cloned()); + o.extend(self.snap.guides().iter().cloned()); o } diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index 5ac4025c24..fc06926247 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -30,7 +30,8 @@ pub fn anchor_controls(app: &VectorcraftApp) -> AnchorControls { /// What the Control bar and the Properties panel offer for selected anchors ([`anchor_controls`]): /// "Convert:" corner or smooth, then "Anchors:" remove, connect (Join) and cut. Each group is a -/// row of its own: inline in the Control bar, one under the other in a panel. +/// row of its own: inline in the Control bar, one under the other in a panel. In the Control bar's +/// row, a group shows only while the row has room for its label and all its buttons. pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorControls) { let t = Tokens::get(ui.ctx()); let mut run = None; @@ -56,9 +57,14 @@ pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorCon AnchorControls::Convert => 1, AnchorControls::All => groups.len(), }; + let in_row = ui.layout().main_dir() == egui::Direction::LeftToRight; for (label, buttons) in groups.into_iter().take(shown) { + let text = egui::RichText::new(label).size(12.0).color(t.text); + if in_row && ui.available_width() < row_width(ui, &[widgets::text_width(ui, text.clone())]) + buttons_width(ui, buttons.len()) { + break; + } ui.horizontal(|ui| { - ui.label(egui::RichText::new(label).size(12.0).color(t.text)); + ui.label(text); for &(icon, tip, id, to) in buttons { if widgets::icon_button(ui, icon, tip, false, 24.0).clicked() { run = Some((id, to.map_or_else(|| json!({}), |to| json!({ "to": to })))); @@ -78,6 +84,12 @@ pub(super) const ISOLATION_BUTTON: &str = "cb-isolation"; /// less, the menu is left out so those controls still fit the narrowest window. const ARTBOARD_SNAPPING_WIDTH: f32 = 900.0; +/// The width of [`snapping_menu`]'s button: its label, as a button draws it, and the button's padding. +fn snapping_width(ui: &Ui) -> f32 { + let label = egui::WidgetText::from(tl!("Snapping")).into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Button); + label.size().x + 2.0 * ui.spacing().button_padding.x +} + /// The Control bar's snapping quick access; every change uses its existing command, and Pixel /// Snapping Options… opens the dialog of `view.snapToPixel.options`. fn snapping_menu(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -115,9 +127,10 @@ fn snapping_menu(app: &mut VectorcraftApp, ui: &mut Ui) { /// while a symbol's art is the isolated layer. Otherwise, with one object selected that /// [`vectorcraft_tools::select::isolates`], it is Isolate Selected Object, which also isolates a /// path inside a group that Direct Selection selected. An envelope's contents get no button: the -/// envelope's Edit Envelope and Edit Contents buttons show instead. +/// envelope's Edit Envelope and Edit Contents buttons show instead. It is left out when the bar has +/// no room left for it. fn isolation_button(app: &mut VectorcraftApp, ui: &mut Ui) { - let Some(st) = app.session.active() else { return }; + let Some(st) = app.session.active().filter(|_| ui.available_width() >= BUTTON) else { return }; let (icon, tip, id) = if st.isolation.is_some() { if st.doc.pattern_edit.is_some() { return; @@ -151,9 +164,9 @@ const GLOBAL_EDIT_OPTIONS_WIDTH: f32 = 24.0; /// It opens the Properties panel's Global Edit Options, which set the same session options, and is /// off and closed while Global Edit is on, as that panel's dropdown is. It sits after Recolor /// Artwork, ahead of the X/Y/W/H fields that the bar leaves out when it runs short of room, and is -/// left out only when no room is left for it. -fn global_edit_options(app: &mut VectorcraftApp, ui: &mut Ui) { - if !crate::menus::enabled(app, "select.globalEdit") || ui.available_width() < GLOBAL_EDIT_OPTIONS_WIDTH { +/// left out when the room left, less `reserve` ([`transform_reserve`]), cannot hold it. +fn global_edit_options(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) { + if !crate::menus::enabled(app, "select.globalEdit") || ui.available_width() < row_width(ui, &[GLOBAL_EDIT_OPTIONS_WIDTH]) + reserve { return; } let on = app.session.active().is_some_and(|d| d.global_edit.is_some()); @@ -204,8 +217,27 @@ fn buttons_width(ui: &Ui, n: usize) -> f32 { (BUTTON + ui.spacing().item_spacing.x) * n as f32 } +/// The room controls `widths` wide take side by side in the bar's row, with the spacing after each. +fn row_width(ui: &Ui, widths: &[f32]) -> f32 { + widths.iter().map(|w| w + ui.spacing().item_spacing.x).sum() +} + +/// The size of the Control bar's Fill, Stroke and Graphic Style chips. +const PAINT_CHIP: f32 = 22.0; + +/// The width each of those chips takes with the chevron after it. +const CHIP_WIDTH: f32 = PAINT_CHIP + 16.0; + +/// The widths of the Control bar's stroke weight and Opacity fields. +const WEIGHT_FIELD_WIDTH: f32 = 100.0; +const OPACITY_FIELD_WIDTH: f32 = 56.0; + /// The separator that ends a group of the Control bar's controls, as [`group_end_width`] measures it. +/// It is left out when the bar has no room left for it. fn group_end(ui: &mut Ui) { + if ui.available_width() < group_end_width(ui) { + return; + } ui.add_space(GROUP_GAP); boundary(ui, separator); } @@ -256,8 +288,11 @@ fn boundary(ui: &mut Ui, draw: impl FnOnce(&mut Ui)) { } /// The separator that ends a group of the Control bar's controls, drawn as `ui.separator()` draws -/// one, and left out when no control follows it ([`boundary`]). +/// one, and left out when no control follows it ([`boundary`]) or the row has no room left for it. pub(crate) fn group_separator(ui: &mut Ui) { + if ui.layout().main_dir() == egui::Direction::LeftToRight && ui.available_width() < separator_width(ui) { + return; + } boundary(ui, |ui| { ui.separator(); }); @@ -298,20 +333,30 @@ fn end_width(app: &VectorcraftApp, ui: &Ui, nothing_selected: bool) -> f32 { } } +/// The room the Control bar's Fill and Stroke, Opacity, Graphic Style and Character groups leave +/// for the Transform link: the link and the gap and separator ahead of it, while something is +/// `selected` and the panel menu has the Transform group on, or none. Those groups show each control +/// only while this room is left after it, so the bar leaves them out before the link. +fn transform_reserve(app: &VectorcraftApp, ui: &Ui, selected: bool) -> f32 { + if selected && group_on(app, "transform") { group_end_width(ui) + crate::panels::transform::link_width(ui) } else { 0.0 } +} + /// Draws the Control bar's groups between its Character controls and the Transform link, and /// returns whether the Transform link and X/Y/W/H follow (false: nothing is selected, and Document /// Setup and Preferences, drawn here, end the bar, or the Transform group is off). The groups are /// the Paragraph section and area type's vertical alignment (#3759), then the Align buttons, the /// Align To menu and, for two or more objects, the Distribute buttons (a single object keeps the bar /// as it was); each shows only while the end of the bar keeps its room. Groups the panel menu has -/// off are left out, and take no room. -fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize) -> bool { +/// off are left out, and take no room. Without `room` (the Character group had none), all of these +/// groups are left out, so the bar trims them from its right end before the Character group. +fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize, room: bool) -> bool { let nothing = objects == 0; let end = end_width(app, ui, nothing); - if group_on(app, "paragraph") && crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end) { + if room && group_on(app, "paragraph") && crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end) { group_end(ui); } - if group_on(app, "verticalAlignment") + if room + && group_on(app, "verticalAlignment") && crate::panels::properties::vertical_align::shown(app).is_some() && ui.available_width() >= buttons_width(ui, 4) + group_end_width(ui) + end && crate::panels::properties::vertical_align::buttons(app, ui, BUTTON) @@ -319,11 +364,13 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize group_end(ui); } if nothing { + // Each button shows only while the bar has room left for it, so Preferences can show + // when Document Setup did not fit. if group_on(app, "documentOptions") { - if widgets::flat_button(ui, tl!("Document Setup"), DOCUMENT_SETUP_WIDTH).clicked() { + if ui.available_width() >= DOCUMENT_SETUP_WIDTH && widgets::flat_button(ui, tl!("Document Setup"), DOCUMENT_SETUP_WIDTH).clicked() { app.run("file.documentSetup", json!({})).ok(); } - if widgets::flat_button(ui, tl!("Preferences"), PREFERENCES_WIDTH).clicked() { + if ui.available_width() >= PREFERENCES_WIDTH && widgets::flat_button(ui, tl!("Preferences"), PREFERENCES_WIDTH).clicked() { app.run("edit.preferences", json!({})).ok(); } } @@ -331,7 +378,7 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize } let transform = group_on(app, "transform"); let end = if transform { separator_width(ui) + end } else { 0.0 }; - if group_on(app, "alignDistribute") && ui.available_width() >= buttons_width(ui, 7) + end { + if room && group_on(app, "alignDistribute") && ui.available_width() >= buttons_width(ui, 7) + end { crate::panels::align::align_buttons(app, ui, BUTTON); if ui.available_width() >= crate::panels::align::align_to_menu_width(BUTTON) + ui.spacing().item_spacing.x + end { crate::panels::align::align_to_menu(app, ui, BUTTON); @@ -545,55 +592,80 @@ fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { /// The Control bar's Fill and Stroke group: the fill and stroke chips, the Stroke link, the weight /// and the width profile, and the separator after them while a control follows ([`group_separator`]). -fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui) { +/// Short of room, the bar leaves out the width profile first, then the Stroke link with the weight, +/// then the two chips; each shows only while the bar has room for all of it and `reserve` after it +/// ([`transform_reserve`]). +fn fill_and_stroke(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) { + if ui.available_width() < row_width(ui, &[CHIP_WIDTH, CHIP_WIDTH]) + reserve { + return; + } let shown_stroke = crate::panels::current_stroke(app); let mixed = crate::panels::stroke_mixed(app, ui.ctx()); let weight = stroke_panel::shown_weight(app, shown_stroke.as_ref(), &mixed); - crate::panels::paint_chip(app, ui, false, 22.0, true); - crate::panels::paint_chip(app, ui, true, 22.0, true); + crate::panels::paint_chip(app, ui, false, PAINT_CHIP, true); + crate::panels::paint_chip(app, ui, true, PAINT_CHIP, true); // The link opens the Stroke panel as a popover under it; then the weight spinner // (with presets) and the width profile. - stroke_panel::link(app, ui, tl!("Stroke:")); - stroke_panel::weight_field(app, ui, "cb-stroke", weight, 100.0); - if let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) { - app.run("stroke.set", json!({"profile": id})).ok(); + let link = widgets::text_width(ui, stroke_panel::link_text(ui, tl!("Stroke:"))); + if ui.available_width() >= row_width(ui, &[link, WEIGHT_FIELD_WIDTH]) + reserve { + stroke_panel::link(app, ui, tl!("Stroke:")); + stroke_panel::weight_field(app, ui, "cb-stroke", weight, WEIGHT_FIELD_WIDTH); + if ui.available_width() >= row_width(ui, &[stroke_panel::PROFILE_DROPDOWN_WIDTH]) + reserve + && let Some(id) = stroke_panel::profile_dropdown(app, ui, shown_stroke.as_ref().and_then(|s| s.profile.as_ref())) + { + app.run("stroke.set", json!({"profile": id})).ok(); + } } - ui.add_space(4.0); - group_separator(ui); + group_end(ui); } /// The Control bar's Opacity group (its link and field, showing `opacity`) and, with something /// `selected`, its Graphic Style group (Style, Recolor Artwork and Global Edit Options), each while -/// the panel menu has it on, and the separator after them while a control follows. -fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: f32, selected: bool) { +/// the panel menu has it on, and the separator after them while a control follows. Short of room, +/// the bar leaves out Global Edit Options first, then Recolor Artwork, then the Style link with its +/// chip. The Opacity link with its field and the Style link with its chip each show only while the +/// bar has room for all of it and `reserve` after it ([`transform_reserve`]), so the narrower Style +/// part can show without the Opacity part. +fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity: f32, selected: bool, reserve: f32) { let (opacity_on, style_on) = (group_on(app, "opacity"), group_on(app, "graphicStyle") && selected); + let mut drawn = false; if opacity_on { - // A click opens the Transparency panel; a drag scrubs the field (#4088). - if crate::scrub::link_label(ui, egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline()).clicked() { - app.ui.open_panel = Some("transparency".into()); - } - if let Some(o) = widgets::plain_field(ui, "cb-opacity", opacity as f64 * 100.0, "%", 0, 56.0) - && selected - { - app.run("transparency.set", json!({"opacity": o.clamp(0.0, 100.0)})).ok(); + let text = egui::RichText::new(tl!("Opacity:")).size(12.0).color(t.text).underline(); + if ui.available_width() >= row_width(ui, &[widgets::text_width(ui, text.clone()), OPACITY_FIELD_WIDTH]) + reserve { + // A click opens the Transparency panel; a drag scrubs the field (#4088). + if crate::scrub::link_label(ui, text).clicked() { + app.ui.open_panel = Some("transparency".into()); + } + if let Some(o) = widgets::plain_field(ui, "cb-opacity", opacity as f64 * 100.0, "%", 0, OPACITY_FIELD_WIDTH) + && selected + { + app.run("transparency.set", json!({"opacity": o.clamp(0.0, 100.0)})).ok(); + } + drawn = true; } } - if style_on { + let text = egui::RichText::new(tl!("Style:")).size(12.0).color(t.text).underline(); + if style_on && ui.available_width() >= GROUP_GAP + row_width(ui, &[widgets::text_width(ui, text.clone()), CHIP_WIDTH]) + reserve { // Style picker: the selection's graphic style; its menu applies another. - ui.add_space(4.0); - if ui.link(egui::RichText::new(tl!("Style:")).size(12.0).color(t.text).underline()).clicked() { + ui.add_space(GROUP_GAP); + if ui.link(text).clicked() { app.ui.open_panel = Some("graphicStyles".into()); } - let resp = widgets::chip_button_named(ui, 22.0, true, tl!("Graphic Style"), |ui, r| crate::panels::graphic_styles::paint_linked(app, ui, r)); + let resp = + widgets::chip_button_named(ui, PAINT_CHIP, true, tl!("Graphic Style"), |ui, r| crate::panels::graphic_styles::paint_linked(app, ui, r)); egui::Popup::menu(&resp).show(|ui| crate::panels::graphic_styles::picker(app, ui)); // Recolor Artwork, when the selection has colours to recolour. let colored = app.session.active().is_some_and(|st| crate::canvas::has_colors(&st.doc, &st.selection.objects)); - if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 24.0).clicked() { + if colored + && ui.available_width() >= buttons_width(ui, 1) + reserve + && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, BUTTON).clicked() + { crate::menus::invoke(app, "ui.recolorDialog", json!({})); } - global_edit_options(app, ui); + global_edit_options(app, ui, reserve); + drawn = true; } - if opacity_on || style_on { + if drawn { group_separator(ui); } } @@ -709,11 +781,16 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, } gripper(app, ui, t, bar, dock_room); if app.session.active().is_none() { - ui.label(egui::RichText::new(tl!("No Document")).color(t.text_dim)); + let text = egui::RichText::new(tl!("No Document")).color(t.text_dim); + if ui.available_width() >= widgets::text_width(ui, text.clone()) { + ui.label(text); + } return; } // The Artboard tool's controls fill the narrowest window, so they keep it to themselves there. - if group_on(app, "snapping") && (app.session.tool_id() != "artboard" || ui.available_width() >= ARTBOARD_SNAPPING_WIDTH) { + // Any other tool's bar shows the menu while it has room for it. + let snapping_room = if app.session.tool_id() == "artboard" { ARTBOARD_SNAPPING_WIDTH } else { snapping_width(ui) }; + if group_on(app, "snapping") && ui.available_width() >= snapping_room { snapping_menu(app, ui); } let Some(st) = app.session.active() else { return }; @@ -739,7 +816,10 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, _ => tl!(crate::panels::appearance::object_label(app)), }; let has_first = first.is_some(); - ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text)); + let label = egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text); + if ui.available_width() >= widgets::text_width(ui, label.clone()) { + ui.label(label); + } // With the Artboard tool in use, the bar shows the active artboard's controls in place // of the object controls. let (artboard_options, transform) = (group_on(app, "artboardOptions"), group_on(app, "transform")); @@ -755,7 +835,9 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, if object_options { isolation_button(app, ui); } - ui.add_space(6.0); + if ui.available_width() >= 6.0 { + ui.add_space(6.0); + } // An image or an Image Trace object shows its own controls in place of Fill and Stroke, // and leaves Fill and Stroke out while the Image Options group is off too. let image = if group_on(app, "imageOptions") { @@ -771,19 +853,26 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect, crate::panels::symbols::control_bar(app, ui); } let opacity = if has_first { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 }; + let reserve = transform_reserve(app, ui, !sel.is_empty()); if !image && group_on(app, "fillStroke") { - fill_and_stroke(app, ui); + fill_and_stroke(app, ui, reserve); } - opacity_and_style(app, ui, t, opacity, !sel.is_empty()); - if group_on(app, "character") { - crate::panels::character::control_bar(app, ui); - } - if !groups_before_transform(app, ui, sel.len()) { + opacity_and_style(app, ui, t, opacity, !sel.is_empty(), reserve); + // The Character group shows whole, with the Transform link's room left after it, or not at + // all; left out, it leaves out the groups between it and the link too. + let room = !group_on(app, "character") || crate::panels::character::control_bar(app, ui, reserve); + if !groups_before_transform(app, ui, sel.len(), room) { erase_trailing(ui); return; } // The Transform link opens the whole Transform panel (reference point, rotate, - // shear, options) in a popover; X/Y/W/H follow inline while the bar has room. + // shear, options) in a popover; X/Y/W/H follow inline while the bar has room. The + // groups from Fill and Stroke on keep its room, but the controls ahead of them can + // still take it. + if ui.available_width() < crate::panels::transform::link_width(ui) { + erase_trailing(ui); + return; + } crate::panels::transform::link(app, ui); // The bounding box, rotated with rotated objects: its centre and its own sides. if let Some(b) = app.selection_box() diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index 052c7ec230..be37f10a29 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -3037,6 +3037,127 @@ fn no_separator_is_left_at_the_end_of_the_control_bar() { assert!(stray.is_empty(), "a separator with nothing after it: {stray:#?}"); } +/// The shapes of the Control bar's controls drawn in window `screen` that reach past the left edge +/// of its panel menu button, or past the room a floating bar clips its controls to, or `None` when +/// the bar leaves the button out. +fn control_bar_overflow(app: &mut crate::VectorcraftApp, ctx: &egui::Context, screen: egui::Rect) -> Option> { + control_bar_frame_in(app, ctx, screen, vec![]); + let out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(app, ui)); + let nodes: Vec<_> = out.platform_output.accesskit_update.map(|u| u.nodes.into_iter().map(|(_, n)| n).collect()).unwrap_or_default(); + let menu_left = nodes.iter().find(|n| n.label().is_some_and(|l| l == "Panel menu")).and_then(|n| n.bounds())?.x0 as f32; + // The bar's frame spans the whole bar, the panel menu button included. + let bar_width = if app.ui.control_bar_floating { screen.width().min(800.0) } else { screen.width() }; + let shapes = out.shapes.iter().filter_map(|cs| { + let r = cs.shape.visual_bounding_rect(); + let control = r.is_finite() && cs.clip_rect.intersects(r) && r.width() < bar_width - 4.0 && r.min.x < menu_left - 0.5; + let clipped = cs.clip_rect.right() < menu_left && r.right() > cs.clip_rect.right() + 0.5; + (control && (r.right() > menu_left + 0.5 || clipped)).then_some(r) + }); + Some(shapes.collect()) +} + +/// In a narrow window, the Control bar leaves out a control it has no room for, Fill and Stroke, +/// Opacity and Character included: every control it shows is drawn whole, left of the panel menu button, which +/// shows in every width, with the bar docked to the top or the bottom or floating. +#[test] +fn the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window() { + use crate::tests_removeanchors::{control_frame, has}; + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let mut faults = vec![]; + let mut check = |app: &mut crate::VectorcraftApp, case: &str| { + for (bottom, floating) in [(false, false), (true, false), (false, true)] { + (app.ui.control_bar_bottom, app.ui.control_bar_floating) = (bottom, floating); + for w in (150..=1500).step_by(9) { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w as f32, 300.0)); + match control_bar_overflow(app, &ctx, screen) { + None => faults.push(format!("{case}, bottom {bottom}, floating {floating}, {w} wide: no panel menu")), + Some(over) if !over.is_empty() => faults.push(format!("{case}, bottom {bottom}, floating {floating}, {w} wide: {over:?}")), + Some(_) => continue, + } + break; + } + } + (app.ui.control_bar_bottom, app.ui.control_bar_floating) = (false, false); + }; + app.run("select.set", json!({ "ids": [id] })).unwrap(); + let bar = control_frame(&mut app, &ctx, vec![]); + assert!(has(&bar, "Stroke:") && has(&bar, "Opacity:"), "Fill and Stroke and Opacity show in a wide window: {bar:?}"); + check(&mut app, "a rectangle selected"); + app.select_tool("directSelection"); + app.run("select.anchors", json!({"id": id, "anchors": [[0, 0]]})).unwrap(); + check(&mut app, "an anchor selected"); + app.select_tool("selection"); + let text = app.run("text.create", json!({"x": 20, "y": 120, "text": "Point"})).unwrap()["id"].clone(); + app.run("select.set", json!({ "ids": [text] })).unwrap(); + let bar = control_frame(&mut app, &ctx, vec![]); + assert!(has(&bar, "Character:"), "the Character group shows in a wide window: {bar:?}"); + check(&mut app, "type selected"); + app.run("select.set", json!({ "ids": [] })).unwrap(); + check(&mut app, "nothing selected"); + assert!(faults.is_empty(), "a control drawn past the bar's room: {faults:#?}"); +} + +/// With Opacity and Graphic Style on, a bar too narrow for the Opacity link and its field still +/// shows the narrower Style link and its chip when it has room for them. +#[test] +fn a_narrow_control_bar_shows_the_style_link_without_the_opacity_field() { + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone(); + app.run("select.set", json!({ "ids": [id] })).unwrap(); + app.run("window.control.options", json!({"group": "fillStroke", "visible": false})).unwrap(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let shows = |nodes: &[egui::accesskit::Node], name: &str| nodes.iter().any(|n| n.label().is_some_and(|l| l == name)); + let (mut both, mut style_only) = (false, false); + for w in (150..=900).step_by(2) { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w as f32, 300.0)); + control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let nodes = control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let (opacity, style) = (shows(&nodes, "Opacity:"), shows(&nodes, "Style:")); + both |= opacity && style; + style_only |= style && !opacity; + } + assert!(both, "a wide bar shows the Opacity and Style links"); + assert!(style_only, "some width has room for the Style link and its chip but not the Opacity link and its field"); +} + +/// With nothing selected, a bar too narrow for the Document Setup button still shows the narrower +/// Preferences button when it has room for it. +#[test] +fn a_narrow_control_bar_shows_preferences_without_document_setup() { + crate::i18n::set_current(crate::i18n::Lang::EN); + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + app.run("select.set", json!({ "ids": [] })).unwrap(); + for group in ["fillStroke", "opacity", "graphicStyle"] { + app.run("window.control.options", json!({"group": group, "visible": false})).unwrap(); + } + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + let shows = |nodes: &[egui::accesskit::Node], name: &str| nodes.iter().any(|n| n.label().is_some_and(|l| l == name)); + let (mut both, mut preferences_only) = (false, false); + for w in (150..=900).step_by(2) { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(w as f32, 300.0)); + control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let nodes = control_bar_frame_in(&mut app, &ctx, screen, vec![]); + let (setup, preferences) = (shows(&nodes, "Document Setup"), shows(&nodes, "Preferences")); + both |= setup && preferences; + preferences_only |= preferences && !setup; + } + assert!(both, "a wide bar shows Document Setup and Preferences"); + assert!(preferences_only, "some width has room for Preferences but not Document Setup"); +} + /// The Anchor Points group holds Convert and Anchors for direct-selected anchors; off, the bar keeps /// its Anchor Point label. #[test] diff --git a/crates/ui-egui/src/document_drag.rs b/crates/ui-egui/src/document_drag.rs index 2784f29eef..b44d09b5c3 100644 --- a/crates/ui-egui/src/document_drag.rs +++ b/crates/ui-egui/src/document_drag.rs @@ -1,8 +1,10 @@ //! A selection dragged across document tabs remains editable and leaves its source intact. +use std::sync::Arc; + use egui::{Context, Id, Response}; use serde_json::json; -use vectorcraft_doc::NodeId; +use vectorcraft_doc::{Document, NodeId}; use vectorcraft_geom::Point; use crate::{VectorcraftApp, canvas, widgets}; @@ -19,7 +21,11 @@ struct Origin { struct Artwork { origin: Origin, ids: Vec, - revision: u64, + /// The source document when the drag left it. The drag ends when the source's content differs + /// from it ([`vectorcraft_engine::DocState::same_content`]). A redraw that raises its revision + /// (another document's assigned color profiles brought to the front) and a change that is not + /// an edit (its type measured again) keep it. + source: Arc, target: u64, } @@ -75,7 +81,7 @@ pub(crate) fn cleanup(app: &VectorcraftApp, ctx: &Context) { } }); if let Some(art) = egui::DragAndDrop::payload::(ctx) { - let valid = app.session.documents().iter().any(|d| d.uid == art.origin.uid && d.revision == art.revision) + let valid = app.session.documents().iter().any(|d| d.uid == art.origin.uid && d.same_content(&art.source)) && app.session.active().is_some_and(|d| d.uid == art.target); let cancel = ctx.input(|i| { !i.focused @@ -143,13 +149,14 @@ pub(crate) fn hover_tab(app: &mut VectorcraftApp, resp: &Response, win: crate::d return true; } // Alt-drag previews use temporary duplicate ids; only the restored originals travel. - let ids = app.session.active().map(|d| d.selection.objects.clone()).unwrap_or_default(); + let Some((ids, source)) = app.session.active().map(|d| (d.selection.objects.clone(), d.doc.clone())) else { + return true; + }; if ids.is_empty() { return true; } canvas::end_document_drag(ctx); - let revision = app.session.active().map(|d| d.revision).unwrap_or_default(); - egui::DragAndDrop::set_payload(ctx, Artwork { origin, ids, revision, target: uid }); + egui::DragAndDrop::set_payload(ctx, Artwork { origin, ids, source, target: uid }); } if transfer && let Some(art) = egui::DragAndDrop::payload::(ctx) { let mut art = (*art).clone(); diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index c098a7cb4b..99ecf7dd95 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5292,6 +5292,7 @@ digits Malayalam Malaiàlam @msg `{key}` must be a #rrggbb colour `{key}` ha de ser un color #rrggbb @msg `{key}` must be a number `{key}` ha de ser un nombre @msg `{key}` must be a number not below 0 `{key}` ha de ser un nombre no inferior a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` han de ser quatre nombres no inferiors a 0: [tl, tr, br, bl] @msg `{key}` must be a number, not {v} `{key}` ha de ser un nombre, no {v} @msg `{key}` must be a string `{key}` ha de ser una cadena @msg `{key}` must be a profile name or null `{key}` ha de ser un nom de perfil o null diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index bfe8615b9c..d58e5bd051 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4799,6 +4799,7 @@ imageTrace Outline Obrys @msg `select` must be true or false `select` musí být true nebo false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` a `maxAnchors` musí být celá čísla @msg `{key}` must be a number not below 0 `{key}` musí být číslo nejméně 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` musí být čtyři čísla nejméně 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musí být [x, y, width, height]: čtyři konečná čísla, šířka a výška nesmí být záporné @msg `{min_key}` is greater than `{max_key}` `{min_key}` je větší než `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` zadejte alespoň jeden z parametrů `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index e33ed2a7a9..0143f55457 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4412,6 +4412,7 @@ theme Appearance Erscheinungsbild @msg `select` must be true or false `select` muss true oder false sein @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` und `maxAnchors` müssen ganze Zahlen sein @msg `{key}` must be a number not below 0 `{key}` muss eine Zahl nicht kleiner als 0 sein +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` muss aus vier Zahlen nicht kleiner als 0 bestehen: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` muss [x, y, width, height] sein: vier endliche Zahlen, Breite und Höhe nicht negativ @msg `{min_key}` is greater than `{max_key}` `{min_key}` ist größer als `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` geben Sie mindestens einen der Parameter `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` an diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 9ff2eae9a2..172e91d0ef 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2559,6 +2559,7 @@ axis Both Και τα δύο @msg `select` must be true or false Το `select` πρέπει να είναι true ή false @msg `minAnchors` and `maxAnchors` must be whole numbers Τα `minAnchors` και `maxAnchors` πρέπει να είναι ακέραιοι αριθμοί @msg `{key}` must be a number not below 0 Το `{key}` πρέπει να είναι αριθμός όχι μικρότερος από 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] Το `{key}` πρέπει να είναι τέσσερις αριθμοί όχι μικρότεροι από 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative Το `{key}` πρέπει να είναι [x, y, width, height]: τέσσερις πεπερασμένοι αριθμοί, με πλάτος και ύψος όχι αρνητικά @msg `{min_key}` is greater than `{max_key}` Το `{min_key}` είναι μεγαλύτερο από το `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` δώστε τουλάχιστον ένα από τα `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e51ba240a8..865b2b532f 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4489,6 +4489,7 @@ axis Both Ambos @msg `select` must be true or false `select` debe ser true o false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` y `maxAnchors` deben ser números enteros @msg `{key}` must be a number not below 0 `{key}` debe ser un número no inferior a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` debe ser cuatro números no inferiores a 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` debe ser [x, y, width, height]: cuatro números finitos, con anchura y altura no negativas @msg `{min_key}` is greater than `{max_key}` `{min_key}` es mayor que `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica al menos uno de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 2661bf3191..e40c199f74 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4713,6 +4713,7 @@ imageTrace Outline Ääriviiva @msg `select` must be true or false `select` on oltava true tai false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ja `maxAnchors` on oltava kokonaislukuja @msg `{key}` must be a number not below 0 `{key}` on oltava luku, joka on vähintään 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` on oltava neljä lukua, jotka ovat vähintään 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` on oltava [x, y, width, height]: neljä äärellistä lukua, joista leveys ja korkeus eivät ole negatiivisia @msg `{min_key}` is greater than `{max_key}` `{min_key}` on suurempi kuin `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` anna vähintään yksi parametreista `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a3d1c9b785..0063c984a1 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4478,6 +4478,7 @@ axis Both Les deux @msg `select` must be true or false `select` doit valoir true ou false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` et `maxAnchors` doivent être des nombres entiers @msg `{key}` must be a number not below 0 `{key}` doit être un nombre supérieur ou égal à 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` doit être quatre nombres supérieurs ou égaux à 0 : [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` doit être [x, y, width, height] : quatre nombres finis, largeur et hauteur non négatives @msg `{min_key}` is greater than `{max_key}` `{min_key}` est supérieur à `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indiquez au moins l’un des paramètres `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -7484,6 +7485,1810 @@ breakText Glyphs Glyphes Margin Marge Box Cadre Swatch Nuance +@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions 'caller', 'callee' et 'arguments' ne peuvent pas être utilisés sur les fonctions en mode strict +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. Un profil accepte un seul effet Révolution. Modifiez plutôt les options de l’effet existant. +@msg Add Points at Crossings: a path went missing Ajouter des points aux intersections : un tracé a disparu +@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean.prototype.{name} a été appelé sur une valeur qui n’est pas un booléen +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB Les images CMJN dont les échantillons n’ont pas pu être lus en CMJN ont été importées en RVB +@msg CSS options: {e} Options CSS : {e} +@msg CSV has a header but no data rows Le fichier CSV a un en-tête mais aucune ligne de données +@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark Le fichier CSV contient des octets NUL : enregistrez-le en UTF-8, ou en UTF-16 avec une marque d’ordre des octets +@msg CSV is empty Le fichier CSV est vide +@msg CSV is larger than {MAX_CSV_BYTES} bytes Le fichier CSV dépasse {MAX_CSV_BYTES} octets +@msg CSV is not UTF-16 Le fichier CSV n’est pas en UTF-16 +@msg CSV is not UTF-8 Le fichier CSV n’est pas en UTF-8 +@msg CSV would be larger than {MAX_CSV_BYTES} bytes Le fichier CSV dépasserait {MAX_CSV_BYTES} octets +@msg Cannot write to the location of “{name}”; choose where to save it Impossible d’écrire à l’emplacement de « {name} » ; choisissez où l’enregistrer +@msg Choose original artwork, not a dimension's generated paths Choisissez l’illustration d’origine, pas les tracés générés d’une cote +@msg Contour: select paths or shapes Contour : sélectionnez des tracés ou des formes +@msg Contour: the Offset Path effect is not available Contour : l’effet Décalage n’est pas disponible +@msg Create needs fills, strokes or both La création nécessite des fonds, des contours ou les deux +@msg DCS files can only be placed linked: use File › Place with Link on Les fichiers DCS ne peuvent être importés que liés : utilisez Fichier › Importer avec l’option Lien activée +@msg DXF options: {e} Options DXF : {e} +@msg DXF scale must be a positive number up to {_1}, not {scale} L’échelle DXF doit être un nombre positif d’au plus {_1}, pas {scale} +@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}` : {allowed} +@msg Date.prototype method called on a value that is not a Date object Méthode de Date.prototype appelée sur une valeur qui n’est pas un objet Date +@msg Dimension is locked or hidden La cote est verrouillée ou masquée +@msg Dimension mode does not match its source Le mode de la cote ne correspond pas à sa source +@msg Draw Inside needs exactly one selected path, compound path or text Dessin intérieur nécessite exactement un tracé, un tracé transparent ou un texte sélectionné +@msg EPS Save writes all artwork: use document.export for selected-only EPS L’enregistrement EPS écrit toute l’illustration : utilisez document.export pour un EPS de la sélection seule +@msg EPS flattener: {e} Aplatissement EPS : {e} +@msg EPS options: {e} Options EPS : {e} +@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}` : {allowed} +@msg Error.prototype.toString was called on a value that is not an object Error.prototype.toString a été appelé sur une valeur qui n’est pas un objet +@msg Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft Exporter pour les écrans écrit png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai ou vectorcraft +@msg Flare click coordinates must be finite numbers Les coordonnées du clic de l’halo doivent être des nombres finis +@msg Function.prototype.toString was called on a value that is not a function Function.prototype.toString a été appelé sur une valeur qui n’est pas une fonction +@msg HTML Text applies to object slices of type only Texte HTML s’applique uniquement aux tranches d’objet de texte +@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) Identifier les objets par est réglé sur ID XML : le nom doit être un ID XML valide (commençant par une lettre, un trait de soulignement ou deux-points ; sans espaces) +@msg Image Trace failed: internal error: its thread stopped (please report this bug) Échec de la vectorisation de l’image : erreur interne : son thread s’est arrêté (merci de signaler ce bogue) +@msg Image Trace failed: {e} Échec de la vectorisation de l’image : {e} +@msg Image Trace finished, but its document was closed La vectorisation de l’image est terminée, mais son document a été fermé +@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again La vectorisation de l’image est terminée, mais l’image ou ses réglages ont changé entre-temps : vectorisez-la à nouveau +@msg Import SVG Filter reads SVG files, not SVGZ Importer un filtre SVG lit les fichiers SVG, pas SVGZ +@msg Invalid dimension offset Décalage de cote non valide +@msg Invalid point Point non valide +@msg JSON.parse: {why} (at offset {at}) JSON.parse : {why} (à la position {at}) +@msg JSON.stringify cannot write a structure that contains itself JSON.stringify ne peut pas écrire une structure qui se contient elle-même +@msg Join: a subpath went missing Joindre : un sous-tracé a disparu +@msg Lock Workspace is not available in VectorCraft Verrouiller l’espace de travail n’est pas disponible dans VectorCraft +@msg Move Panel to New Window is not available: floating panels stay in the main window Déplacer le panneau dans une nouvelle fenêtre n’est pas disponible : les panneaux flottants restent dans la fenêtre principale +@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString nécessite un Namespace +@msg New Document profiles need the preferences folder, which this run doesn't have Les profils de nouveau document nécessitent le dossier des préférences, dont cette exécution ne dispose pas +@msg Number.prototype.{name} was called on a value that is not a number Number.prototype.{name} a été appelé sur une valeur qui n’est pas un nombre +@msg Object.create's prototype must be an object or null Le prototype d’Object.create doit être un objet ou null +@msg Object.{function} was given a value that is not an object Object.{function} a reçu une valeur qui n’est pas un objet +@msg Offset Path: the path is too complex to offset Décalage : le tracé est trop complexe pour être décalé +@msg Offset Path: {e} Décalage : {e} +@msg PDF content is incomplete or exceeds the reader's limits; no document was imported Le contenu PDF est incomplet ou dépasse les limites du lecteur ; aucun document n’a été importé +@msg PDF flattener: {e} Aplatissement PDF : {e} +@msg PDF options: {e} Options PDF : {e} +@msg QName.prototype.toString needs a QName QName.prototype.toString nécessite un QName +@msg Recolor Artwork is no longer open Redéfinir les couleurs de l’illustration n’est plus ouvert +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp.prototype.{name} a été appelé sur une valeur qui n’est pas un objet RegExp +@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 Le SVG n’est pas en UTF-8, UTF-16 ou ISO 8859-1 +@msg Save writes one artboard: name one, or export several with document.export L’enregistrement écrit un seul plan de travail : indiquez-en un, ou exportez-en plusieurs avec document.export +@msg Select a dimension first Sélectionnez d’abord une cote +@msg Select dimensions to update Sélectionnez les cotes à mettre à jour +@msg Select only dimensions Sélectionnez uniquement des cotes +@msg Select two or more objects, or a group Sélectionnez au moins deux objets, ou un groupe +@msg Shaper needs 1–64 source paths Shaper nécessite de 1 à 64 tracés source +@msg Stencil Bridges: the selected art is too large or too detailed to bridge Ponts de pochoir : l’illustration sélectionnée est trop grande ou trop détaillée pour être reliée +@msg String.prototype.toString was called on a value that is not a string String.prototype.toString a été appelé sur une valeur qui n’est pas une chaîne +@msg Switch x/y is for scatter graphs Permuter x/y s’applique aux graphes de dispersion +@msg Text options: {e} Options du texte : {e} +@msg The {name} profile could not be read, so the new document has the default settings: {reason} Le profil {name} n’a pas pu être lu, le nouveau document a donc les réglages par défaut : {reason} +@msg Trap: more than {_1} pairs of objects lie close enough to trap, or too many objects share a horizontal range; trap fewer objects at a time Recouvrement : plus de {_1} paires d’objets sont assez proches pour un recouvrement, ou trop d’objets partagent une même plage horizontale ; traitez moins d’objets à la fois +@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off Cadre et lignes d’échenillage : aucune ligne d’échenillage ne tient hors de l’illustration, et le cadre d’échenillage est désactivé +@msg \ at end of pattern \ à la fin du motif +@msg `alignOn` cannot be empty `alignOn` ne peut pas être vide +@msg `alignOn` must be a string `alignOn` doit être une chaîne +@msg `align` is start, center or end `align` vaut start, center ou end +@msg `align` must be a string `align` doit être une chaîne +@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` doit être une liste de paires [sous-tracé, point d’ancrage] +@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` doit lister des indices de points d’ancrage (0, 1, 2…), pas {k} +@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` doit lister des indices de points d’ancrage (0, 1, 2…), pas {v} +@msg `answer` for this {kind} dialog must be one of: {_1} `answer` pour cette boîte de dialogue {kind} doit valoir l’une de ces valeurs : {_1} +@msg `answers` has more than {MAX_ANSWERS} answers `answers` contient plus de {MAX_ANSWERS} réponses +@msg `answers` is larger than 1 MiB `answers` dépasse 1 Mio +@msg `answers` must be a list of answers `answers` doit être une liste de réponses +@msg `artboard` must be the index of an artboard of the document `artboard` doit être l’indice d’un plan de travail du document +@msg `artboardsAt` must be [x, y] `artboardsAt` doit être [x, y] +@msg `artboards` must be a list of artboard indices `artboards` doit être une liste d’indices de plans de travail +@msg `artboards` names an artboard the document does not have `artboards` désigne un plan de travail que le document ne contient pas +@msg `artboards` names an artboard twice `artboards` désigne deux fois le même plan de travail +@msg `asset` is the asset id (see assets.list) `asset` est l’identifiant de la ressource (voir assets.list) +@msg `at` must be [x, y] (got {at}) `at` doit être [x, y] (reçu : {at}) +@msg `at` must be a byte offset `at` doit être une position en octets +@msg `autoLeading` must be a number `autoLeading` doit être un nombre +@msg `background` must be "", "matte" or #rrggbb `background` doit valoir "", "matte" ou #rrggbb +@msg `beside` needs `side` `beside` nécessite `side` +@msg `burasagari` must be "none", "standard" or "forced" `burasagari` doit valoir "none", "standard" ou "forced" +@msg `center` must be [x, y] `center` doit être [x, y] +@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign` doit valoir "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" ou "icfBottom" +@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection` doit valoir "default", "leftToRight" ou "rightToLeft" +@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke` nécessite `stroke: false`, `ids: []` et aucun `stop` +@msg `clear` must be a list of type object ids, not {v} `clear` doit être une liste d’identifiants d’objets texte, pas {v} +@msg `clear` must list type objects; {_1} is not one `clear` doit lister des objets texte ; {_1} n’en est pas un +@msg `clear` must not list `id` `clear` ne doit pas lister `id` +@msg `code` must be a whole number `code` doit être un nombre entier +@msg `colorMode` must be one of rgb, cmyk `colorMode` doit valoir rgb ou cmyk +@msg `color` needs a `rule` to make the theme from `color` nécessite une règle `rule` à partir de laquelle créer le thème +@msg `colors` does not apply to the {name} reduction `colors` ne s’applique pas à la réduction {name} +@msg `colors` is empty `colors` est vide +@msg `colors` must be a whole number from 2 to 256 `colors` doit être un nombre entier compris entre 2 et 256 +@msg `colors` must be at least 9 for the uniform reduction `colors` doit valoir au moins 9 pour la réduction uniforme +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors` doit être compris entre 1 et {MAX_THEME_COLORS} (reçu : {n}) +@msg `columnType` is vertical, uniform, repeating or sliding `columnType` vaut vertical, uniform, repeating ou sliding +@msg `columns` must be a whole number 1..50 `columns` doit être un nombre entier de 1 à 50 +@msg `commands` must not be empty `commands` ne doit pas être vide +@msg `contents` goes with `to` `contents` s’utilise avec `to` +@msg `corners` must be sharp, round or square, not `{other}` `corners` doit valoir sharp, round ou square, pas `{other}` +@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners` doit lister des indices de points d’ancrage (0, 1, 2…), pas {v} +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count` doit être un nombre entier compris entre 0 et {MAX_COUNT} +@msg `count` must be a whole number from 1 to 100 `count` doit être un nombre entier compris entre 1 et 100 +@msg `dataBase64` is larger than the module limit `dataBase64` dépasse la limite des modules +@msg `dataBase64` is not valid base64 `dataBase64` n’est pas du base64 valide +@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly` ne peut pas être combiné avec `ids`, `reverse` ou `append` +@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth` et `childLimit` découpent le résumé : passez `summary: true` +@msg `design` is a design name, or null for plain columns `design` est un nom de motif, ou null pour des colonnes simples +@msg `design` is a design name, or null for the default marker `design` est un nom de motif, ou null pour le repère par défaut +@msg `destinationFolder` is required when destination is `folder` `destinationFolder` est obligatoire quand la destination est `folder` +@msg `destination` must be `none`, `saveAndClose` or `folder` `destination` doit valoir `none`, `saveAndClose` ou `folder` +@msg `diameter` must be from 0.1 to 2500 pt `diameter` doit être compris entre 0.1 et 2500 pt +@msg `digits` must be one of {_1} `digits` doit valoir l’une de ces valeurs : {_1} +@msg `dimImages` is a percentage or false `dimImages` est un pourcentage ou false +@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction` doit valoir "auto", "leftToRight" ou "rightToLeft" +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount` doit être un nombre compris entre 0 et 100 +@msg `each` is the value one design stands for, more than 0 `each` est la valeur que représente un motif, supérieure à 0 +@msg `end` must be "start" or "end" `end` doit valoir "start" ou "end" +@msg `ends` applies to joining exactly two paths `ends` s’applique à la jonction d’exactement deux tracés +@msg `event` must be one of {_1} `event` doit valoir l’une de ces valeurs : {_1} +@msg `every` must be "all", "odd" or "even", not {v} `every` doit valoir "all", "odd" ou "even", pas {v} +@msg `expand` must be true or false `expand` doit valoir true ou false +@msg `fidelity` must be from {lo} to {hi} pt `fidelity` doit être compris entre {lo} et {hi} pt +@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` doit contenir de 1 à 1000 chemins de fichiers de police trouvés par la dernière recherche text.findFontFiles +@msg `focal` and `radius` cannot be used together `focal` et `radius` ne peuvent pas être utilisés ensemble +@msg `focal` must be an array of [subpath, anchor] pairs `focal` doit être une liste de paires [sous-tracé, point d’ancrage] +@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` désigne [{si}, {ai}], qui n’est pas un point d’ancrage du tracé +@msg `folder` must be an absolute folder path `folder` doit être le chemin absolu d’un dossier +@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` doit être un objet de balises d’axe et de valeurs, comme {"wght"} +@msg `format` must be a string `format` doit être une chaîne +@msg `format` must be one of {_1} `format` doit valoir l’une de ces valeurs : {_1} +@msg `fractions` is chop or scale `fractions` vaut chop ou scale +@msg `from` and `to` must differ `from` et `to` doivent être différents +@msg `from` has an empty frame `from` a un cadre vide +@msg `from` must be a non-negative integer `from` doit être un entier positif ou nul +@msg `from` must be area type or type on a path `from` doit être un texte captif ou un texte curviligne +@msg `gid` must be a glyph id, not {v} `gid` doit être un identifiant de glyphe, pas {v} +@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate` concerne un seul caractère, pas un style de caractère (text.setGlyphAlternate le définit) +@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate` n’accepte que null, qui retire un choix de glyphe (text.setGlyphAlternate en crée un) +@msg `group` must be one of {_1} `group` doit valoir l’une de ces valeurs : {_1} +@msg `group` must be the Objects on Path group of `member` and `target` `group` doit être le groupe Objets sur tracé de `member` et `target` +@msg `guide` must be a guide index (guide.list) `guide` doit être un indice de repère (guide.list) +@msg `hAlign` must be default, left, center or right `hAlign` doit valoir default, left, center ou right +@msg `id` is an object id, not {v} `id` est un identifiant d’objet, pas {v} +@msg `id` must be a node id `id` doit être un identifiant de nœud +@msg `id` must be an object id, not {v} `id` doit être un identifiant d’objet, pas {v} +@msg `ids` must name Objects on Path groups or objects in them `ids` doit désigner des groupes Objets sur tracé ou des objets qu’ils contiennent +@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids` accepte au plus {MAX_INSPECT_IDS} identifiants +@msg `imageMap` must be none, rectangle or polygon `imageMap` doit valoir none, rectangle ou polygon +@msg `includeArtboardFill` must be true or false `includeArtboardFill` doit valoir true ou false +@msg `includeSubdirs` must be true or false `includeSubdirs` doit valoir true ou false +@msg `index` must be a point index `index` doit être un indice de point +@msg `index` must be a whole number or null `index` doit être un nombre entier ou null +@msg `indices` must be a non-empty array of item indices `indices` doit être une liste non vide d’indices d’éléments +@msg `item` needs `index` `item` nécessite `index` +@msg `items` is a list of {kind, item} `items` est une liste de {kind, item} +@msg `items` is empty `items` est vide +@msg `items` must be a list of {id, points} `items` doit être une liste de {id, points} +@msg `items` must be a list of {{id, anchors?}}, not {v} `items` doit être une liste de {{id, anchors?}}, pas {v} +@msg `items` must be an array of {id, anchors} `items` doit être une liste de {id, anchors} +@msg `items` must be an array of {{id, corners?}}, not {v} `items` doit être une liste de {{id, corners?}}, pas {v} +@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida` doit valoir "none", "short", "medium", "long" ou "stylistic" +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore` concerne le premier caractère d’une ligne, pas un style de caractère (text.setRangeStyle le définit) +@msg `kind` is one of {_1} `kind` vaut l’une de ces valeurs : {_1} +@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind` doit valoir "round", "invertedRound" ou "chamfer", pas {v} +@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind` doit valoir `text`, `visibility`, `image` ou `graph` +@msg `kind` must be a string `kind` doit être une chaîne +@msg `kind` must be image, noImage or htmlText `kind` doit valoir image, noImage ou htmlText +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType` doit valoir "pushInFirst", "pushOutFirst" ou "pushOutOnly" +@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku` doit valoir "none", "hard" ou "soft" +@msg `label` must be a non-empty name of at most 200 characters `label` doit être un nom non vide d’au plus 200 caractères +@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language` doit être un nom ou une balise de langue, comme "Turkish" ou "tr" +@msg `leader` must be a string `leader` doit être une chaîne +@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel` doit valoir "romanBaseline" ou "emBoxTop" +@msg `leading` must be from 0.1 to 5000, or null for Auto `leading` doit être compris entre 0.1 et 5000, ou null pour Auto +@msg `length` must be a number of at most 1000000 pt either way `length` doit être un nombre d’au plus 1000000 pt dans un sens ou dans l’autre +@msg `line` must be true or false `line` doit valoir true ou false +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max` doit être compris entre 1 et {MAX_SUGGESTIONS} +@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle` ({min_angle}°) ne peut pas dépasser `maxAngle` ({max_angle}°) +@msg `mode` must be "handles", "straight" or "smooth" `mode` doit valoir "handles", "straight" ou "smooth" +@msg `mode` must be "straight", "bezier" or "arc" `mode` doit valoir "straight", "bezier" ou "arc" +@msg `mode` must be one of {_1} `mode` doit valoir l’une de ces valeurs : {_1} +@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi` doit valoir "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" ou "lineEndHalf" +@msg `name` must be a name that is not empty `name` doit être un nom non vide +@msg `name` must not be empty `name` ne doit pas être vide +@msg `names` is a list of preset names `names` est une liste de noms de paramètres prédéfinis +@msg `names` lists preset names `names` liste des noms de paramètres prédéfinis +@msg `names` must be a list of names `names` doit être une liste de noms +@msg `names` must be a list of swatch names `names` doit être une liste de noms de nuances +@msg `names` must be an array of style names `names` doit être une liste de noms de styles +@msg `new` is a design name, or true `new` est un nom de motif, ou true +@msg `origin` must be [x, y] in points `origin` doit être [x, y] en points +@msg `path` must be a folder `path` doit être un dossier +@msg `path` must be a path object `path` doit être un objet tracé +@msg `picks` must be an array (got {v}) `picks` doit être une liste (reçu : {v}) +@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks` doit contenir de 1 à {MAX_PICKS} prélèvements (reçu : {_1}) +@msg `pivot` must be a whole number from 0 to 8 `pivot` doit être un nombre entier compris entre 0 et 8 +@msg `points` must be a list of [x, y] points `points` doit être une liste de points [x, y] +@msg `points` must be a list of point indexes `points` doit être une liste d’indices de points +@msg `points` must be an array of point indices `points` doit être une liste d’indices de points +@msg `points` must be point indexes `points` doit contenir des indices de points +@msg `port` must be "in" or "out" `port` doit valoir "in" ou "out" +@msg `position` is above or below `position` vaut above ou below +@msg `preset` must be a string `preset` doit être une chaîne +@msg `radius` must be 0 or more `radius` doit être supérieur ou égal à 0 +@msg `ratio` must be from 0.1 to 0.5 `ratio` doit être compris entre 0.1 et 0.5 +@msg `rect` must be [x, y, width, height] with a positive size `rect` doit être [x, y, width, height] avec une taille positive +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect` doit être [x0, y0, x1, y1] (reçu : {r}) +@msg `relation` must be overlap, inside or outside `relation` doit valoir overlap, inside ou outside +@msg `replace` must be a string `replace` doit être une chaîne +@msg `reversed` must be true or false `reversed` doit valoir true ou false +@msg `rotate` must be a finite number `rotate` doit être un nombre fini +@msg `rows` must be lists of numbers or nulls, not {r} `rows` doit être des listes de nombres ou de null, pas {r} +@msg `rule` must be nonZero or evenOdd `rule` doit valoir nonZero ou evenOdd +@msg `scale` must be a number from 0.001 to 1000 `scale` doit être un nombre compris entre 0.001 et 1000 +@msg `scale` must be a number in (0, 100] `scale` doit être un nombre dans (0, 100] +@msg `scope.{key}` must be a non-empty array `scope.{key}` doit être une liste non vide +@msg `scope` must be "document", {artboards} or {layers} `scope` doit valoir "document", {artboards} ou {layers} +@msg `scope` must be "selection" or "document", not {other} `scope` doit valoir "selection" ou "document", pas {other} +@msg `script` must be a string `script` doit être une chaîne +@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes` est une liste non vide d’indices de séries +@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes` nécessite un `type` ou un `valueAxis` +@msg `shift` must be a number of points `shift` doit être un nombre de points +@msg `showCenter` must be true or false `showCenter` doit valoir true ou false +@msg `side` must be "left", "right", "top" or "bottom" `side` doit valoir "left", "right", "top" ou "bottom" +@msg `side` places the artboard: leave out x, y, width and height `side` place le plan de travail : omettez x, y, width et height +@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification` doit valoir "fullJustify", "alignLeft", "alignCenter" ou "alignRight" +@msg `size` is a percentage of the base text's size, 1 to 100 `size` est un pourcentage de la taille du texte de base, de 1 à 100 +@msg `source` and `target` are the same object `source` et `target` sont le même objet +@msg `source` is the only path in series group `target` `source` est le seul tracé du groupe de séries `target` +@msg `source` must be `folder` or `datasets` `source` doit valoir `folder` ou `datasets` +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` produit plus de {MAX_COUNT} lignes dans le cadre +@msg `spacing` must be a number `spacing` doit être un nombre +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing` doit être un nombre de points supérieur ou égal à 0, ou "auto" +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing` doit être supérieur à 0 et d’au plus {MAX_DISTANCE} pt +@msg `start` must be a finite number `start` doit être un nombre fini +@msg `starts` must be an array of anchor indices (or nulls) `starts` doit être une liste d’indices de points d’ancrage (ou de null) +@msg `status` must be linked, embedded, missing or modified, or a list of them `status` doit valoir linked, embedded, missing ou modified, ou une liste de ces valeurs +@msg `stop` must be a stop index `stop` doit être un indice d’arrêt +@msg `story` styles the whole story: give no `start` or `end` `story` met en forme tout l’article : n’indiquez ni `start` ni `end` +@msg `story` takes the whole story: give no `start` or `end` `story` prend tout l’article : n’indiquez ni `start` ni `end` +@msg `strokeMode` applies to strokes (stroke: true) `strokeMode` s’applique aux contours (stroke: true) +@msg `style` must be roman or japanese `style` doit valoir roman ou japanese +@msg `subpaths` must be two integer indices `subpaths` doit être deux indices entiers +@msg `symbol` must be a symbol name `symbol` doit être un nom de symbole +@msg `target` must be "object" or "contents", not {v} `target` doit valoir "object" ou "contents", pas {v} +@msg `target` must be an object of the same Objects on Path group as `member` `target` doit être un objet du même groupe Objets sur tracé que `member` +@msg `tint` applies to global and spot colours `tint` s’applique aux couleurs globales et aux tons directs +@msg `tint` goes with a global or spot `swatch` `tint` s’utilise avec une nuance `swatch` globale ou en ton direct +@msg `tint` must be a number (%) `tint` doit être un nombre (%) +@msg `tint` needs a global or spot colour `tint` nécessite une couleur globale ou un ton direct +@msg `to` is already threaded `to` est déjà chaîné +@msg `to` is the design's new name `to` est le nouveau nom du motif +@msg `to` must be [x, y] `to` doit être [x, y] +@msg `to` must be a closed path, area type or type on a path other than `from` `to` doit être un tracé fermé, un texte captif ou un texte curviligne autre que `from` +@msg `to` must be a whole number `to` doit être un nombre entier +@msg `to` takes the rows of one object's stack, not aligned rows `to` prend les lignes de la pile d’un seul objet, pas des lignes alignées +@msg `useLab` must be true or false `useLab` doit valoir true ou false +@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign` doit valoir default, top, middle, baseline ou bottom +@msg `values` must be a list `values` doit être une liste +@msg `values` must be an object `values` doit être un objet +@msg `values` must name hex, rgb, cmyk, lab or gray `values` doit indiquer hex, rgb, cmyk, lab ou gray +@msg `values` names a kind twice `values` indique deux fois le même type +@msg `version` must be 0 or {_1} `version` doit valoir 0 ou {_1} +@msg `version` must be 0 or {newest} `version` doit valoir 0 ou {newest} +@msg `weight` must be from 0.01 to 1000 pt `weight` doit être compris entre 0.01 et 1000 pt +@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights` doit être une liste de {_1} nombres, un par ligne (reçu : {v}) +@msg `xml` must be a string `xml` doit être une chaîne +@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it l’entrée {n} de `{KEY}` donne au paragraphe à l’octet {start} de {_1} d’autres retraits qu’une entrée précédente +@msg `{KEY}` entry {n} must be an object l’entrée {n} de `{KEY}` doit être un objet +@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` l’entrée {n} de `{KEY}` ne définit ni `leftIndent` ni `firstLineIndent` +@msg `{KEY}` entry {n}: `id` must be an object id entrée {n} de `{KEY}` : `id` doit être un identifiant d’objet +@msg `{KEY}` entry {n}: `start` must be a byte offset entrée {n} de `{KEY}` : `start` doit être une position en octets +@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt entrée {n} de `{KEY}` : `{k}` doit être un nombre compris entre -{MAX_PARA} et {MAX_PARA} pt +@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} entrée {n} de `{KEY}` : aucun paragraphe de {_1} ne commence à l’octet {start} +@msg `{KEY}` entry {n}: unknown key `{k}` entrée {n} de `{KEY}` : clé inconnue `{k}` +@msg `{KEY}` entry {n}: {_1} is not a type object entrée {n} de `{KEY}` : {_1} n’est pas un objet texte +@msg `{KEY}` is empty `{KEY}` est vide +@msg `{KEY}` must be a list `{KEY}` doit être une liste +@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}` accepte au plus {MAX_PARAGRAPH_INDENTS} entrées (reçu : {_1}) +@msg `{KEY}` takes no `{other}` beside it `{KEY}` n’accepte pas `{other}` à côté de lui +@msg `{_1}` can't be edited or removed `{_1}` ne peut être ni modifié ni supprimé +@msg `{_1}` is built in and always listed `{_1}` est intégré et toujours listé +@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}` n’est pas un fichier de police trouvé par la dernière recherche text.findFontFiles +@msg `{_1}` would be overwritten by the package: choose a different folder or name `{_1}` serait écrasé par l’assemblage : choisissez un autre dossier ou un autre nom +@msg `{base}` is larger than {_1} MB `{base}` dépasse {_1} Mo +@msg `{base}` is not in the DCS file's folder `{base}` n’est pas dans le dossier du fichier DCS +@msg `{file}` has no art to place `{file}` ne contient aucune illustration à importer +@msg `{file}` is over {_1} MB `{file}` dépasse {_1} Mo +@msg `{f}` belongs to a VectorCraft that is running `{f}` appartient à une instance de VectorCraft en cours d’exécution +@msg `{f}` is the copy of a document open now `{f}` est la copie d’un document actuellement ouvert +@msg `{id}` is no writable format (see document.formats) `{id}` n’est pas un format inscriptible (voir document.formats) +@msg `{id}` isn't a loaded library `{id}` n’est pas une bibliothèque chargée +@msg `{key}` has no colours `{key}` ne contient aucune couleur +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` contient plus de {MAX_EDIT_WORDS} mots +@msg `{key}` is a design name `{key}` est un nom de motif +@msg `{key}` is a design name or a list of them `{key}` est un nom de motif ou une liste de noms de motifs +@msg `{key}` is longer than {MAX_NAME} characters `{key}` dépasse {MAX_NAME} caractères +@msg `{key}` is out of range `{key}` est hors limites +@msg `{key}` must be 0 or more `{key}` doit être supérieur ou égal à 0 +@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` doit être [min, desired, max] ou {{min?, desired?, max?}}, en pourcentage +@msg `{key}` must be [x, y] numbers `{key}` doit être des nombres [x, y] +@msg `{key}` must be a list of words `{key}` doit être une liste de mots +@msg `{key}` must be a list, not {v} `{key}` doit être une liste, pas {v} +@msg `{key}` must be a non-negative integer `{key}` doit être un entier positif ou nul +@msg `{key}` must be a number or null, not {v} `{key}` doit être un nombre ou null, pas {v} +@msg `{key}` must be a number {_1}..{_2} `{key}` doit être un nombre de {_1} à {_2} +@msg `{key}` must be a whole number of 0 or more `{key}` doit être un nombre entier supérieur ou égal à 0 +@msg `{key}` must be a whole number of at least 0, not {n} `{key}` doit être un nombre entier d’au moins 0, pas {n} +@msg `{key}` must be an appearance item index (paint order) or null `{key}` doit être un indice d’élément d’aspect (ordre de peinture) ou null +@msg `{key}` must be an array of asset ids (see assets.list) `{key}` doit être une liste d’identifiants de ressources (voir assets.list) +@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}` doit être compris entre 0 et 180 degrés, pas {a} +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}` doit être compris entre 0 et {MAX_DISTANCE} pt +@msg `{key}` must be from {_1} to {shift_max} `{key}` doit être compris entre {_1} et {shift_max} +@msg `{key}` must be from {lo} to {hi}, not {v} `{key}` doit être compris entre {lo} et {hi}, pas {v} +@msg `{key}` must be one of auto, always, never `{key}` doit valoir auto, always ou never +@msg `{key}` must be one of miter, round, bevel `{key}` doit valoir miter, round ou bevel +@msg `{key}` must be one word of at most {_1} characters `{key}` doit être un seul mot d’au plus {_1} caractères +@msg `{key}` must hold single words of at most {_1} characters `{key}` doit contenir des mots isolés d’au plus {_1} caractères +@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}` doit désigner un objet attaché au tracé d’un groupe Objets sur tracé +@msg `{key}` names an object that is hidden or locked `{key}` désigne un objet masqué ou verrouillé +@msg `{key}`: not a colour: {v} `{key}` : ce n’est pas une couleur : {v} +@msg `{k}.family` is required `{k}.family` est obligatoire +@msg `{k}` is a number 0..1 `{k}` est un nombre de 0 à 1 +@msg `{k}` is true or false `{k}` vaut true ou false +@msg `{k}` isn't an export setting: {_1} `{k}` n’est pas un réglage d’export : {_1} +@msg `{k}` must be a byte offset `{k}` doit être une position en octets +@msg `{k}` must be a number `{k}` doit être un nombre +@msg `{k}` must be a number of points `{k}` doit être un nombre de points +@msg `{k}` must be a string `{k}` doit être une chaîne +@msg `{k}` must be a whole number `{k}` doit être un nombre entier +@msg `{k}` must be from {lo} to {hi} for {key} `{k}` doit être compris entre {lo} et {hi} pour {key} +@msg `{k}` must be {what}, not {v} `{k}` doit être {what}, pas {v} +@msg `{l}` isn't a global or spot colour `{l}` n’est ni une couleur globale ni un ton direct +@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}` n’est pas une image bitmap : utilisez clipboard.importSvg, importPdf ou importText +@msg `{name}` can't be deleted `{name}` ne peut pas être supprimé +@msg `{name}` can't be duplicated `{name}` ne peut pas être dupliqué +@msg `{name}` can't be edited `{name}` ne peut pas être modifié +@msg `{name}` can't be edited this way `{name}` ne peut pas être modifié de cette manière +@msg `{name}` can't be renamed `{name}` ne peut pas être renommé +@msg `{name}` can't name a folder `{name}` ne peut pas nommer un dossier +@msg `{name}` has no art to place `{name}` ne contient aucune illustration à importer +@msg `{name}` has no text to place `{name}` ne contient aucun texte à importer +@msg `{name}` in the Word document is not UTF-8 or UTF-16 text `{name}` dans le document Word n’est pas du texte UTF-8 ou UTF-16 +@msg `{name}` in the Word document is not well-formed XML: {e} `{name}` dans le document Word n’est pas du XML bien formé : {e} +@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep `{name}` dans le document Word imbrique des éléments sur plus de {MAX_NESTING} niveaux +@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}` est un paramètre prédéfini intégré en lecture seule : enregistrez-le sous un autre nom +@msg `{name}` is a built-in preset: save under another name `{name}` est un paramètre prédéfini intégré : enregistrez sous un autre nom +@msg `{name}` is a built-in profile and stays `{name}` est un profil intégré et ne peut pas être retiré +@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` est un fichier distinct, qui ne peut être trouvé qu’à côté du chemin du fichier DCS +@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}` est une teinte de `{base}` : modifiez `{base}` (une nuance de teinte n’accepte qu’un nouveau nom) +@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}` est un espace de travail colorimétrique : choisissez-en d’abord un autre avec edit.colorSettings +@msg `{name}` is built in and stays `{name}` est intégré et ne peut pas être retiré +@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}` accepte une peinture de son propre type (couleur, dégradé ou motif) +@msg `{name}`: {_1}: {e} `{name}` : {_1} : {e} +@msg `{n}` can't be deleted `{n}` ne peut pas être supprimé +@msg `{n}` can't be merged `{n}` ne peut pas être fusionné +@msg `{n}` can't be moved `{n}` ne peut pas être déplacé +@msg `{n}` is not a colour swatch `{n}` n’est pas une nuance de couleur +@msg `{n}` isn't a solid colour `{n}` n’est pas une couleur unie +@msg `{n}`: colour groups hold solid colours only `{n}` : les groupes de couleurs ne contiennent que des couleurs unies +@msg `{other}` doesn't trace an image `{other}` ne vectorise pas une image +@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}` : les références de l’étape copient plus de {_1} Mo de valeurs +@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}` n’est pas une balise d’axe : une balise compte quatre caractères, comme "wght" +@msg `{to}` is empty or already used `{to}` est vide ou déjà utilisé +@msg `{u}` is no Unicode character `{u}` n’est pas un caractère Unicode +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` dépasse {MAX_TEXT} octets +@msg `{what}` must be text `{what}` doit être du texte +@msg `{word}` isn't in the custom dictionary `{word}` n’est pas dans le dictionnaire personnalisé +@msg a CMap has too many mappings une CMap contient trop de correspondances +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard un dessin DXF a une seule origine : passez useArtboards: true pour un dessin par plan de travail +@msg a PDF presets file with over {_1} presets un fichier de paramètres prédéfinis PDF de plus de {_1} paramètres +@msg a \u escape needs four hexadecimal digits un échappement \u nécessite quatre chiffres hexadécimaux +@msg a batch preview cannot switch documents un aperçu de traitement par lots ne peut pas changer de document +@msg a brush named `{nn}` exists une forme nommée `{nn}` existe déjà +@msg a comma in an XPath expression must separate function arguments une virgule dans une expression XPath doit séparer des arguments de fonction +@msg a container of the listed objects is gone un conteneur des objets listés a disparu +@msg a content stream can't be read un flux de contenu ne peut pas être lu +@msg a content stream isn't a stream un flux de contenu n’est pas un flux +@msg a design holds at most {_1} objects un motif contient au plus {_1} objets +@msg a design needs a name un motif nécessite un nom +@msg a dictionary grew too large un dictionnaire est devenu trop grand +@msg a document holds at most {_1} SVG filters un document contient au plus {_1} filtres SVG +@msg a document keeps at most {_1} graph designs un document conserve au plus {_1} motifs de graphe +@msg a document keeps at most {_1} saved selections un document conserve au plus {_1} sélections enregistrées +@msg a document keeps at most {_1} views un document conserve au plus {_1} vues +@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files un document lie au plus {MAX_SVG_SCRIPTS} fichiers JavaScript +@msg a document with unsaved changes has no recovery copy un document comportant des modifications non enregistrées n’a pas de copie de récupération +@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes l’ordre d’un filtre feConvolveMatrix est {x}×{y}, ce qui dépasse la capacité de la multiplication de taille de noyau effectuée par usvg +@msg a font's data is too large les données d’une police sont trop volumineuses +@msg a font's subroutines are too large les sous-programmes d’une police sont trop volumineux +@msg a function declaration without a name une déclaration de fonction sans nom +@msg a gradient preset is already called {n} un paramètre prédéfini de dégradé s’appelle déjà {n} +@msg a graph has at most {_1} series, so it transposes with at most that many categories un graphe a au plus {_1} séries, il se transpose donc avec au plus autant de catégories +@msg a host value has too many entries une valeur de l’hôte a trop d’entrées +@msg a host value is nested too deep une valeur de l’hôte est imbriquée trop profondément +@msg a layer can't be merged into its own sublayer un calque ne peut pas être fusionné dans son propre sous-calque +@msg a layer's form box isn't an array le cadre du formulaire d’un calque n’est pas un tableau +@msg a layer's form can't be read le formulaire d’un calque ne peut pas être lu +@msg a layer's form has no box le formulaire d’un calque n’a pas de cadre +@msg a layer's form has no dictionary le formulaire d’un calque n’a pas de dictionnaire +@msg a legend draws at most {MAX_CHIPS} swatches une légende dessine au plus {MAX_CHIPS} nuances +@msg a name names one asset: give multiple: false, or one object un nom désigne une seule ressource : indiquez multiple: false, ou un seul objet +@msg a namespace declaration uses a reserved prefix or URI une déclaration d’espace de noms utilise un préfixe ou un URI réservé +@msg a namespace qualifier is undefined un qualificateur d’espace de noms n’est pas défini +@msg a namespace with an empty URI cannot have a prefix un espace de noms dont l’URI est vide ne peut pas avoir de préfixe +@msg a number has no digits un nombre n’a aucun chiffre +@msg a number's exponent has no digits l’exposant d’un nombre n’a aucun chiffre +@msg a number's fraction has no digits la partie décimale d’un nombre n’a aucun chiffre +@msg a page can't be read une page ne peut pas être lue +@msg a page object is used by something else un objet de page est utilisé par autre chose +@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) une vignette de page doit être en pixels RVB, de 1 à {THUMBNAIL_SIZE} de côté (reçu : {w} × {h}) +@msg a pattern layer doesn't end what it starts un calque de motif ne termine pas ce qu’il commence +@msg a pattern layer's colors hold something else les couleurs d’un calque de motif contiennent autre chose +@msg a pattern named `{n}` exists un motif nommé `{n}` existe déjà +@msg a plug-in with id {_1} is already installed un plug-in d’identifiant {_1} est déjà installé +@msg a preset named `{n}` exists un paramètre prédéfini nommé `{n}` existe déjà +@msg a print presets file with over {_1} presets un fichier de paramètres prédéfinis d’impression de plus de {_1} paramètres +@msg a profile is already called {n} un profil s’appelle déjà {n} +@msg a property descriptor cannot have both a value or writable and a get or set un descripteur de propriété ne peut pas avoir à la fois value ou writable et get ou set +@msg a property descriptor must be an object un descripteur de propriété doit être un objet +@msg a property's {name} must be a function or undefined le {name} d’une propriété doit être une fonction ou undefined +@msg a row can't go into itself une ligne ne peut pas être placée dans elle-même +@msg a series goes on the left or the right value axis une série se place sur l’axe des valeurs gauche ou droit +@msg a start must be an anchor index ≥ 0 or null un début doit être un indice de point d’ancrage ≥ 0 ou null +@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied un de ses flux se décoderait en plus de 256 Mo, ou ses paramètres de décodage ne peuvent pas être appliqués +@msg a string holds a control character une chaîne contient un caractère de contrôle +@msg a string holds an escape sequence JSON does not have une chaîne contient une séquence d’échappement qui n’existe pas en JSON +@msg a string is not closed une chaîne n’est pas fermée +@msg a style named `{name}` already exists un style nommé `{name}` existe déjà +@msg a style named `{to}` already exists un style nommé `{to}` existe déjà +@msg a swatch needs a colour, gradient or pattern une nuance nécessite une couleur, un dégradé ou un motif +@msg a symbol named `{n}` already exists un symbole nommé `{n}` existe déjà +@msg a theme has 1 to {THEME_SIZE} colours un thème compte de 1 à {THEME_SIZE} couleurs +@msg a theme is already called {name} (replace: "{name}" overwrites it) un thème s’appelle déjà {name} (replace: "{name}" l’écrase) +@msg a toolbar name has at most {MAX_NAME} characters un nom de barre d’outils compte au plus {MAX_NAME} caractères +@msg a view named `{_1}` exists une vue nommée `{_1}` existe déjà +@msg a view named `{name}` exists une vue nommée `{name}` existe déjà +@msg a workspace keeps at most {MAX} toolbars un espace de travail conserve au plus {MAX} barres d’outils +@msg a {_1} series can't go in a {_2} graph une série {_1} ne peut pas aller dans un graphe {_2} +@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v} : "pdf" (compatible PDF, par défaut) ou "7" (.ai ancien, version 7) +@msg already editing an opacity mask un masque d’opacité est déjà en cours de modification +@msg already editing another object's opacity mask le masque d’opacité d’un autre objet est déjà en cours de modification +@msg already in pattern editing mode déjà en mode d’édition de motif +@msg an SVG filter is named by its id: give `xml` with `name` un filtre SVG est nommé par son identifiant : indiquez `xml` avec `name` +@msg an XML element has two attributes with the same name un élément XML a deux attributs portant le même nom +@msg an XML value cannot be inserted into itself or its descendants une valeur XML ne peut pas être insérée dans elle-même ou dans ses descendants +@msg an XML value's children cannot be assigned by position les enfants d’une valeur XML ne peuvent pas être affectés par position +@msg an XML value's children cannot be deleted by position les enfants d’une valeur XML ne peuvent pas être supprimés par position +@msg an XMLList converts to XML only when it has one item un XMLList ne se convertit en XML que s’il contient un seul élément +@msg an XPath node test is missing un test de nœud XPath est manquant +@msg an XPath operand is missing un opérande XPath est manquant +@msg an XPath operator is missing un opérateur XPath est manquant +@msg an XPath predicate or step cannot follow here un prédicat ou une étape XPath ne peut pas suivre ici +@msg an XPath predicate or step follows a value that is not a node-set un prédicat ou une étape XPath suit une valeur qui n’est pas un ensemble de nœuds +@msg an XPath step is missing une étape XPath est manquante +@msg an artboard is listed twice un plan de travail est listé deux fois +@msg an empty artboard has no width or height to scale to un plan de travail vide n’a ni largeur ni hauteur à laquelle mettre à l’échelle +@msg an image couldn't be decoded and is missing from the picture une image n’a pas pu être décodée et manque dans l’illustration +@msg an image is too large for DXF and was left out une image est trop grande pour le DXF et a été omise +@msg an image of {_1} MB, at most {_2} MB une image de {_1} Mo, au plus {_2} Mo +@msg an image of {declared_w} × {declared_h} pixels is too large to draw une image de {declared_w} × {declared_h} pixels est trop grande pour être dessinée +@msg an image of {width} × {height} pixels is too large to draw une image de {width} × {height} pixels est trop grande pour être dessinée +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) une image de {w} × {h} pixels est trop grande pour être convertie (au plus {_1} pixels) +@msg an index must be a whole number un indice doit être un nombre entier +@msg an inline graphic has no glyphs un graphique incorporé n’a aucun glyphe +@msg an invisible text form can't be read un formulaire de texte invisible ne peut pas être lu +@msg an object is missing un objet est manquant +@msg an object's header is missing l’en-tête d’un objet est manquant +@msg an overprinting form can't be read un formulaire en surimpression ne peut pas être lu +@msg an unread object has no readable bounds un objet non lu n’a pas de limites lisibles +@msg an unread object holds a frame of threaded type un objet non lu contient un cadre de texte chaîné +@msg an unread object is missing from its layer un objet non lu manque dans son calque +@msg anchor `smooth` must be true or false `smooth` d’un point d’ancrage doit valoir true ou false +@msg anchor `{key}` must be [x, y] `{key}` d’un point d’ancrage doit être [x, y] +@msg anchor `{key}` must be a number `{key}` d’un point d’ancrage doit être un nombre +@msg angle and roundness must be finite l’angle et l’arrondi doivent être finis +@msg angles must be a list as long as pins (degrees or null) angles doit être une liste aussi longue que pins (degrés ou null) +@msg appearance must be flat|center|edge appearance doit valoir flat|center|edge +@msg apply is "fill" or "stroke", not `{other}` apply vaut "fill" ou "stroke", pas `{other}` +@msg apply was called on a value that is not a function apply a été appelé sur une valeur qui n’est pas une fonction +@msg area must be a closed polygon of 3 to 100000 [x, y] points area doit être un polygone fermé de 3 à 100000 points [x, y] +@msg area type in a thread cannot use Dynamic Text: {frames} un texte captif chaîné ne peut pas utiliser le texte dynamique : {frames} +@msg area type needs a path that encloses an area le texte captif nécessite un tracé qui délimite une zone +@msg area type {_1} is named twice le texte captif {_1} est désigné deux fois +@msg array literal too long littéral de tableau trop long +@msg arrowAlign must be extend|tip, got {o} arrowAlign doit valoir extend|tip, reçu : {o} +@msg arrowScale must be [start, end] arrowScale doit être [start, end] +@msg artboard is a 0-based index artboard est un indice commençant à 0 +@msg artboard must be a 0-based index artboard doit être un indice commençant à 0 +@msg artboard {i} doesn't print le plan de travail {i} ne s’imprime pas +@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout doit être {layout?, columns?, spacing?, rightToLeft?} +@msg artboardLayout.columns must be 1 or more artboardLayout.columns doit être supérieur ou égal à 1 +@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout doit valoir gridByRow, gridByColumn, row ou column +@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft doit valoir true ou false +@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing doit être compris entre 0 et {MAX_SPACING} pt +@msg artboards must be a non-empty list artboards doit être une liste non vide +@msg artboards must be a number artboards doit être un nombre +@msg asset `{_1}` has no art to export la ressource `{_1}` ne contient aucune illustration à exporter +@msg assignment to a constant binding affectation à une liaison constante +@msg at least one row that isn't excluded needs a weight above 0 au moins une ligne non exclue doit avoir un poids supérieur à 0 +@msg at most {MAX_KEYWORDS} keywords au plus {MAX_KEYWORDS} mots-clés +@msg at most {MAX_MATCHES} matches are kept au plus {MAX_MATCHES} correspondances sont conservées +@msg at most {MAX_POINTS} points au plus {MAX_POINTS} points +@msg at most {MAX_QUEUE} files au plus {MAX_QUEUE} fichiers +@msg at must be [x, y] at doit être [x, y] +@msg at/rect lie outside the canvas at/rect se trouvent en dehors de la zone de travail +@msg attribute must be size|leading|tracking|kerning|baselineShift attribute doit valoir size|leading|tracking|kerning|baselineShift +@msg attrs must be an object attrs doit être un objet +@msg automation path rejected: the folder's name isn't valid text chemin d’automatisation refusé : le nom du dossier n’est pas un texte valide +@msg automation path rejected: {_1} isn't valid text chemin d’automatisation refusé : {_1} n’est pas un texte valide +@msg automation path rejected: {why}: {path} chemin d’automatisation refusé : {why} : {path} +@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis doit valoir "vertical", "horizontal" ou un angle en degrés, pas `{given}` +@msg backgroundContents must be transparent or white backgroundContents doit valoir transparent ou white +@msg bad AES key clé AES incorrecte +@msg bad `runs`: {e} `runs` incorrect : {e} +@msg bad arrowScale arrowScale incorrect +@msg bad base64 base64 incorrect +@msg bad color {c} couleur incorrecte {c} +@msg bad colour couleur incorrecte +@msg bad colour in `colors` couleur incorrecte dans `colors` +@msg bad colour {v} couleur incorrecte {v} +@msg bad colour {v} in groupColors couleur incorrecte {v} dans groupColors +@msg bad cross-reference entry entrée de table de références croisées incorrecte +@msg bad cross-reference table table de références croisées incorrecte +@msg bad current colour {v} couleur actuelle incorrecte {v} +@msg bad dataBase64 dataBase64 incorrect +@msg bad edit modification incorrecte +@msg bad fill {v} fond incorrect {v} +@msg bad fit: {e} ajustement incorrect : {e} +@msg bad model {other} (keep, new or document) modèle incorrect {other} (keep, new ou document) +@msg bad new colour {v} nouvelle couleur incorrecte {v} +@msg bad object objet incorrect +@msg bad object number numéro d’objet incorrect +@msg bad offset position incorrecte +@msg bad path data: {e} données de tracé incorrectes : {e} +@msg bad point {v}: expected [x, y] or [x, y, pressure] point incorrect {v} : [x, y] ou [x, y, pressure] attendu +@msg bad reference référence incorrecte +@msg bad trailer size taille de trailer incorrecte +@msg bad {k} colour couleur {k} incorrecte +@msg batches cannot nest les traitements par lots ne peuvent pas être imbriqués +@msg bind something to a variable first liez d’abord quelque chose à une variable +@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed doit être compris entre 0 et {MAX_BLEED} pt : un nombre, [top, bottom, left, right] ou {{top, bottom, left, right}} +@msg blend must be a mode name blend doit être un nom de mode +@msg both sides of an XPath | must select nodes les deux côtés d’un | XPath doivent sélectionner des nœuds +@msg brush must be a name or null brush doit être un nom ou null +@msg by must be a number by doit être un nombre +@msg can't add page thumbnails: {why} impossible d’ajouter les vignettes de page : {why} +@msg can't add the editing data for other apps: {why} impossible d’ajouter les données d’édition destinées à d’autres applications : {why} +@msg can't edit the written PDF: {why} impossible de modifier le PDF écrit : {why} +@msg can't encrypt the PDF: {why} impossible de chiffrer le PDF : {why} +@msg can't linearise the PDF: {why} impossible de linéariser le PDF : {why} +@msg can't open `{_1}`: {_2} impossible d’ouvrir `{_1}` : {_2} +@msg can't open `{_1}`: {e} impossible d’ouvrir `{_1}` : {e} +@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) impossible d’ouvrir `{name}` : ce n’est pas un format que VectorCraft lit (voir document.formats) +@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) impossible d’importer `{name}` : ce n’est pas un format que VectorCraft lit (voir document.formats) +@msg can't read back the written PDF: {e} impossible de relire le PDF écrit : {e} +@msg can't read this Word document: {why} impossible de lire ce document Word : {why} +@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp impossible d’écrire des images .{e} : utilisez .png, .jpg, .gif, .webp, .tif ou .bmp +@msg can't write PDF layers, overprint or invisible text: {why} impossible d’écrire les calques PDF, la surimpression ou le texte invisible : {why} +@msg can't write the file's copyright: {why} impossible d’écrire le copyright du fichier : {why} +@msg can't write the file's modification date: {why} impossible d’écrire la date de modification du fichier : {why} +@msg can't write the pages' user units: {why} impossible d’écrire les unités utilisateur des pages : {why} +@msg canceled annulé +@msg cannot convert the object to a primitive value impossible de convertir l’objet en valeur primitive +@msg cannot convert undefined or null to an object impossible de convertir undefined ou null en objet +@msg cannot distort a zero-size or non-finite bounding box impossible de déformer un cadre de sélection de taille nulle ou non fini +@msg cannot move an object into itself impossible de déplacer un objet dans lui-même +@msg case must be upper|lower|title|sentence case doit valoir upper|lower|title|sentence +@msg category `{c}`: one of {_1} catégorie `{c}` : l’une de ces valeurs : {_1} +@msg center must be [x, y] center doit être [x, y] +@msg character dimensions cannot be represented with the source text transform les dimensions des caractères ne peuvent pas être représentées avec la transformation du texte source +@msg character dimensions cannot be represented with the target text transform les dimensions des caractères ne peuvent pas être représentées avec la transformation du texte cible +@msg choose at least one artboard choisissez au moins un plan de travail +@msg choose at least one status choisissez au moins un état +@msg clip must be true or false clip doit valoir true ou false +@msg clippingMask must be true or false clippingMask doit valoir true ou false +@msg closed must be a boolean closed doit être un booléen +@msg collapsed must be true or false collapsed doit valoir true ou false +@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile : aucun profil RVB ou CMJN ne s’appelle « {profile} » +@msg colorMode must be rgb or cmyk, not `{m}` colorMode doit valoir rgb ou cmyk, pas `{m}` +@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel doit valoir {own} (le mode du document), grayscale ou bitmap +@msg colour groups can't go into colour groups un groupe de couleurs ne peut pas aller dans un groupe de couleurs +@msg colour, mode and spot apply to solid-colour swatches only couleur, mode et ton direct s’appliquent uniquement aux nuances de couleur unie +@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read colonne {column} : `{cell}` est une colonne `%` des fichiers de données de graphe, qui ne sont pas lus +@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable colonne {column} : `{cell}` désigne `{name}`, qui est une variable {_1} +@msg column {column}: `{cell}` names no variable colonne {column} : `{cell}` ne désigne aucune variable +@msg column {column}: the variable name is longer than {MAX_NAME} characters colonne {column} : le nom de variable dépasse {MAX_NAME} caractères +@msg column: true floats the whole column and takes no onto, above or below column: true rend flottante toute la colonne et n’accepte ni onto, ni above, ni below +@msg command must be a shape.* command command doit être une commande shape.* +@msg commands[{i}]: missing `command` commands[{i}] : `command` manquant +@msg composer must be "singleLine" or "everyLine" composer doit valoir "singleLine" ou "everyLine" +@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes les formes composées acceptent les tracés, tracés transparents, groupes, formes composées, textes, dégradés de formes et enveloppes +@msg compound {_1} must contain exactly one open path to join le tracé transparent {_1} doit contenir exactement un tracé ouvert à joindre +@msg connect end must be "start" or "end" l’extrémité de connect doit valoir "start" ou "end" +@msg connect needs a path other than the one extended connect nécessite un tracé autre que celui qui est prolongé +@msg connect needs id connect nécessite id +@msg connect path has no open end le tracé de connect n’a pas d’extrémité ouverte +@msg continuation must be an array of path chunks continuation doit être une liste de fragments de tracé +@msg continuation requires points continuation nécessite des points +@msg copy needs path: the file to copy into the document's folder copy nécessite path : le fichier à copier dans le dossier du document +@msg copy takes images only: relink placed documents where their files are copy n’accepte que des images : reliez les documents importés là où se trouvent leurs fichiers +@msg copying a file isn't available on the web la copie d’un fichier n’est pas disponible sur le Web +@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus doit valoir unknown, copyrighted ou publicDomain +@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl doit être une URL (sans espaces, au plus 2048 caractères) +@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners doit être 4 points [x, y] (TL, TR, BR, BL) +@msg counterPer `{c}`: item or file counterPer `{c}` : item ou file +@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}` : seules les pages PDF ont des zones art, trim, bleed et media ; utilisez "crop" ou "bounding" +@msg crop rectangle doesn't overlap the image le rectangle de recadrage ne chevauche pas l’image +@msg cross-reference and object streams aren't supported les flux de références croisées et d’objets ne sont pas pris en charge +@msg damaged compressed data: {e} données compressées endommagées : {e} +@msg dash lengths must be numbers, got {v} les longueurs de tiret doivent être des nombres, reçu : {v} +@msg dash must be a list of lengths or null, got {v} dash doit être une liste de longueurs ou null, reçu : {v} +@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) clé de données `{k}` : lettres, chiffres, -, _ et . (ni name ni vc-…) +@msg data must be an object of key: value, not {v} data doit être un objet clé: valeur, pas {v} +@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare le jeu de données `{name}` contient une valeur pour `{var}`, que la bibliothèque ne déclare pas +@msg data set `{name}` holds two values for `{var}` le jeu de données `{name}` contient deux valeurs pour `{var}` +@msg data set `{name}` is declared twice le jeu de données `{name}` est déclaré deux fois +@msg data takes at most {MAX_DATA} keys at once data accepte au plus {MAX_DATA} clés à la fois +@msg dataset `{name}` already exists le jeu de données `{name}` existe déjà +@msg dataset `{to}` already exists le jeu de données `{to}` existe déjà +@msg date `{d}`: YYYY-MM-DD date `{d}` : AAAA-MM-JJ +@msg decoded data is too large les données décodées sont trop volumineuses +@msg define a variable first définissez d’abord une variable +@msg detect must be true or false detect doit valoir true ou false +@msg direct-select anchor points of a path sélectionnez des points d’ancrage d’un tracé avec l’outil Sélection directe +@msg distance must be > 0 distance doit être > 0 +@msg dx and dy give the arrow's direction dx et dy donnent la direction de la flèche +@msg dx and dy must be numbers dx et dy doivent être des nombres +@msg dxf fitTo must be [width, height], positive dxf fitTo doit être [width, height], positifs +@msg dxf must be an object of DXF import options dxf doit être un objet d’options d’import DXF +@msg dxf scale must be a positive number up to {_1}, not {s} l’échelle dxf doit être un nombre positif d’au plus {_1}, pas {s} +@msg dxf unit `{u}`: a unit such as mm, cm, in or pt unité dxf `{u}` : une unité comme mm, cm, in ou pt +@msg dxf: {e} dxf : {e} +@msg each format is an object {format, scale?, suffix?} chaque format est un objet {format, scale?, suffix?} +@msg each item needs an id chaque élément nécessite un identifiant +@msg each match needs `kind` chaque correspondance nécessite `kind` +@msg each match needs a `name` chaque correspondance nécessite un `name` +@msg each of `fonts` is {family, style?}; 1 to 1000 of them chaque élément de `fonts` est {family, style?} ; de 1 à 1000 éléments +@msg each of `items` is {kind, item} chaque élément de `items` est {kind, item} +@msg each of `items` is {{id, corners?}} with an object id, not {it} chaque élément de `items` est {{id, corners?}} avec un identifiant d’objet, pas {it} +@msg each path chunk must contain 1 to 4096 points chaque fragment de tracé doit contenir de 1 à 4096 points +@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) chaque prélèvement est {{at: [x, y]}} ou {{rect: [x0, y0, x1, y1]}} (reçu : {v}) +@msg each row needs `from` chaque ligne nécessite `from` +@msg each stop needs a position chaque taquet nécessite une position +@msg each subpath needs an anchors array chaque sous-tracé nécessite une liste anchors +@msg each weight must be a number of 0 or more (got {v}) chaque poids doit être un nombre supérieur ou égal à 0 (reçu : {v}) +@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType doit valoir easeIn, easeOut, easeInOut ou easeOutIn +@msg editingData must be true or false editingData doit valoir true ou false +@msg editingData must be true, false or "always" editingData doit valoir true, false ou "always" +@msg edits overlap des modifications se chevauchent +@msg effects of objects whose appearances differ can't be moved together les effets d’objets dont l’aspect diffère ne peuvent pas être déplacés ensemble +@msg empty header in column {_1} en-tête vide dans la colonne {_1} +@msg encoding `{_1}`: utf8 or utf16 encodage `{_1}` : utf8 ou utf16 +@msg ends must be two of "first" and "last" ends doit être deux valeurs parmi "first" et "last" +@msg every numbered name for the result is taken tous les noms numérotés pour le résultat sont pris +@msg expanded SVG styles exceed the import size limit les styles SVG développés dépassent la taille limite d’import +@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes le fichier de paramètres prédéfinis d’export dépasse {MAX_PRESET_FILE_BYTES} octets +@msg exportText must be editable or appearance exportText doit valoir editable ou appearance +@msg exportType must be movieClip or graphic, not `{s}` exportType doit valoir movieClip ou graphic, pas `{s}` +@msg extend needs id extend nécessite id +@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges liste au plus {MAX_FEATURE_CHANGES} balises +@msg featureChanges must be a list of tags featureChanges doit être une liste de balises +@msg features must be a list of tags features doit être une liste de balises +@msg fidelity must be 0..100 fidelity doit être compris entre 0 et 100 +@msg files must be {name, dataBase64} files doit être {name, dataBase64} +@msg finally table {t} out of range table finally {t} hors limites +@msg fit must be none|autoHeight|shrinkText|block, got {id} fit doit valoir none|autoHeight|shrinkText|block, reçu : {id} +@msg flags cannot be given with a RegExp object les indicateurs ne peuvent pas être fournis avec un objet RegExp +@msg flattener: {e} aplatissement : {e} +@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}` : high, medium, low ou un paramètre enregistré (voir flattener.presets.list) +@msg flipProfile must be along|across, got {o} flipProfile doit valoir along|across, reçu : {o} +@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}` : none, rows, format, item ou itemFormat +@msg folders deeper than {MAX_BATCH_DEPTH} levels dossiers de plus de {MAX_BATCH_DEPTH} niveaux de profondeur +@msg folders deeper than {MAX_DEPTH} levels dossiers de plus de {MAX_DEPTH} niveaux de profondeur +@msg font family `{family}` is not available la famille de polices `{family}` n’est pas disponible +@msg font family `{tf}` is not available la famille de polices `{tf}` n’est pas disponible +@msg format `{f}`: pdf or postscript format `{f}` : pdf ou postscript +@msg format `{f}`: text or json format `{f}` : text ou json +@msg format is `{format}` le format est `{format}` +@msg formats must be an array of rows, not {v} formats doit être une liste de lignes, pas {v} +@msg frequency must be between 1 and 300 lines per inch la linéature doit être comprise entre 1 et 300 lignes par pouce +@msg from and to must be finite from et to doivent être finis +@msg from must be [x0, y0, x1, y1] from doit être [x0, y0, x1, y1] +@msg function declaration index out of range indice de déclaration de fonction hors limites +@msg give `before` or `after`, not both indiquez `before` ou `after`, pas les deux +@msg give `char`, `text`, `code` or `unicode` indiquez `char`, `text`, `code` ou `unicode` +@msg give `colors`, or `color` and `rule` indiquez `colors`, ou `color` et `rule` +@msg give `count` or `spacing`, not both indiquez `count` ou `spacing`, pas les deux +@msg give `event`, such as onclick indiquez `event`, par exemple onclick +@msg give `file`, the JavaScript file's path or URL indiquez `file`, le chemin ou l’URL du fichier JavaScript +@msg give `fill` and/or `stroke` indiquez `fill` et/ou `stroke` +@msg give `fill`, `stroke` or both indiquez `fill`, `stroke` ou les deux +@msg give `folder` indiquez `folder` +@msg give `fonts`, or open a document indiquez `fonts`, ou ouvrez un document +@msg give `from` or `indices`, not both indiquez `from` ou `indices`, pas les deux +@msg give `id` (no text is being edited) indiquez `id` (aucun texte n’est en cours de modification) +@msg give `id` and `anchors`, or `items`, not both indiquez `id` et `anchors`, ou `items`, pas les deux +@msg give `id` or `guide`, not both indiquez `id` ou `guide`, pas les deux +@msg give `length` or `to` indiquez `length` ou `to` +@msg give `matches`: [{kind, name, target}] indiquez `matches` : [{kind, name, target}] +@msg give `name` (or edit a pattern) indiquez `name` (ou modifiez un motif) +@msg give `name` or `names` indiquez `name` ou `names` +@msg give `name`, an SVG filter's name indiquez `name`, le nom d’un filtre SVG +@msg give `path` (a .wasm file) or `dataBase64` (the module) indiquez `path` (un fichier .wasm) ou `dataBase64` (le module) +@msg give `path`, `data` or `dataBase64` indiquez `path`, `data` ou `dataBase64` +@msg give `path`, `data`, `dataBase64` or `csv` indiquez `path`, `data`, `dataBase64` ou `csv` +@msg give `path`, `xml` or `dataBase64` indiquez `path`, `xml` ou `dataBase64` +@msg give `path`, an SVG file, or `svg`, SVG text indiquez `path`, un fichier SVG, ou `svg`, du texte SVG +@msg give `script`, the JavaScript the event runs ("" removes the event) indiquez `script`, le code JavaScript exécuté par l’événement ("" supprime l’événement) +@msg give `text` (the reading) or `remove` indiquez `text` (la lecture) ou `remove` +@msg give `x` and `y` (and a `tolerance` of 0 or more) indiquez `x` et `y` (et une `tolerance` supérieure ou égale à 0) +@msg give `xml`, one SVG element indiquez `xml`, un seul élément SVG +@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments indiquez un mode ou des réglages r/g/b, c/m/y/k, gray ou tint +@msg give a name, or paint the active proxy with a saved preset indiquez un nom, ou peignez l’échantillon actif avec un paramètre prédéfini enregistré +@msg give a name, or select a stroke with a saved profile indiquez un nom, ou sélectionnez un contour doté d’un profil enregistré +@msg give a pattern name or select an object painted with a pattern indiquez un nom de motif ou sélectionnez un objet peint avec un motif +@msg give at least one of clip, invert, disabled, linked indiquez au moins l’un des paramètres clip, invert, disabled, linked +@msg give color, none, swatch or gradient indiquez color, none, swatch ou gradient +@msg give dataBase64 indiquez dataBase64 +@msg give either at (a click) or area (a path drawn around the overlaps) indiquez soit at (un clic), soit area (un tracé dessiné autour des chevauchements) +@msg give exactly one of `path`, `data` and `dataBase64` indiquez exactement un paramètre parmi `path`, `data` et `dataBase64` +@msg give formats or a preset, not both indiquez formats ou un paramètre prédéfini, pas les deux +@msg give index or indexes, not both indiquez index ou indexes, pas les deux +@msg give newName or delete indiquez newName ou delete +@msg give offset or by indiquez offset ou by +@msg give one element indiquez un seul élément +@msg give one of `to`, `rect` or `at` indiquez l’un des paramètres `to`, `rect` ou `at` +@msg give one of assets, layers, selection or fullDocument indiquez l’un des paramètres assets, layers, selection ou fullDocument +@msg give only one of onto, above, below, icon and a column place indiquez un seul paramètre parmi onto, above, below, icon et un emplacement de colonne +@msg give opacity or blend indiquez opacity ou blend +@msg give origin: [x, y], or reset: true indiquez origin: [x, y], ou reset: true +@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note indiquez overprintFill, overprintStroke, showCenter, imageMap, url ou note +@msg give path (a file) or folder indiquez path (un fichier) ou folder +@msg give path, or name and dataBase64 indiquez path, ou name et dataBase64 +@msg give paths or files indiquez paths ou files +@msg give rows, rowHeight, columns or columnWidth indiquez rows, rowHeight, columns ou columnWidth +@msg give svg, or dataBase64 of an SVG or SVGZ file indiquez svg, ou le dataBase64 d’un fichier SVG ou SVGZ +@msg give text indiquez text +@msg give the asset a name donnez un nom à la ressource +@msg give the preset a `name` donnez un `name` au paramètre prédéfini +@msg give the preset a name donnez un nom au paramètre prédéfini +@msg give the preset a name (1–64 characters) donnez un nom au paramètre prédéfini (de 1 à 64 caractères) +@msg give the settings to change, e.g. {"formats"}]} indiquez les réglages à modifier, par exemple {"formats"}]} +@msg give the style a name donnez un nom au style +@msg give two or more style `names` indiquez au moins deux noms de styles `names` +@msg give two or more swatches indiquez au moins deux nuances +@msg give x, y, or a handle's length or angle indiquez x, y, ou la longueur ou l’angle d’une poignée +@msg give {key, value} or {values} indiquez {key, value} ou {values} +@msg global must be true or false global doit valoir true ou false +@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) le glyphe {_1} n’est pas dessiné pour {ch} avec le style du caractère (comme Tout en capitales, Petites capitales ou un système de chiffres) +@msg glyph {g} is not an alternate of {ch} in {_1} {_2} le glyphe {g} n’est pas une variante de {ch} dans {_1} {_2} +@msg gradient must be "objects" or "mesh" gradient doit valoir "objects" ou "mesh" +@msg grays must be "profile" or "black", not `{g}` grays doit valoir "profile" ou "black", pas `{g}` +@msg gridColors must be a preset name or two colours gridColors doit être un nom de paramètre prédéfini ou deux couleurs +@msg gridSize must be small, medium or large gridSize doit valoir small, medium ou large +@msg guide {g} is not selected le repère {g} n’est pas sélectionné +@msg handle must be 0..3 handle doit être compris entre 0 et 3 +@msg handler at {_1} expects {_2} values but its range starts with {d} le gestionnaire à {_1} attend {_2} valeurs, mais sa plage commence avec {d} +@msg handler depth too large profondeur de gestionnaire trop grande +@msg handler range {_1}..{_2} out of range plage de gestionnaire {_1}..{_2} hors limites +@msg hiddenLayers must be true or false hiddenLayers doit valoir true ou false +@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v} : "layer" (par défaut), "omit" ou "visible" +@msg highlight a layer to merge into mettez en surbrillance un calque dans lequel fusionner +@msg highlight two or more rows in one layer or group mettez en surbrillance au moins deux lignes d’un même calque ou groupe +@msg highlight two or more rows to merge mettez en surbrillance au moins deux lignes à fusionner +@msg hvcC hvcC +@msg icon docks a panel in the icon column: use window.panel.dock icon ancre un panneau dans la colonne d’icônes : utilisez window.panel.dock +@msg id must be a dimension id id doit être un identifiant de cote +@msg image has no bounds l’image n’a pas de limites +@msg image {_1} is linked already l’image {_1} est déjà liée +@msg imageMap `{m}`: none, client or server imageMap `{m}` : none, client ou server +@msg images in a spot ink whose samples couldn't be read were imported in RGB les images en encre de ton direct dont les échantillons n’ont pas pu être lus ont été importées en RVB +@msg images past the most pixels one file may hold were left out les images dépassant le nombre maximal de pixels qu’un fichier peut contenir ont été omises +@msg images too large to read were left out les images trop grandes pour être lues ont été omises +@msg index out of range indice hors limites +@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes peut contenir au plus {MAX_GUIDE_INDEXES} entrées +@msg indexes must be an array of guide indexes indexes doit être une liste d’indices de repères +@msg instances must be 1..1000 instances doit être compris entre 1 et 1000 +@msg instruction out of range instruction hors limites +@msg instruction {pc} ({op}) pops below the stack l’instruction {pc} ({op}) dépile sous le bas de la pile +@msg instruction {pc} reached with stack depths {seen} and {d} instruction {pc} atteinte avec des profondeurs de pile {seen} et {d} +@msg invalid `color` `color` non valide +@msg invalid anchor index {v}: expected a non-negative integer indice de point d’ancrage non valide {v} : entier positif ou nul attendu +@msg invalid anchor reference {v}: expected [subpath, anchor] référence de point d’ancrage non valide {v} : [subpath, anchor] attendu +@msg invalid angle angle non valide +@msg invalid array length longueur de tableau non valide +@msg invalid brush: {e} forme non valide : {e} +@msg invalid color couleur non valide +@msg invalid colour {c} couleur non valide {c} +@msg invalid delta delta non valide +@msg invalid group groupe non valide +@msg invalid object id {v}: expected a non-negative integer identifiant d’objet non valide {v} : entier positif ou nul attendu +@msg invalid point point non valide +@msg invalid regular expression flag '{_1}' indicateur d’expression régulière non valide '{_1}' +@msg invalid regular expression: {m} expression régulière non valide : {m} +@msg invalid size taille non valide +@msg invalid t t non valide +@msg invalid time value valeur de temps non valide +@msg invalid tolerance tolérance non valide +@msg is a confirm's answer, whose `value` must be true or false est la réponse à une confirmation, dont `value` doit valoir true ou false +@msg is a prompt's answer, whose `value` must be a string or null est la réponse à une invite, dont `value` doit être une chaîne ou null +@msg is an alert's answer, whose `value` must be "ok" est la réponse à une alerte, dont `value` doit valoir "ok" +@msg is longer than 16 KiB dépasse 16 Kio +@msg it draws its image through a clipping path il dessine son image à travers un tracé de détourage +@msg it draws no image il ne dessine aucune image +@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes il dépasse {MAX_DICTIONARY_FILE_BYTES} octets +@msg it isn't a file ce n’est pas un fichier +@msg it names {_1} plates, and at most {MAX_PLATES} are read il désigne {_1} plaques, et au plus {MAX_PLATES} sont lues +@msg its art is too long or too detailed for a head son illustration est trop longue ou trop détaillée pour une pointe +@msg its art paints nothing son illustration ne peint rien +@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) tous ses plans de travail se trouvent au-delà de la zone de travail (à plus de {_1} pt de l’origine) +@msg its clipping masks are nested too deeply ses masques d’écrêtage sont imbriqués trop profondément +@msg its image is too large to read son image est trop grande pour être lue +@msg its metadata has no description ses métadonnées n’ont pas de description +@msg its metadata has no direct length ses métadonnées n’ont pas de longueur directe +@msg its plates are too large: more than {MAX_SAMPLES} pixels together ses plaques sont trop grandes : plus de {MAX_SAMPLES} pixels au total +@msg its plates are too large: {_1} plates of {w} × {h} pixels ses plaques sont trop grandes : {_1} plaques de {w} × {h} pixels +@msg its plates can't be kept together ses plaques ne peuvent pas être conservées ensemble +@msg its shapes could not be combined into one ses formes n’ont pas pu être combinées en une seule +@msg join two different paths joignez deux tracés différents +@msg jump to {pc}, past the end saut vers {pc}, au-delà de la fin +@msg justify must be auto|left|center|right|justifyAll justify doit valoir auto|left|center|right|justifyAll +@msg kbps must be a positive number, not {kbps} kbps doit être un nombre positif, pas {kbps} +@msg keywords must be a list of words or a comma-separated string keywords doit être une liste de mots ou une chaîne séparée par des virgules +@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords doit compter au plus {MAX_KEYWORD} caractères, sans caractères de contrôle +@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind doit valoir 1, 2 ou 3, ou indiquez un `name` de paramètre prédéfini (voir perspective.presets.list) +@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind doit valoir graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle ou paraStyle +@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout doit valoir "on", "off", "neutral" ou un booléen, pas {v} +@msg layer `{name}` has no art to export le calque `{name}` ne contient aucune illustration à exporter +@msg layer must name a layer or sublayer layer doit désigner un calque ou un sous-calque +@msg layers can't be intertwined: select the objects in them les calques ne peuvent pas être entrelacés : sélectionnez les objets qu’ils contiennent +@msg layers go only at the top level or in layers, objects in layers and groups les calques vont uniquement au niveau supérieur ou dans des calques, les objets dans des calques et des groupes +@msg layers must be true or false layers doit valoir true ou false +@msg layout must be gridByRow, gridByColumn, row or column layout doit valoir gridByRow, gridByColumn, row ou column +@msg leading must be a number or "auto" leading doit être un nombre ou "auto" +@msg left/right must be non-negative widths left/right doivent être des largeurs positives ou nulles +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding doit valoir l’une de ces valeurs : {_1} +@msg level `{t}`: 2 or 3 niveau `{t}` : 2 ou 3 +@msg line {line}: more than {MAX_CSV_COLS} columns ligne {line} : plus de {MAX_CSV_COLS} colonnes +@msg line {line}: unclosed quote ligne {line} : guillemet non fermé +@msg line {line}: value for `{var}` must match its variable kind ligne {line} : la valeur de `{var}` doit correspondre au type de sa variable +@msg lineEndings `{other}`: lf or crlf lineEndings `{other}` : lf ou crlf +@msg loading profiles from disk isn't available in the browser le chargement de profils depuis le disque n’est pas disponible dans le navigateur +@msg localize: the first argument must be a localization object or a string localize : le premier argument doit être un objet de localisation ou une chaîne +@msg margin: a {m} pt margin leaves no imageable area on the paper marge : une marge de {m} pt ne laisse aucune zone imprimable sur le papier +@msg matrix must be finite and invertible, depth finite matrix doit être finie et inversible, et depth finie +@msg matte: {e} cache : {e} +@msg member is for order front, forward, backward or back member s’utilise avec order front, forward, backward ou back +@msg missing `artboards` `artboards` manquant +@msg missing `at` [x, y] `at` [x, y] manquant +@msg missing `attrs` `attrs` manquant +@msg missing `color` or `opacity` `color` ou `opacity` manquant +@msg missing `colors` `colors` manquant +@msg missing `commands` `commands` manquant +@msg missing `destinationFolder` `destinationFolder` manquant +@msg missing `effect` id identifiant `effect` manquant +@msg missing `family` `family` manquant +@msg missing `find` `find` manquant +@msg missing `gid` (a glyph id, or null for the default glyph) `gid` manquant (un identifiant de glyphe, ou null pour le glyphe par défaut) +@msg missing `group` `group` manquant +@msg missing `id` `id` manquant +@msg missing `id`: give one, or select one type object `id` manquant : indiquez-en un, ou sélectionnez un objet texte +@msg missing `index` (a point index, or null to clear) `index` manquant (un indice de point, ou null pour effacer) +@msg missing `index` (a stop index, or null to clear) `index` manquant (un indice d’arrêt, ou null pour effacer) +@msg missing `index` (an item index or null) `index` manquant (un indice d’élément ou null) +@msg missing `item` `item` manquant +@msg missing `library` `library` manquant +@msg missing `library` (see graphicStyle.libraries) `library` manquant (voir graphicStyle.libraries) +@msg missing `library` (see swatch.library.list) `library` manquant (voir swatch.library.list) +@msg missing `map` (rows [{from, to}] or {current}) `map` manquant (lignes [{from, to}] ou {current}) +@msg missing `matrix` [a,b,c,d,e,f] `matrix` [a,b,c,d,e,f] manquant +@msg missing `name` (see gradient.presets.list) `name` manquant (voir gradient.presets.list) +@msg missing `offset` (a distance in points) `offset` manquant (une distance en points) +@msg missing `path` `path` manquant +@msg missing `path` (a path object id) `path` manquant (un identifiant d’objet tracé) +@msg missing `picks` `picks` manquant +@msg missing `rule` `rule` manquant +@msg missing `source` id identifiant `source` manquant +@msg missing `start` (the byte offset of a character) `start` manquant (la position en octets d’un caractère) +@msg missing `status`: linked, embedded, missing or modified `status` manquant : linked, embedded, missing ou modified +@msg missing `target` id identifiant `target` manquant +@msg missing `text` `text` manquant +@msg missing `to` `to` manquant +@msg missing `to` (an index) `to` manquant (un indice) +@msg missing `to`: a non-negative integer `to` manquant : un entier positif ou nul +@msg missing `type` `type` manquant +@msg missing `value` `value` manquant +@msg missing `word` `word` manquant +@msg missing `{key}` `{key}` manquant +@msg missing `{k}` `{k}` manquant +@msg missing anchor point d’ancrage manquant +@msg missing anchors points d’ancrage manquants +@msg missing anchors [[subpath, anchor]…] anchors [[subpath, anchor]…] manquant +@msg missing array `{key}` liste `{key}` manquante +@msg missing case case manquant +@msg missing color couleur manquante +@msg missing commands commandes manquantes +@msg missing destination folder dossier de destination manquant +@msg missing from from manquant +@msg missing group groupe manquant +@msg missing id or ids id ou ids manquant +@msg missing ids ids manquant +@msg missing indexes indexes manquant +@msg missing matrix [a,b,c,d,e,f] matrix [a,b,c,d,e,f] manquant +@msg missing name nom manquant +@msg missing op op manquant +@msg missing or invalid `color` `color` manquant ou non valide +@msg missing or unknown `tool` `tool` manquant ou inconnu +@msg missing point point manquant +@msg missing point [x,y] point [x,y] manquant +@msg missing point `{key}` ([x, y]) point `{key}` manquant ([x, y]) +@msg missing points points manquants +@msg missing points [[x,y]…] points [[x,y]…] manquants +@msg missing segment segment manquant +@msg missing stops taquets manquants +@msg missing subpaths sous-tracés manquants +@msg missing target cible manquante +@msg missing to to manquant +@msg missing x, y x, y manquants +@msg mode is set, add or toggle, not `{other}` mode vaut set, add ou toggle, pas `{other}` +@msg mode must be "cmyk" or "rgb" mode doit valoir "cmyk" ou "rgb" +@msg mode must be area|onPath, got {o} mode doit valoir area|onPath, reçu : {o} +@msg more than 10000 linked files: package dependencies in smaller parts plus de 10000 fichiers liés : assemblez les dépendances en plusieurs parties plus petites +@msg more than 256 commands plus de 256 commandes +@msg more than {MAX_BATCH_FILES} data sets plus de {MAX_BATCH_FILES} jeux de données +@msg more than {MAX_BATCH_FILES} files plus de {MAX_BATCH_FILES} fichiers +@msg more than {MAX_CSV_ROWS} data rows plus de {MAX_CSV_ROWS} lignes de données +@msg more than {MAX_FILES} images plus de {MAX_FILES} images +@msg more than {MAX_IMPORT} presets plus de {MAX_IMPORT} paramètres prédéfinis +@msg moved must be [[x,y]…] moved doit être [[x,y]…] +@msg must be an object with `kind` and `value` doit être un objet avec `kind` et `value` +@msg must have `kind` alert, confirm or prompt doit avoir `kind` alert, confirm ou prompt +@msg must have exactly `kind` and `value` doit avoir exactement `kind` et `value` +@msg name (a toolbar's name) is required name (le nom d’une barre d’outils) est obligatoire +@msg name (a toolbar's name) must be a string name (le nom d’une barre d’outils) doit être une chaîne +@msg name must be a toolbar's name name doit être le nom d’une barre d’outils +@msg names is empty names est vide +@msg names must be an array of symbol names names doit être une liste de noms de symboles +@msg need `anchor` or `segment` + `t` `anchor` ou `segment` + `t` nécessaire +@msg need at least one anchor au moins un point d’ancrage est nécessaire +@msg need at least one point au moins un point est nécessaire +@msg need at least two points au moins deux points sont nécessaires +@msg needs a closing quote after the name nécessite un guillemet fermant après le nom +@msg needs a name nécessite un nom +@msg needs a name of at most 64 bytes nécessite un nom d’au plus 64 octets +@msg needs a name of letters, digits and _, or a quoted name nécessite un nom composé de lettres, de chiffres et de _, ou un nom entre guillemets +@msg needs a name without control characters nécessite un nom sans caractères de contrôle +@msg needs points nécessite des points +@msg nesting too deep imbrication trop profonde +@msg nineSlice must be true or false nineSlice doit valoir true ou false +@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides doit être [left, top, right, bottom], chacun entre -16 et 16, avec left + right < 1 et top + bottom < 1 +@msg no HP-GL format aucun format HP-GL +@msg no New Document profile called `{shown}` ({_1}) aucun profil de nouveau document nommé `{shown}` ({_1}) +@msg no PDF preset named `{n}` aucun paramètre prédéfini PDF nommé `{n}` +@msg no SVG filter at index {k} aucun filtre SVG à l’indice {k} +@msg no action set is current aucun ensemble d’actions n’est actif +@msg no active data set aucun jeu de données actif +@msg no appearance item {index} aucun élément d’aspect {index} +@msg no arrowheads file here aucun fichier de pointes de flèche ici +@msg no artboard {_1} aucun plan de travail {_1} +@msg no artboard {artboard} (0-based; the document has {_1}) aucun plan de travail {artboard} (à partir de 0 ; le document en compte {_1}) +@msg no asset {id} aucune ressource {id} +@msg no asset {id} (see assets.list) aucune ressource {id} (voir assets.list) +@msg no assets to export aucune ressource à exporter +@msg no assets yet: collect art for export first (assets.add) aucune ressource pour l’instant : rassemblez d’abord des illustrations à exporter (assets.add) +@msg no brush named `{n}` aucune forme nommée `{n}` +@msg no character at byte {start} aucun caractère à l’octet {start} +@msg no character has code {code} aucun caractère n’a le code {code} +@msg no colour aucune couleur +@msg no colour group `{group}` aucun groupe de couleurs `{group}` +@msg no colour group `{name}` aucun groupe de couleurs `{name}` +@msg no custom arrowhead {name} (see stroke.arrowheads.list) aucune pointe de flèche personnalisée {name} (voir stroke.arrowheads.list) +@msg no data sets aucun jeu de données +@msg no dataset named `{from}` aucun jeu de données nommé `{from}` +@msg no dataset named `{name}` aucun jeu de données nommé `{name}` +@msg no datasets aucun jeu de données +@msg no design named `{name}` aucun motif nommé `{name}` +@msg no design named `{n}` aucun motif nommé `{n}` +@msg no design named `{old}` aucun motif nommé `{old}` +@msg no encoder for {_1} yet pas encore d’encodeur pour {_1} +@msg no export preset `{name}` (see document.exportPresets.list) aucun paramètre prédéfini d’export `{name}` (voir document.exportPresets.list) +@msg no file to open aucun fichier à ouvrir +@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder aucun dossier : passez `path` ou définissez Préférences › Plug-ins et disques de travail › Dossier de plug-ins supplémentaire +@msg no fonts to look for: they are all available aucune police à rechercher : elles sont toutes disponibles +@msg no formats to export aucun format à exporter +@msg no gradient preset `{name}` (see gradient.presets.list) aucun paramètre prédéfini de dégradé `{name}` (voir gradient.presets.list) +@msg no graphic style `{n}` in `{_1}` aucun style graphique `{n}` dans `{_1}` +@msg no graphic style library `{key}` (see graphicStyle.libraries) aucune bibliothèque de styles graphiques `{key}` (voir graphicStyle.libraries) +@msg no guide selected aucun repère sélectionné +@msg no guide selected (or give an index) aucun repère sélectionné (ou indiquez un indice) +@msg no guide {i} aucun repère {i} +@msg no images in the folder aucune image dans le dossier +@msg no instructions aucune instruction +@msg no item at index {_1} aucun élément à l’indice {_1} +@msg no item at index {from} aucun élément à l’indice {from} +@msg no item at one of the indices {idx} aucun élément à l’un des indices {idx} +@msg no layer aucun calque +@msg no layer to rename aucun calque à renommer +@msg no layer {id} (see layer.list) aucun calque {id} (voir layer.list) +@msg no layers to export aucun calque à exporter +@msg no library `{key}` aucune bibliothèque `{key}` +@msg no metafile format aucun format de métafichier +@msg no object {_1} aucun objet {_1} +@msg no objects aucun objet +@msg no parent layer aucun calque parent +@msg no perspective grid preset named `{b}` (see perspective.presets.list) aucun paramètre prédéfini de grille de perspective nommé `{b}` (voir perspective.presets.list) +@msg no perspective grid preset named `{n}` aucun paramètre prédéfini de grille de perspective nommé `{n}` +@msg no point selected (give `index`) aucun point sélectionné (indiquez `index`) +@msg no preset called `{from}` (see file.newPresets) aucun paramètre prédéfini nommé `{from}` (voir file.newPresets) +@msg no preset called `{name}` (see file.newPresets) aucun paramètre prédéfini nommé `{name}` (voir file.newPresets) +@msg no preset named `{n}` aucun paramètre prédéfini nommé `{n}` +@msg no print preset named `{b}` (see print.presets.list) aucun paramètre prédéfini d’impression nommé `{b}` (voir print.presets.list) +@msg no print preset named `{name}` (presets: {_1}) aucun paramètre prédéfini d’impression nommé `{name}` (paramètres prédéfinis : {_1}) +@msg no print preset named `{n}` aucun paramètre prédéfini d’impression nommé `{n}` +@msg no profile named `{name}` aucun profil nommé `{name}` +@msg no random numbers ({e}) aucun nombre aléatoire ({e}) +@msg no readable files in the folder aucun fichier lisible dans le dossier +@msg no recovery copy `{f}` (see file.recovery.list) aucune copie de récupération `{f}` (voir file.recovery.list) +@msg no rows aucune ligne +@msg no saved PDF preset named `{name}` aucun paramètre prédéfini PDF enregistré nommé `{name}` +@msg no saved export preset `{name}` aucun paramètre prédéfini d’export enregistré `{name}` +@msg no saved gradient preset called {name} aucun paramètre prédéfini de dégradé enregistré nommé {name} +@msg no saved gradient presets aucun paramètre prédéfini de dégradé enregistré +@msg no saved preset called `{name}` aucun paramètre prédéfini enregistré nommé `{name}` +@msg no saved presets to export (name [Default] in `names` to export it) aucun paramètre prédéfini enregistré à exporter (indiquez [Default] dans `names` pour l’exporter) +@msg no saved presets to export (name built-in ones in `names`) aucun paramètre prédéfini enregistré à exporter (indiquez les paramètres intégrés dans `names`) +@msg no saved print preset named `{name}` aucun paramètre prédéfini d’impression enregistré nommé `{name}` +@msg no saved profile called {name} aucun profil enregistré nommé {name} +@msg no saved selection `{name}` aucune sélection enregistrée `{name}` +@msg no saved theme called {name} (see colorTheme.list) aucun thème enregistré nommé {name} (voir colorTheme.list) +@msg no saved views aucune vue enregistrée +@msg no selected or given object has a visible 3D effect aucun objet sélectionné ou indiqué n’a d’effet 3D visible +@msg no series {i} aucune série {i} +@msg no slice {n} in the image (see slice.list) aucune tranche {n} dans l’image (voir slice.list) +@msg no source art aucune illustration source +@msg no stop {i} (the gradient has {n}) aucun arrêt {i} (le dégradé en compte {n}) +@msg no style `{name}` aucun style `{name}` +@msg no such artboard {v} plan de travail {v} introuvable +@msg no such assets (see assets.list) ressources introuvables (voir assets.list) +@msg no such frame anchor, or the frame would reach past the canvas point d’ancrage de cadre introuvable, ou le cadre dépasserait la zone de travail +@msg no such objects objets introuvables +@msg no such path anchor, or the path would reach past the canvas point d’ancrage de tracé introuvable, ou le tracé dépasserait la zone de travail +@msg no such rows lignes introuvables +@msg no swatch `{keep}` aucune nuance `{keep}` +@msg no swatch `{n}` aucune nuance `{n}` +@msg no swatch `{n}` (swatches and colour groups move separately) aucune nuance `{n}` (les nuances et les groupes de couleurs se déplacent séparément) +@msg no swatch library `{key}` (see swatch.library.list) aucune bibliothèque de nuances `{key}` (voir swatch.library.list) +@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) aucune bibliothèque de nuances `{key}` (voir swatch.library.list ; "document" : les nuances du document) +@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) aucune bibliothèque de nuances `{key}` (voir swatch.library.list ; "{DOCUMENT_SWATCHES}" : les nuances du document) +@msg no swatch named `{n}` aucune nuance nommée `{n}` +@msg no swatch or colour group `{n}` aucune nuance ni aucun groupe de couleurs `{n}` +@msg no swatch or colour group `{n}` in `{_1}` aucune nuance ni aucun groupe de couleurs `{n}` dans `{_1}` +@msg no symbol named `{_1}` aucun symbole nommé `{_1}` +@msg no symbol named `{name}` aucun symbole nommé `{name}` +@msg no text objects selected aucun objet texte sélectionné +@msg no toolbar named `{_1}` aucune barre d’outils nommée `{_1}` +@msg no toolbar named `{name}` aucune barre d’outils nommée `{name}` +@msg no tracing preset `{preset}` (see imageTrace.presets) aucun paramètre prédéfini de vectorisation `{preset}` (voir imageTrace.presets) +@msg no user library folder here (save with a path, or without one to get the data) aucun dossier de bibliothèques utilisateur ici (enregistrez avec un chemin, ou sans chemin pour obtenir les données) +@msg no variable named `{from}` aucune variable nommée `{from}` +@msg no variable named `{name}` aucune variable nommée `{name}` +@msg no variable named `{var}` aucune variable nommée `{var}` +@msg no variable named `{v}` aucune variable nommée `{v}` +@msg no view named `{name}` aucune vue nommée `{name}` +@msg node {_1} already has the text variable `{_2}` le nœud {_1} a déjà la variable de texte `{_2}` +@msg node {_1} is not a graph le nœud {_1} n’est pas un graphe +@msg node {_1} is not an image le nœud {_1} n’est pas une image +@msg node {_1} is not type le nœud {_1} n’est pas du texte +@msg not a DCS file ce n’est pas un fichier DCS +@msg not a PDF presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis PDF (format `{_1}`) +@msg not a PDF presets file: {e} ce n’est pas un fichier de paramètres prédéfinis PDF : {e} +@msg not a RegExp object ce n’est pas un objet RegExp +@msg not a Word document: its WordDocument stream doesn't start with a file information block ce n’est pas un document Word : son flux WordDocument ne commence pas par un bloc d’informations de fichier +@msg not a Word document: its compound file has no WordDocument stream ce n’est pas un document Word : son fichier composé n’a pas de flux WordDocument +@msg not a Word document: its main part is not a WordprocessingML document ce n’est pas un document Word : sa partie principale n’est pas un document WordprocessingML +@msg not a Word document: its package has no main document part ce n’est pas un document Word : son package n’a pas de partie de document principale +@msg not a closed path ce n’est pas un tracé fermé +@msg not a flattener presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis d’aplatissement (format `{_1}`) +@msg not a flattener presets file: {e} ce n’est pas un fichier de paramètres prédéfinis d’aplatissement : {e} +@msg not a layer or group ce n’est ni un calque ni un groupe +@msg not a library file: {e} ce n’est pas un fichier de bibliothèque : {e} +@msg not a mesh envelope point ce n’est pas un point d’enveloppe de maillage +@msg not a perspective grid presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis de grille de perspective (format `{_1}`) +@msg not a perspective grid presets file: {e} ce n’est pas un fichier de paramètres prédéfinis de grille de perspective : {e} +@msg not a print presets file (format `{_1}`) ce n’est pas un fichier de paramètres prédéfinis d’impression (format `{_1}`) +@msg not a print presets file: {e} ce n’est pas un fichier de paramètres prédéfinis d’impression : {e} +@msg not a regular expression ce n’est pas une expression régulière +@msg not a stored or deflated central record ce n’est pas un enregistrement central stocké ou compressé +@msg not a valid SVGZ file: {e} ce n’est pas un fichier SVGZ valide : {e} +@msg not an RTF file: it doesn't start with {\rtf ce n’est pas un fichier RTF : il ne commence pas par {\rtf +@msg not an action set: the text doesn't start with /version and a number ce n’est pas un ensemble d’actions : le texte ne commence pas par /version et un nombre +@msg not an image VectorCraft reads (see document.formats) ce n’est pas une image que VectorCraft lit (voir document.formats) +@msg not area type (text in a frame) outside perspective ce n’est pas un texte captif (texte dans un cadre) hors perspective +@msg not editing an opacity mask aucun masque d’opacité en cours de modification +@msg not type on a path outside perspective ce n’est pas un texte curviligne hors perspective +@msg nothing prints rien ne s’imprime +@msg nothing to bind rien à lier +@msg nothing to change rien à modifier +@msg nothing to change: give dx, dy, hScale, vScale or rotation rien à modifier : indiquez dx, dy, hScale, vScale ou rotation +@msg nothing to crop rien à recadrer +@msg nothing to export rien à exporter +@msg nothing to export: the document has no visible art rien à exporter : le document n’a aucune illustration visible +@msg nothing to mark rien à marquer +@msg nothing to print: every artboard picked is blank rien à imprimer : chaque plan de travail choisi est vide +@msg nothing to print: the document has no art that prints rien à imprimer : le document n’a aucune illustration imprimable +@msg nothing to repeat rien à répéter +@msg numbers out of order in {_1} quantifier nombres dans le désordre dans le quantificateur {_1} +@msg object is not a path l’objet n’est pas un tracé +@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object la transparence d’un objet ne peut pas être prélevée sur des caractères en surbrillance ; désactivez Transparence dans les options de la pipette ou sélectionnez l’objet entier +@msg object {_1} can't be set as point type l’objet {_1} ne peut pas être converti en texte de point +@msg object {_1} has lost its file's contents: relink it l’objet {_1} a perdu le contenu de son fichier : liez-le à nouveau +@msg object {_1} has no path l’objet {_1} n’a pas de tracé +@msg object {_1} has no size l’objet {_1} n’a pas de taille +@msg object {_1} is hidden or locked, or on a locked artboard l’objet {_1} est masqué ou verrouillé, ou sur un plan de travail verrouillé +@msg object {_1} is not a placed document l’objet {_1} n’est pas un document importé +@msg object {_1} is not an Intertwine group l’objet {_1} n’est pas un groupe Entrelacer +@msg object {_1} is not an image l’objet {_1} n’est pas une image +@msg object {_1} is not an image or a linked file l’objet {_1} n’est ni une image ni un fichier lié +@msg object {_1} is not an image or a placed document l’objet {_1} n’est ni une image ni un document importé +@msg object {_1} is not in this Intertwine group l’objet {_1} n’est pas dans ce groupe Entrelacer +@msg object {_1} needs a 3D effect l’objet {_1} nécessite un effet 3D +@msg object {_1} needs a 3D effect at index {index} l’objet {_1} nécessite un effet 3D à l’indice {index} +@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic objet {_1} : une extrusion et biseautage creuse n’a pas de fermetures : activez Fermeture pour placer un graphique +@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front objet {_1} : avec une profondeur de 0, l’objet n’a qu’une face : placez ses graphiques sur l’avant +@msg object {_1}: {_2} objet {_1} : {_2} +@msg object {_1}: {e} objet {_1} : {e} +@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams objet {_1} : {name} à l’indice {index} n’a pas de paramètres prédéfinis d’éclairage ; définissez ses éclairages avec effect.setParams +@msg object {id} has anchors 0–{_1} for `corners`, not {past} l’objet {id} a les points d’ancrage 0–{_1} pour `corners`, pas {past} +@msg object {id} has {n} anchors, so no anchor {past} l’objet {id} a {n} points d’ancrage, donc pas de point d’ancrage {past} +@msg objects must be none, move or copy objects doit valoir none, move ou copy +@msg objects were left out des objets ont été omis +@msg offset out of range décalage hors limites +@msg on must be true or false on doit valoir true ou false +@msg only groups, layers, type and images have a Contents (Characters) row seuls les groupes, calques, textes et images ont une ligne Contenu (Caractères) +@msg only interior mesh lines can be deleted seules les lignes de filet intérieures peuvent être supprimées +@msg only solid colours can be added (not gradients or patterns) seules les couleurs unies peuvent être ajoutées (pas les dégradés ni les motifs) +@msg only solid-colour swatches can be spot colours seules les nuances de couleur unie peuvent être des tons directs +@msg only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first seul l’aperçu incorporé de ce fichier Affinity a pu être lu : utilisez Fichier › Ouvrir pour l’afficher avec son avertissement, ou exportez d’abord en SVG ou PDF depuis Affinity +@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning seul l’aperçu incorporé de ce fichier Affinity a pu être lu ; utilisez Fichier › Ouvrir pour l’afficher avec son avertissement +@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon et un emplacement de colonne s’utilisent avec window.panel.float et window.panel.dock +@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon et un emplacement de colonne n’acceptent ni anchor, ni side, ni zone, ni before +@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon et un emplacement de colonne n’acceptent ni x, ni y, ni width, ni height +@msg opacity must be a number opacity doit être un nombre +@msg open a document to look for its linked files ouvrez un document pour rechercher ses fichiers liés +@msg operand too large opérande trop grand +@msg options must be an object (see file.formatOptions) options doit être un objet (voir file.formatOptions) +@msg order is a whole number from 0 to {others}, not `{v}` order est un nombre entier de 0 à {others}, pas `{v}` +@msg order must be leftToRight or rightToLeft order doit valoir leftToRight ou rightToLeft +@msg orientation must be portrait or landscape, not `{o}` orientation doit valoir portrait ou landscape, pas `{o}` +@msg outlineText {v}: true or false outlineText {v} : true ou false +@msg output intent profile: {e} profil de mode de sortie : {e} +@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination : aucun profil RVB ou CMJN ne s’appelle « {name} » +@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination : les fichiers {_1} sont en CMJN, et « {name} » est un profil RVB +@msg output.destination: {e} output.destination : {e} +@msg output.inks: no ink is set to print output.inks : aucune encre n’est réglée pour l’impression +@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent : aucun profil ne s’appelle « {name} », et un mode de sortie {std} incorpore son profil +@msg output.outputIntent: no profile is called “{name}”: a {std} output intent embeds a profile, or names a registered printing condition (outputConditionId and registry) output.outputIntent : aucun profil ne s’appelle « {name} » : un mode de sortie {std} incorpore un profil, ou désigne une condition d’impression enregistrée (outputConditionId et registry) +@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent : les fichiers {std} s’impriment en CMJN, et « {name} » est un profil RVB +@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent : « {name} » est un profil de niveaux de gris, qui ne peut pas être un mode de sortie {std} +@msg overlap: {o} pt is half a tile or more chevauchement : {o} pt représente la moitié d’une mosaïque ou plus +@msg overlapping Shaper paths must be in the same layer or group les tracés Shaper qui se chevauchent doivent être dans le même calque ou groupe +@msg overlapping edits modifications qui se chevauchent +@msg page art conflicts with image data for {key} l’illustration de la page est en conflit avec les données d’image de {key} +@msg page art conflicts with pattern {name} l’illustration de la page est en conflit avec le motif {name} +@msg page art conflicts with swatch {name} l’illustration de la page est en conflit avec la nuance {name} +@msg page art is missing image data for {key} il manque à l’illustration de la page les données d’image de {key} +@msg page art is missing pattern {name} il manque à l’illustration de la page le motif {name} +@msg page art is missing swatch {name} il manque à l’illustration de la page la nuance {name} +@msg page must be a whole number from 1 page doit être un nombre entier à partir de 1 +@msg page thumbnail: {e} vignette de page : {e} +@msg page {page} of the PDF has no objects la page {page} du PDF n’a aucun objet +@msg page {page}: `{file}` has {n} {artboards} page {page} : `{file}` contient {n} {artboards} +@msg page {page}: `{name}` has {n} {pages} page {page} : `{name}` contient {n} {pages} +@msg pages must be a range such as "2-3" or a page number, not {v} pages doit être une plage comme "2-3" ou un numéro de page, pas {v} +@msg pages: {e} pages : {e} +@msg panel position must be finite la position du panneau doit être finie +@msg parameters must be an object parameters doit être un objet +@msg params must be an object params doit être un objet +@msg passwords can have up to {MAX_PASSWORD} bytes les mots de passe peuvent compter jusqu’à {MAX_PASSWORD} octets +@msg path chunks must share their boundary point les fragments de tracé doivent partager leur point de jonction +@msg path has no open end le tracé n’a pas d’extrémité ouverte +@msg paths must be strings paths doit contenir des chaînes +@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false nécessite que chaque plan de travail conserve l’illustration modifiable +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false nécessite que les plans de travail conservent l’illustration modifiable quand useArtboards vaut false +@msg pdfs `{m}`: single or multiple pdfs `{m}` : single ou multiple +@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom vaut "artwork" ou "swatches", pas {v} +@msg pins and moved must be non-empty lists of the same length pins et moved doivent être des listes non vides de même longueur +@msg pins must be [[x,y]…] pins doit être [[x,y]…] +@msg pins must be finite pins doit être fini +@msg place is above, below or inside, not `{other}` place vaut above, below ou inside, pas `{other}` +@msg plane must be left|right|ground plane doit valoir left|right|ground +@msg plane must be left|right|ground|none plane doit valoir left|right|ground|none +@msg point is outside the mesh le point est en dehors du filet +@msg point x must be a number le x du point doit être un nombre +@msg point y must be a number le y du point doit être un nombre +@msg points must be [t, left, right] with non-negative widths points doit être [t, left, right] avec des largeurs positives ou nulles +@msg points must be [x,y] pairs points doit être des paires [x,y] +@msg points must be a non-empty [[x,y,pressure?]…] list points doit être une liste [[x,y,pressure?]…] non vide +@msg points must be a non-empty list of finite [x,y] points doit être une liste non vide de [x,y] finis +@msg points must be an array or null points doit être une liste ou null +@msg points must be finite points doit être fini +@msg points must be finite [x,y] pairs points doit être des paires [x,y] finies +@msg points must be finite canvas points points doit être des points finis de la zone de travail +@msg points must contain 1–4096 coordinates points doit contenir de 1 à 4096 coordonnées +@msg position must be normal, superscript or subscript position doit valoir normal, superscript ou subscript +@msg position must be topLeft, topRight, bottomLeft or bottomRight position doit valoir topLeft, topRight, bottomLeft ou bottomRight +@msg positions must be between 0 and 100000 pt les positions doivent être comprises entre 0 et 100000 pt +@msg ppi must be a positive number, not {ppi} ppi doit être un nombre positif, pas {ppi} +@msg ppi must be between 1 and 2400 ppi doit être compris entre 1 et 2400 +@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}` n’est pas transforms|bounds|fileDimensions|fit|fill +@msg preserveShape must be clippingMask|transparency preserveShape doit valoir clippingMask|transparency +@msg preset `{id}`: mobile or density preset `{id}` : mobile ou density +@msg preset must be standard, diffuse, topLeft or right preset doit valoir standard, diffuse, topLeft ou right +@msg preset must be the name of a print preset preset doit être le nom d’un paramètre prédéfini d’impression +@msg previewMode must be default, pixel or overprint previewMode doit valoir default, pixel ou overprint +@msg print settings: {e} réglages d’impression : {e} +@msg procedures nest too deeply les procédures sont imbriquées trop profondément +@msg procedures nested too deeply procédures imbriquées trop profondément +@msg quotes must be {double?, single?} quotes doit être {double?, single?} +@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k} doit être un guillemet ouvrant et un guillemet fermant, par exemple "“”" +@msg range out of order in character class plage dans le désordre dans une classe de caractères +@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi doit être compris entre 1 et 2400 (72 écran, 150 moyenne, 300 haute) +@msg rating must be a whole number from 0 to {MAX_RATING} rating doit être un nombre entier compris entre 0 et {MAX_RATING} +@msg rect and {k} contradict each other: give the area or the artboards rect et {k} se contredisent : indiquez la zone ou les plans de travail +@msg rect applies to raster formats, not {_1} rect s’applique aux formats pixellisés, pas à {_1} +@msg rect is [x, y, width, height] in points rect est [x, y, width, height] en points +@msg rect must be [x, y, width, height] rect doit être [x, y, width, height] +@msg rect must be [x, y, width, height] with a positive width and height rect doit être [x, y, width, height] avec une largeur et une hauteur positives +@msg rect needs finite numbers and a positive width and height rect nécessite des nombres finis et une largeur et une hauteur positives +@msg redraw needs id redraw nécessite id +@msg redraw within must be a finite distance of 0 or more within de redraw doit être une distance finie supérieure ou égale à 0 +@msg reduce of an empty array with no initial value reduce d’un tableau vide sans valeur initiale +@msg registration must be 0..8 registration doit être compris entre 0 et 8 +@msg regular expression flag '{_1}' given twice indicateur d’expression régulière '{_1}' fourni deux fois +@msg release a layer or a group libérez un calque ou un groupe +@msg replace keeps the replaced object's place and transform: drop template, at and rect replace conserve la position et la transformation de l’objet remplacé : retirez template, at et rect +@msg replace: select the one object to replace replace : sélectionnez le seul objet à remplacer +@msg resolution must be a number or screen|medium|high resolution doit être un nombre ou screen|medium|high +@msg resolution must be between 1 and 2400 ppi la résolution doit être comprise entre 1 et 2400 ppp +@msg rotation must be a number of degrees rotation doit être un nombre de degrés +@msg rotation must be finite rotation doit être finie +@msg rows and cols must be 1..50 rows et cols doivent être compris entre 1 et 50 +@msg rows and columns must be 1..500 rows et columns doivent être compris entre 1 et 500 +@msg rows of objects whose appearances differ can't be reordered together les lignes d’objets dont l’aspect diffère ne peuvent pas être réordonnées ensemble +@msg sampled text would exceed the canvas (coordinates out of range) le texte prélevé dépasserait la zone de travail (coordonnées hors limites) +@msg save the document first: Package collects a saved document enregistrez d’abord le document : Assemblage rassemble un document enregistré +@msg save the document first: copy puts the file in its folder enregistrez d’abord le document : copy place le fichier dans son dossier +@msg savedPreset: {e} savedPreset : {e} +@msg scale `{s}`: a factor such as 2x, a width (100w), a height (100h) or a resolution (72ppi) échelle `{s}` : un facteur comme 2x, une largeur (100w), une hauteur (100h) ou une résolution (72ppi) +@msg scale must be a number above 0, at most 64 scale doit être un nombre supérieur à 0, d’au plus 64 +@msg scale must be non-zero scale doit être non nul +@msg scope must be selection or all (got {other}) scope doit valoir selection ou all (reçu : {other}) +@msg scribble is too long le gribouillis est trop long +@msg segment does not exist le segment n’existe pas +@msg select a Shaper group sélectionnez un groupe Shaper +@msg select a blend to set easing or color shift sélectionnez un dégradé de formes pour régler l’accélération ou le décalage de couleur +@msg select a compound shape sélectionnez une forme composée +@msg select a graph sélectionnez un graphe +@msg select a group or layer with visible, unlocked members sélectionnez un groupe ou un calque dont les membres sont visibles et déverrouillés +@msg select a layer sélectionnez un calque +@msg select a text object sélectionnez un objet texte +@msg select an SVG filter sélectionnez un filtre SVG +@msg select an image (or an Image Trace group) sélectionnez une image (ou un groupe de vectorisation de l’image) +@msg select an image, or pass id sélectionnez une image, ou passez id +@msg select an object (or pass `id`) sélectionnez un objet (ou passez `id`) +@msg select an object with a 3D effect sélectionnez un objet doté d’un effet 3D +@msg select an object with a 3D effect, or give its id in ids sélectionnez un objet doté d’un effet 3D, ou indiquez son identifiant dans ids +@msg select an object with a freeform gradient sélectionnez un objet doté d’un dégradé de forme libre +@msg select an object with an opacity mask sélectionnez un objet doté d’un masque d’opacité +@msg select area type sélectionnez du texte captif +@msg select area type (text in a frame) sélectionnez du texte captif (texte dans un cadre) +@msg select area type and closed paths (at least one area type object) sélectionnez du texte captif et des tracés fermés (au moins un objet texte captif) +@msg select area type whose first line can fit its frame sélectionnez du texte captif dont la première ligne tient dans son cadre +@msg select artwork to add its colours sélectionnez l’illustration dont ajouter les couleurs +@msg select artwork to make a group of its colours sélectionnez l’illustration dont créer un groupe de couleurs +@msg select artwork to recolour, or give a colour `group` sélectionnez l’illustration dont redéfinir les couleurs, ou indiquez un groupe de couleurs `group` +@msg select exactly two path ends to join sélectionnez exactement deux extrémités de tracé à joindre +@msg select members of a compound shape sélectionnez des membres d’une forme composée +@msg select objects or give ids sélectionnez des objets ou indiquez ids +@msg select objects to align to the guide sélectionnez les objets à aligner sur le repère +@msg select one Intertwine group sélectionnez un seul groupe Entrelacer +@msg select one image or pass id sélectionnez une seule image ou passez id +@msg select one layer, group or object to rename sélectionnez un seul calque, groupe ou objet à renommer +@msg select paths sélectionnez des tracés +@msg select paths or compound paths sélectionnez des tracés ou des tracés transparents +@msg select rows or objects to collect sélectionnez les lignes ou objets à rassembler +@msg select something to export sélectionnez quelque chose à exporter +@msg select something to export (or turn off selectedOnly) sélectionnez quelque chose à exporter (ou désactivez selectedOnly) +@msg select text to redefine the style from sélectionnez le texte à partir duquel redéfinir le style +@msg select the art and, on top of it, the mask object sélectionnez l’illustration et, au-dessus, l’objet de masque +@msg select the art for the design sélectionnez l’illustration du motif +@msg select the art for the pattern tile sélectionnez l’illustration du carreau de motif +@msg select the art to add first sélectionnez d’abord l’illustration à ajouter +@msg select the art to collect (or give its ids) sélectionnez l’illustration à rassembler (ou indiquez ses identifiants) +@msg select the art to weed sélectionnez l’illustration à écheniller +@msg select the content and a path on top sélectionnez le contenu et un tracé au-dessus +@msg select the images to relink, or pass ids sélectionnez les images à lier à nouveau, ou passez ids +@msg select the images, or pass ids sélectionnez les images, ou passez ids +@msg select the linked images or placed documents, or pass ids sélectionnez les images liées ou les documents importés, ou passez ids +@msg select the object to copy from sélectionnez l’objet à partir duquel copier +@msg select the objects (or give their ids) sélectionnez les objets (ou indiquez leurs identifiants) +@msg select the objects to attach to the path sélectionnez les objets à attacher au tracé +@msg select the objects to bind sélectionnez les objets à lier +@msg select two or more objects to intertwine sélectionnez au moins deux objets à entrelacer +@msg select two or more slices to combine sélectionnez au moins deux tranches à combiner +@msg select type on a path sélectionnez du texte curviligne +@msg select type on a path or area type sélectionnez du texte curviligne ou captif +@msg selection `{m}`: one or each selection `{m}` : one ou each +@msg settings is an object of document.exportForScreens params settings est un objet de paramètres de document.exportForScreens +@msg settings must be an object settings doit être un objet +@msg show `{show}`: all, missing, modified or embedded show `{show}` : all, missing, modified ou embedded +@msg show must be true or false show doit valoir true ou false +@msg side is left or right, not `{v}` side vaut left ou right, pas `{v}` +@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size doit valoir "small", "medium", "large" ou un nombre de points supérieur à 0 et d’au plus {_1} +@msg size must be positive size doit être positif +@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode vaut "fixed" ou "pressure", pas `{other}` +@msg sizeMode is "fixed" or "pressure", not {other} sizeMode vaut "fixed" ou "pressure", pas {other} +@msg sizeVariation must be 0 or more sizeVariation doit être supérieur ou égal à 0 +@msg smallCaps must be true or false smallCaps doit valoir true ou false +@msg sort `{x}`: name, kind or status sort `{x}` : name, kind ou status +@msg source paths are too large for Shaper les tracés source sont trop grands pour Shaper +@msg source text transform is collapsed or unrepresentable la transformation du texte source est écrasée ou impossible à représenter +@msg spot, mode and group apply to solid colours only spot, mode et group s’appliquent uniquement aux couleurs unies +@msg status `{s}`: linked, embedded, missing or modified status `{s}` : linked, embedded, missing ou modified +@msg steps must be 1..1000 steps doit être compris entre 1 et 1000 +@msg steps must be 1..{MAX_STEPS} steps doit être compris entre 1 et {MAX_STEPS} +@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) arrêt {i} : `plates` doit être quatre nombres compris entre 0 et 1 (C, M, Y, K) +@msg stopped before the archive was finished arrêté avant la fin de l’archive +@msg strokeMode must be within|along|across, got {m} strokeMode doit valoir within|along|across, reçu : {m} +@msg strokeOptions must be an object of stroke.set options strokeOptions doit être un objet d’options de stroke.set +@msg strokeWidth must be a positive number of pixels strokeWidth doit être un nombre positif de pixels +@msg subpath {si} is not an open subpath le sous-tracé {si} n’est pas un sous-tracé ouvert +@msg subpaths required when joining the same path subpaths est obligatoire pour joindre un même tracé +@msg swatchConflict must be "merge" or "add", not {v} swatchConflict doit valoir "merge" ou "add", pas {v} +@msg table must be true or false table doit valoir true ou false +@msg takes one name accepte un seul nom +@msg target text transform is collapsed or unrepresentable la transformation du texte cible est écrasée ou impossible à représenter +@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text doit être {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} +@msg text transform is collapsed or unrepresentable la transformation du texte est écrasée ou impossible à représenter +@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces doit être un nombre entier compris entre 2 et 100 +@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}` : utilisez "{_1}" ou "{_2}" +@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage : indiquez {path} ou {name, dataBase64} d’un fichier image, ou une texture telle que la donne effect.list +@msg textureImage: {e} textureImage : {e} +@msg that makes more than {MAX_PIECES} slices cela produit plus de {MAX_PIECES} tranches +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer cela produit trop de morceaux (au plus 100000) ou d’étapes de tirets : simplifiez le motif de tirets ou allongez les tirets +@msg that object is not in the overlap cet objet n’est pas dans le chevauchement +@msg the Affinity document has no page with a size le document Affinity n’a aucune page dotée d’une taille +@msg the CMYK image couldn't be written l’image CMJN n’a pas pu être écrite +@msg the Layers panel is not shown le panneau Calques n’est pas affiché +@msg the Layers panel's search, filter or Show Layers Only hides its row la recherche, le filtre ou l’option Afficher les calques uniquement du panneau Calques masque sa ligne +@msg the Objects on Path group is hidden or locked le groupe Objets sur tracé est masqué ou verrouillé +@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected les réglages de sécurité PDF ont été enregistrés sans leurs mots de passe : indiquez à nouveau openPassword ou permissionsPassword, ou security sans passwordRequired pour exporter sans protection +@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) le fichier RTF a trop de paragraphes ou de changements de mise en forme pour être importé comme texte (au plus {MAX_PARAGRAPHS} paragraphes et {MAX_RUNS} segments) +@msg the RTF file is too large to place as type (at most {_1} MB) le fichier RTF est trop volumineux pour être importé comme texte (au plus {_1} Mo) +@msg the SVG expands to more than {0} nested SVG images le SVG se développe en plus de {0} images SVG imbriquées +@msg the SVG expands to more than {0} objects le SVG se développe en plus de {0} objets +@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes le fichier SVG dépasse {MAX_IMPORT_BYTES} octets +@msg the SVG has no drawable objects le SVG n’a aucun objet dessinable +@msg the SVGZ file unpacks to more than {_1} MB le fichier SVGZ se décompresse en plus de {_1} Mo +@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place le document Word a plus de {MAX_PAGES} pages de propriétés, trop pour être importé +@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place le document Word a plus de {MAX_RUNS} segments de propriétés de caractère ou de paragraphe, trop pour être importé +@msg the Word document has more than {_1} MB of formatting properties, too many to place le document Word a plus de {_1} Mo de propriétés de mise en forme, trop pour être importé +@msg the Word document has too much text or formatting to place as type (at most {MAX_PARAGRAPHS} paragraphs, {MAX_RUNS} runs and {_1} MB of text) le document Word a trop de texte ou de mise en forme pour être importé comme texte (au plus {MAX_PARAGRAPHS} paragraphes, {MAX_RUNS} segments et {_1} Mo de texte) +@msg the Word document has too much text to place as type (at most {_1} MB) le document Word a trop de texte pour être importé comme texte (au plus {_1} Mo) +@msg the Word document is encrypted or protected with a password; save it without one to place it le document Word est chiffré ou protégé par un mot de passe ; enregistrez-le sans mot de passe pour l’importer +@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) le document Word est trop volumineux pour être importé (son flux {name} contient plus de {_1} Mo) +@msg the Word document was last saved with fast save, which VectorCraft doesn't read; save it again with fast save turned off le document Word a été enregistré en dernier avec l’enregistrement rapide, que VectorCraft ne lit pas ; enregistrez-le à nouveau avec l’enregistrement rapide désactivé +@msg the Word document was saved by Word 6.0 or Word 95, which VectorCraft doesn't read; save it as a Word 97-2003 document le document Word a été enregistré par Word 6.0 ou Word 95, que VectorCraft ne lit pas ; enregistrez-le en tant que document Word 97-2003 +@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place le texte du document Word se compose de plus de {MAX_PIECES} morceaux, trop pour être importé +@msg the XML operation needs an XML or XMLList value l’opération XML nécessite une valeur XML ou XMLList +@msg the XPath expression does not evaluate to a node-set l’expression XPath ne donne pas un ensemble de nœuds +@msg the XPath expression ends early l’expression XPath se termine trop tôt +@msg the XPath expression has an unmatched ) l’expression XPath a une ) sans correspondance +@msg the XPath expression has an unmatched ] l’expression XPath a un ] sans correspondance +@msg the XPath expression has an unmatched closing bracket l’expression XPath a un crochet fermant sans correspondance +@msg the XPath expression selects a namespace node, which is not an XML value l’expression XPath sélectionne un nœud d’espace de noms, qui n’est pas une valeur XML +@msg the XPath expression selects the root node, which is not an XML value l’expression XPath sélectionne le nœud racine, qui n’est pas une valeur XML +@msg the XPath function {name} needs a node-set la fonction XPath {name} nécessite un ensemble de nœuds +@msg the XPath function {name} takes the wrong number of arguments la fonction XPath {name} reçoit un nombre d’arguments incorrect +@msg the XPath name functions need a node-set les fonctions de nom XPath nécessitent un ensemble de nœuds +@msg the XPath prefix {shown} is not bound to a namespace le préfixe XPath {shown} n’est lié à aucun espace de noms +@msg the XPath variable ${shown} is not bound la variable XPath ${shown} n’est pas liée +@msg the action set ids are not a list les identifiants des ensembles d’actions ne sont pas une liste +@msg the active paint is not a gradient la peinture active n’est pas un dégradé +@msg the argument list is not an object la liste d’arguments n’est pas un objet +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size les plans de travail ne tiennent pas dans la zone de travail : réduisez leur nombre, l’espacement ou la taille +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out l’illustration est trop volumineuse pour l’EPS : ce qui suit 256 Mo de PostScript a été omis +@msg the bitmaps hold more pixels than one file may les images bitmap contiennent plus de pixels qu’un fichier ne peut en contenir +@msg the character's size can't be measured la taille du caractère ne peut pas être mesurée +@msg the code runs past its last instruction le code s’exécute au-delà de sa dernière instruction +@msg the compound shape is too complex to expand la forme composée est trop complexe pour être décomposée +@msg the container of the listed objects is gone le conteneur des objets listés a disparu +@msg the copy `{user_id}` can't be read as a library la copie `{user_id}` ne peut pas être lue comme une bibliothèque +@msg the copy would leave the canvas la copie sortirait de la zone de travail +@msg the curve tolerance must be from 0.01 to 10 points la tolérance de courbe doit être comprise entre 0.01 et 10 points +@msg the custom dictionary couldn't be saved to {_1}: {e} le dictionnaire personnalisé n’a pas pu être enregistré dans {_1} : {e} +@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is le dictionnaire personnalisé {_1} ne peut pas être lu ({why}), il est donc laissé tel quel +@msg the cut cannot be made on this art: {e} la coupe ne peut pas être réalisée sur cette illustration : {e} +@msg the data isn't an EMF or WMF picture les données ne sont pas une image EMF ou WMF +@msg the data isn't an EPS or PostScript picture les données ne sont pas une image EPS ou PostScript +@msg the deferred paste is no longer available le collage différé n’est plus disponible +@msg the design is too big to draw at every data point of this graph le motif est trop grand pour être dessiné à chaque point de données de ce graphe +@msg the design is too big to draw in every column of this graph le motif est trop grand pour être dessiné dans chaque colonne de ce graphe +@msg the dialog it was opened over is no longer open la boîte de dialogue sur laquelle il a été ouvert n’est plus ouverte +@msg the dictionary stack overflowed la pile des dictionnaires a débordé +@msg the document already has an SVG filter named {name} le document a déjà un filtre SVG nommé {name} +@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it le document a un autre filtre SVG nommé {name} : utilisez svgFilter.apply, ou svgFilter.define avec `replace` pour le modifier +@msg the document has no SVG filter named {name} le document n’a aucun filtre SVG nommé {name} +@msg the document has no SVG filter named {r} le document n’a aucun filtre SVG nommé {r} +@msg the document has no color swatches le document n’a aucune nuance de couleur +@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 les nuances du document contiennent {_1} couleurs ; la réduction au document en accepte au plus 255 +@msg the editing data's hash has no place l’empreinte des données d’édition n’a pas d’emplacement +@msg the entities the document declares expand to too much text les entités déclarées par le document se développent en trop de texte +@msg the envelope's content has no bounds le contenu de l’enveloppe n’a pas de limites +@msg the envelope's mesh is malformed le filet de l’enveloppe est mal formé +@msg the file holds no `variableSets` element le fichier ne contient aucun élément `variableSets` +@msg the file is larger than {_1} MB le fichier dépasse {_1} Mo +@msg the file is too large le fichier est trop volumineux +@msg the file isn't valid SVG: {e} le fichier n’est pas un SVG valide : {e} +@msg the file names no plate le fichier ne désigne aucune plaque +@msg the file was updated or encrypted already le fichier a déjà été mis à jour ou chiffré +@msg the first character of a line has no character before it to kern: it moves only up and down (dy) le premier caractère d’une ligne n’a pas de caractère avant lui avec lequel créer un crénage : il se déplace uniquement vers le haut et le bas (dy) +@msg the font {_1} is not available la police {_1} n’est pas disponible +@msg the format has no file extension le format n’a pas d’extension de fichier +@msg the function's prototype is not an object le prototype de la fonction n’est pas un objet +@msg the graphic can't be read: {e} le graphique ne peut pas être lu : {e} +@msg the graphic is empty le graphique est vide +@msg the graphic's data is damaged les données du graphique sont endommagées +@msg the graphic's document has no artboard le document du graphique n’a pas de plan de travail +@msg the guide is hidden or locked le repère est masqué ou verrouillé +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them l’en-tête `{cell}` de `{name}` serait relu comme la variable `{other}` : renommez l’une des deux +@msg the image `{path}` isn't followed: {r} l’image `{path}` n’est pas suivie : {r} +@msg the image has no embedded pixels l’image n’a pas de pixels incorporés +@msg the image is empty l’image est vide +@msg the image is fully transparent l’image est entièrement transparente +@msg the image is not on an artboard; pass rect l’image n’est pas sur un plan de travail ; passez rect +@msg the image's pixels are missing les pixels de l’image sont manquants +@msg the image's pixels aren't in the document les pixels de l’image ne sont pas dans le document +@msg the image's pixels can't be read: {e} les pixels de l’image ne peuvent pas être lus : {e} +@msg the image's transparency doesn't fit it la transparence de l’image ne lui correspond pas +@msg the job would print more than {MAX_PAGES} pages la tâche imprimerait plus de {MAX_PAGES} pages +@msg the job would print more than {_1} pages la tâche imprimerait plus de {_1} pages +@msg the layers hold no art to mask les calques ne contiennent aucune illustration à masquer +@msg the layout moved la disposition a changé +@msg the library has a group `{name}` already la bibliothèque a déjà un groupe `{name}` +@msg the library has no group `{name}` la bibliothèque n’a pas de groupe `{name}` +@msg the library has no item `{item}` la bibliothèque n’a pas d’élément `{item}` +@msg the library has no item `{other}` la bibliothèque n’a pas d’élément `{other}` +@msg the library holds {MAX_GROUPS} groups already la bibliothèque contient déjà {MAX_GROUPS} groupes +@msg the library holds {MAX_ITEMS} items of this kind already la bibliothèque contient déjà {MAX_ITEMS} éléments de ce type +@msg the library's file is outside the read roots le fichier de la bibliothèque est en dehors des racines de lecture +@msg the method needs an XML value la méthode nécessite une valeur XML +@msg the name is empty le nom est vide +@msg the object has no fill, stroke or effect l’objet n’a ni fond, ni contour, ni effet +@msg the object has no graphic style (pass `name`) l’objet n’a pas de style graphique (passez `name`) +@msg the objects don't match the plan les objets ne correspondent pas au plan +@msg the open and permissions passwords must differ les mots de passe d’ouverture et d’autorisations doivent être différents +@msg the operand stack overflowed la pile des opérandes a débordé +@msg the page has no area la page n’a pas de surface +@msg the page's type in an unread object's box isn't in its page art le texte de la page dans le cadre d’un objet non lu n’est pas dans l’illustration de sa page +@msg the paint behind the proxy is not a gradient la peinture derrière l’échantillon n’est pas un dégradé +@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) la peinture n’est pas un dégradé de forme libre (appliquez-en un avec paint.editGradient {kind}) +@msg the parameters or body do not form one function les paramètres ou le corps ne forment pas une seule fonction +@msg the parent must be a layer or sublayer le parent doit être un calque ou un sous-calque +@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it le filtre SVG collé {name} dessine autrement que le filtre du document qui porte ce nom, et aucun nom libre ne lui convient +@msg the path can't be extended le tracé ne peut pas être prolongé +@msg the path can't be inside one of the objects to attach le tracé ne peut pas être à l’intérieur de l’un des objets à attacher +@msg the path has no segments to measure le tracé n’a aucun segment à mesurer +@msg the path is empty le tracé est vide +@msg the path is too short to shorten that much le tracé est trop court pour être raccourci autant +@msg the pen table holds more than 255 pens la table des plumes contient plus de 255 plumes +@msg the picture has no objects l’image n’a aucun objet +@msg the plates can't be kept les plaques ne peuvent pas être conservées +@msg the position table does not match the instructions la table des positions ne correspond pas aux instructions +@msg the program runs too long le programme s’exécute trop longtemps +@msg the program uses too much memory le programme utilise trop de mémoire +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes le texte de l’invite dépasse {MAX_PROMPT_BYTES} octets +@msg the radix must be between 2 and 36 la base doit être comprise entre 2 et 36 +@msg the rectangle must have a width and a height le rectangle doit avoir une largeur et une hauteur +@msg the rectangle needs a size and must lie on the canvas le rectangle nécessite une taille et doit se trouver dans la zone de travail +@msg the right side of 'in' is not an object le côté droit de 'in' n’est pas un objet +@msg the right side of 'instanceof' is not a function le côté droit de 'instanceof' n’est pas une fonction +@msg the selected art has no width l’illustration sélectionnée n’a pas de largeur +@msg the selected art is nested more than {MAX_DEPTH} levels deep l’illustration sélectionnée est imbriquée sur plus de {MAX_DEPTH} niveaux +@msg the selected art is too large to measure l’illustration sélectionnée est trop grande pour être mesurée +@msg the selection draws nothing la sélection ne dessine rien +@msg the selection has no area la sélection n’a pas de surface +@msg the selection has no art to export la sélection ne contient aucune illustration à exporter +@msg the selection has no bounds la sélection n’a pas de limites +@msg the selection has no graphic style la sélection n’a pas de style graphique +@msg the selection has no outlines to weed around la sélection n’a pas de contours autour desquels écheniller +@msg the slice would leave the canvas la tranche sortirait de la zone de travail +@msg the spot colour couldn't be added le ton direct n’a pas pu être ajouté +@msg the subpath has no ends to extend (it is closed or a single point) le sous-tracé n’a pas d’extrémités à prolonger (il est fermé ou réduit à un point) +@msg the symbols are nested too deeply to expand their instances les symboles sont imbriqués trop profondément pour décomposer leurs occurrences +@msg the text file is too large to place as type (at most {_1} MB) le fichier texte est trop volumineux pour être importé comme texte (au plus {_1} Mo) +@msg the text is empty le texte est vide +@msg the text is over {_1} MB le texte dépasse {_1} Mo +@msg the top object must be a path l’objet du dessus doit être un tracé +@msg the trace was cancelled la vectorisation a été annulée +@msg the trailer has no size le trailer n’a pas de taille +@msg the type object is flattened to nothing l’objet texte est aplati à néant +@msg the value bind was called on is not a function la valeur sur laquelle bind a été appelé n’est pas une fonction +@msg the value is not a constructor la valeur n’est pas un constructeur +@msg the value is not a function la valeur n’est pas une fonction +@msg the value is not a valid XML name la valeur n’est pas un nom XML valide +@msg the value is not an XML attribute name la valeur n’est pas un nom d’attribut XML +@msg the value is not an XML name la valeur n’est pas un nom XML +@msg the value of axis `{tag}` must be a number or null la valeur de l’axe `{tag}` doit être un nombre ou null +@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets la bibliothèque de variables déclare plus de {MAX_LIBRARY_DATASETS} jeux de données +@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities la bibliothèque de variables déclare plus de {MAX_LIBRARY_ENTITIES} entités +@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables la bibliothèque de variables déclare plus de {MAX_LIBRARY_VARIABLES} variables +@msg the variable library declares no variables la bibliothèque de variables ne déclare aucune variable +@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes la bibliothèque de variables dépasse {MAX_LIBRARY_BYTES} octets +@msg the variable library is not UTF-8 la bibliothèque de variables n’est pas en UTF-8 +@msg the variable library is not well-formed XML: {e} la bibliothèque de variables n’est pas du XML bien formé : {e} +@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep la bibliothèque de variables imbrique des éléments sur plus de {MAX_LIBRARY_NESTING} niveaux +@msg the variable library's document type declaration holds something other than entities of plain text la déclaration de type de document de la bibliothèque de variables contient autre chose que des entités de texte brut +@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes les entités de la bibliothèque de variables se développent en plus de {MAX_LIBRARY_BYTES} octets +@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines les lignes d’échenillage croisent les contours de l’illustration trop de fois pour être mesurées ; utilisez moins de lignes +@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap les lignes d’échenillage passent trop près de trop de segments de contour pour être mesurées ; utilisez moins de lignes ou un écart plus petit +@msg the weeding lines would be more than {max_pieces} pieces les lignes d’échenillage feraient plus de {max_pieces} morceaux +@msg the window has no dock la fenêtre n’a pas de zone d’ancrage +@msg the written PDF has no catalog le PDF écrit n’a pas de catalogue +@msg the written PDF has no cross-reference table le PDF écrit n’a pas de table de références croisées +@msg the written PDF's metadata can't be edited les métadonnées du PDF écrit ne peuvent pas être modifiées +@msg the {_1} plate lies outside the file la plaque {_1} se trouve en dehors du fichier +@msg there are no colour swatches to draw il n’y a aucune nuance de couleur à dessiner +@msg there is already a design named `{name}` il existe déjà un motif nommé `{name}` +@msg there is already a design named `{to}` il existe déjà un motif nommé `{to}` +@msg there is already a toolbar named `{name}` il existe déjà une barre d’outils nommée `{name}` +@msg there is already a toolbar named `{to}` il existe déjà une barre d’outils nommée `{to}` +@msg there is no library: create one first il n’y a aucune bibliothèque : créez-en d’abord une +@msg these paths are too degenerate for Shaper ces tracés sont trop dégénérés pour Shaper +@msg this is a binary DXF file, which can't be read: save it from the CAD app as an ASCII DXF and open that ceci est un fichier DXF binaire, qui ne peut pas être lu : enregistrez-le depuis l’application de CAO en DXF ASCII et ouvrez ce dernier +@msg this warp's points can't be edited: use Reset with Mesh first les points de cette déformation ne peuvent pas être modifiés : utilisez d’abord Réinitialiser avec un filet +@msg thumbnail: no page {v} (the {what} has {count}) vignette : aucune page {v} (le {what} en compte {count}) +@msg tileRange of {what}: {e} tileRange de {what} : {e} +@msg title can't be empty title ne peut pas être vide +@msg to (the new name) is required to (le nouveau nom) est obligatoire +@msg to must be fill or stroke to doit valoir fill ou stroke +@msg to must be smooth or corner to doit valoir smooth ou corner +@msg toExponential() digits must be between 0 and 20 les chiffres de toExponential() doivent être compris entre 0 et 20 +@msg toFixed() digits must be between 0 and 20 les chiffres de toFixed() doivent être compris entre 0 et 20 +@msg toISOString is not a function toISOString n’est pas une fonction +@msg toPrecision() argument must be between 1 and 21 l’argument de toPrecision() doit être compris entre 1 et 21 +@msg toSource: a getter cannot be written as source toSource : un getter ne peut pas être écrit sous forme de source +@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource : un tableau doté de propriétés autres que ses éléments, d’une propriété non énumérable ou d’un autre prototype ne peut pas être écrit sous forme de source +@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource : un objet doté d’une propriété non énumérable, ou dont le prototype n’est pas Object.prototype, ne peut pas être écrit sous forme de source +@msg toSource: the source is too long toSource : la source est trop longue +@msg toSource: the value contains itself toSource : la valeur se contient elle-même +@msg toSource: the value has too many entries toSource : la valeur a trop d’entrées +@msg toSource: the value is nested too deep toSource : la valeur est imbriquée trop profondément +@msg toString is not a function toString n’est pas une fonction +@msg too large for a zip archive: package to a folder trop volumineux pour une archive zip : assemblez dans un dossier +@msg too many Shaper edits trop de modifications Shaper +@msg too many `points` (at most 100000) trop de `points` (au plus 100000) +@msg too many arguments trop d’arguments +@msg too many characters: at most {MAX_CHARS} trop de caractères : au plus {MAX_CHARS} +@msg too many dots for this size: lower the frequency trop de points pour cette taille : réduisez la linéature +@msg too many filters over one file trop de filtres sur un même fichier +@msg too many overlapping paths for Shaper trop de tracés qui se chevauchent pour Shaper +@msg too many quantifiers trop de quantificateurs +@msg too many saved clips trop de détourages enregistrés +@msg too many saved graphics states trop d’états graphiques enregistrés +@msg toolbar (a toolbar's name) must be a string toolbar (le nom d’une barre d’outils) doit être une chaîne +@msg toolbar name can't be empty le nom de la barre d’outils ne peut pas être vide +@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v} : "flatten" (par défaut) ou "omit" +@msg two saved selections would be named `{_1}` deux sélections enregistrées seraient nommées `{_1}` +@msg type dimension cannot be represented with this text transform la dimension du texte ne peut pas être représentée avec cette transformation de texte +@msg type dimension must be finite la dimension du texte doit être finie +@msg unexpected resources ressources inattendues +@msg unknown CMYK profile `{name}` profil CMJN inconnu `{name}` +@msg unknown File Info field `{k}` champ Informations sur le fichier inconnu `{k}` +@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; prefix - to turn off; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take an alternate's number as `tag=N` with N from 0 to {_1}, as `salt=2`) fonctionnalité OpenType inconnue `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt ; préfixe - pour désactiver ; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom et aalt acceptent un numéro de variante sous la forme `tag=N` avec N de 0 à {_1}, comme `salt=2`) +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) fonctionnalité OpenType inconnue `{t}` dans featureChanges (les balises qu’accepte `features`) +@msg unknown PDF preset `{name}` (presets: {_1}) paramètre prédéfini PDF inconnu `{name}` (paramètres prédéfinis : {_1}) +@msg unknown Warp style style de déformation inconnu +@msg unknown alignToPath `{a}` alignToPath inconnu `{a}` +@msg unknown arrowhead {v} pointe de flèche inconnue {v} +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) clé artboardLayout inconnue `{k}` (layout, columns, spacing, rightToLeft) +@msg unknown attribute `{attribute}` attribut inconnu `{attribute}` +@msg unknown attribute `{k}` attribut inconnu `{k}` +@msg unknown axis `{other}` (vertical|horizontal|free) axe inconnu `{other}` (vertical|horizontal|free) +@msg unknown blend mode `{b}` mode de fusion inconnu `{b}` +@msg unknown blend mode {b} mode de fusion inconnu {b} +@msg unknown brush type `{o}` type de forme inconnu `{o}` +@msg unknown category `{c}` catégorie inconnue `{c}` +@msg unknown category `{c}` ({_1}) catégorie inconnue `{c}` ({_1}) +@msg unknown character `{c}` caractère inconnu `{c}` +@msg unknown character style number field champ numérique de style de caractère inconnu +@msg unknown dither `{d}` (diffusion, pattern, noise or none) tramage inconnu `{d}` (diffusion, pattern, noise ou none) +@msg unknown effect `{e}` effet inconnu `{e}` +@msg unknown end {e} extrémité inconnue {e} +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) flattenerPreset inconnu `{name}` (High Resolution, Medium Resolution, Low Resolution ou un paramètre enregistré : voir flattener.presets.list) +@msg unknown format `{f}` (vcswatches, gpl, ase or css) format inconnu `{f}` (vcswatches, gpl, ase ou css) +@msg unknown from {f} from inconnu {f} +@msg unknown graph type `{t}` type de graphe inconnu `{t}` +@msg unknown grid colours `{name}` (one of {_1}) couleurs de grille inconnues `{name}` (l’une de ces valeurs : {_1}) +@msg unknown gridType `{t}` gridType inconnu `{t}` +@msg unknown highlight `{v}` mise en surbrillance inconnue `{v}` +@msg unknown intent `{i}` mode de rendu inconnu `{i}` +@msg unknown issue kind {id} type de problème inconnu {id} +@msg unknown issue kind {v} type de problème inconnu {v} +@msg unknown knockout state {k} état de masquage inconnu {k} +@msg unknown language `{language}` (one of {_1}) langue inconnue `{language}` (l’une de ces valeurs : {_1}) +@msg unknown language `{l}` (one of {_1}) langue inconnue `{l}` (l’une de ces valeurs : {_1}) +@msg unknown method `{m}` ({_1}) méthode inconnue `{m}` ({_1}) +@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) mode inconnu `{other}` (gray, rgb, hsb, lab, cmyk ou web) +@msg unknown mode `{other}` (mono|cmyk) mode inconnu `{other}` (mono|cmyk) +@msg unknown mode `{other}` (rgb|cmyk|gray|global) mode inconnu `{other}` (rgb|cmyk|gray|global) +@msg unknown mode `{other}` (set, add, subtract) mode inconnu `{other}` (set, add, subtract) +@msg unknown mode `{o}` mode inconnu `{o}` +@msg unknown mode {m} mode inconnu {m} +@msg unknown model `{other}` modèle inconnu `{other}` +@msg unknown model `{other}` (keep, new or document) modèle inconnu `{other}` (keep, new ou document) +@msg unknown op {o} op inconnu {o} +@msg unknown order `{o}`: toggle, front, forward, backward or back ordre inconnu `{o}` : toggle, front, forward, backward ou back +@msg unknown overprints `{v}` (preserve, simulate or discard) surimpressions inconnues `{v}` (preserve, simulate ou discard) +@msg unknown panel panneau inconnu +@msg unknown panel `{panel}` panneau inconnu `{panel}` +@msg unknown parameter `{key}` paramètre inconnu `{key}` +@msg unknown parameter `{within}settings.{format}`; {why} paramètre inconnu `{within}settings.{format}` ; {why} +@msg unknown parameter `{within}{key}`; {why} paramètre inconnu `{within}{key}` ; {why} +@msg unknown pieLegend `{v}` (none, standard or wedges) pieLegend inconnu `{v}` (none, standard ou wedges) +@msg unknown piePosition `{v}` (even, ratio or stacked) piePosition inconnu `{v}` (even, ratio ou stacked) +@msg unknown pieSort `{v}` (none, all or first) pieSort inconnu `{v}` (none, all ou first) +@msg unknown plate `{n}` plaque inconnue `{n}` +@msg unknown preference `{k}` préférence inconnue `{k}` +@msg unknown profile {id} (see stroke.widthProfile.list) profil inconnu {id} (voir stroke.widthProfile.list) +@msg unknown proof target `{t}` cible d’épreuve inconnue `{t}` +@msg unknown quotes key `{k}` (double, single) clé de guillemets inconnue `{k}` (double, single) +@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) réduction inconnue `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform ou document) +@msg unknown resolution `{v}` résolution inconnue `{v}` +@msg unknown rule `{rule}` ({_1}) règle inconnue `{rule}` ({_1}) +@msg unknown rule `{r}` (see color.harmony) règle inconnue `{r}` (voir color.harmony) +@msg unknown setting `{k}` réglage inconnu `{k}` +@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) forme inconnue `{v}` (circle|ellipse|square|diamond|line) +@msg unknown spacing `{o}` (smooth|steps|distance) espacement inconnu `{o}` (smooth|steps|distance) +@msg unknown tileType `{t}` ({_1}) tileType inconnu `{t}` ({_1}) +@msg unknown units unités inconnues +@msg unknown units `{u}` unités inconnues `{u}` +@msg unknown value axis `{v}` (left, right or both) axe des valeurs inconnu `{v}` (left, right ou both) +@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) variation inconnue `{v}` (tintsShades|warmCool|vividMuted) +@msg unknown warp style `{s}` style de déformation inconnu `{s}` +@msg unknown {key} `{v}` (none, short or full) {key} inconnu `{v}` (none, short ou full) +@msg unknown {key} profile `{n}` profil {key} inconnu `{n}` +@msg unknown {k} key `{key}` (size, position) clé {k} inconnue `{key}` (size, position) +@msg unmatched ')' ')' sans correspondance +@msg unreadable object objet illisible +@msg unsafe destination file name nom de fichier de destination non sûr +@msg unterminated character class classe de caractères non terminée +@msg unterminated group groupe non terminé +@msg use paths or compound paths utilisez des tracés ou des tracés transparents +@msg useArtboards must be true or false, not {v} useArtboards doit valoir true ou false, pas {v} +@msg value for `{var}` must match its variable kind la valeur de `{var}` doit correspondre au type de sa variable +@msg variable `{_1}` is already defined la variable `{_1}` est déjà définie +@msg variable `{name}` has no `trait` la variable `{name}` n’a pas de `trait` +@msg variable `{name}` is already defined la variable `{name}` est déjà définie +@msg variable `{name}` is declared twice la variable `{name}` est déclarée deux fois +@msg variable `{to}` already exists la variable `{to}` existe déjà +@msg view is required view est obligatoire +@msg view must be one of {_1} view doit valoir l’une de ces valeurs : {_1} +@msg warpStyle can only edit an applied Warp effect warpStyle ne peut modifier qu’un effet Déformation appliqué +@msg width and height must be more than 0 and at most {limit} width et height doivent être supérieurs à 0 et d’au plus {limit} +@msg width factors must be finite les facteurs de largeur doivent être finis +@msg width must be positive width doit être positif +@msg width out of range largeur hors limites +@msg width/height must be > 0 width/height doivent être > 0 +@msg wordGap and lineGap need area type in the selection wordGap et lineGap nécessitent du texte captif dans la sélection +@msg x and y must be finite numbers x et y doivent être des nombres finis +@msg x and y must be given together x et y doivent être indiqués ensemble +@msg xpath needs its variables as an object xpath nécessite ses variables sous forme d’objet +@msg xpath needs the expression as a string xpath nécessite l’expression sous forme de chaîne +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone nécessite un ancrage : ancrez à côté d’un panneau avec anchor, ou dans une nouvelle colonne d’ancrage avec side (Start ou End) +@msg zoom must be positive zoom doit être positif +@msg {C}: no effect at index {list} {C} : aucun effet à l’indice {list} +@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET} est protégé et ne peut pas être retiré +@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET} est protégé : enregistrez les réglages sous un autre nom +@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED}, et ses réglages et son cache ne peuvent pas être replacés de {_1} vers {_2} : {e} +@msg {LABEL} produced an empty result {LABEL} a produit un résultat vide +@msg {_1} MB of document, at most {_2} MB {_1} Mo de document, au plus {_2} Mo +@msg {_1} MB of images, at most {_2} MB {_1} Mo d’images, au plus {_2} Mo +@msg {_1} bytes of RGBA data for a {width} × {height} image {_1} octets de données RVBA pour une image de {width} × {height} +@msg {_1} can't be read, or changed since it was read: relink or update it first {_1} ne peut pas être lu, ou a changé depuis sa lecture : liez-le à nouveau ou mettez-le d’abord à jour +@msg {_1} can't go into {into} {_1} ne peut pas aller dans {into} +@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1} ne peut pas limiter l’impression à la basse résolution : choisissez PDF 1.4 ou ultérieur, ou autorisez l’impression haute résolution +@msg {_1} files can't be opened yet les fichiers {_1} ne peuvent pas encore être ouverts +@msg {_1} files can't be password-protected: remove the passwords or choose no standard les fichiers {_1} ne peuvent pas être protégés par mot de passe : supprimez les mots de passe ou ne choisissez aucune norme +@msg {_1} files can't be {_2} les fichiers {_1} ne peuvent pas être {_2} +@msg {_1} files can't carry editing data: turn off preserveEditing les fichiers {_1} ne peuvent pas contenir de données d’édition : désactivez preserveEditing +@msg {_1} files can't have PDF layers: turn off createLayers les fichiers {_1} ne peuvent pas avoir de calques PDF : désactivez createLayers +@msg {_1} files need a creation date, and this platform has no clock to give one les fichiers {_1} nécessitent une date de création, et cette plateforme n’a pas d’horloge pour en fournir une +@msg {_1} is not a folder: {e} {_1} n’est pas un dossier : {e} +@msg {_1} isn't a path {_1} n’est pas un tracé +@msg {_1} options: {e} Options {_1} : {e} +@msg {_1} paints nothing to rasterize {_1} ne peint rien à pixelliser +@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) les mots de passe {_1} peuvent compter jusqu’à {MAX_LEGACY_PASSWORD} caractères ASCII (PDF 1.7 et ultérieur acceptent tous les caractères) +@msg {_1} with linked images or an image map writes several files: use document.export {_1} avec des images liées ou une image réactive écrit plusieurs fichiers : utilisez document.export +@msg {_1} writes one file per artboard here: use document.export, or name one artboard {_1} écrit ici un fichier par plan de travail : utilisez document.export, ou indiquez un seul plan de travail +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} pixels est trop grand pour {_3} (au plus {max} pixels de côté) : réduisez l’échelle +@msg {_1}: belongs to another account {_1} : appartient à un autre compte +@msg {_1}: not a folder of its own {_1} : ce n’est pas un dossier distinct +@msg {_1}: not a plain file {_1} : ce n’est pas un fichier ordinaire +@msg {_1}: {_2} and {_3} are in the way {_1} : {_2} et {_3} gênent +@msg {code}: {0} {code} : {0} +@msg {dest}: {e} {dest} : {e} +@msg {dir}: {e} {dir} : {e} +@msg {doc} has no folder {doc} n’a pas de dossier +@msg {file} isn't linked {file} n’est pas lié +@msg {file} names more than one linked JavaScript file {file} désigne plusieurs fichiers JavaScript liés +@msg {folder} is not a package folder {folder} n’est pas un dossier d’assemblage +@msg {folder}: more than {max} files and folders {folder} : plus de {max} fichiers et dossiers +@msg {folder}: {e} {folder} : {e} +@msg {g} is not a Live Paint group {g} n’est pas un groupe de peinture dynamique +@msg {id} is not a slice {id} n’est pas une tranche +@msg {key} must be 0–{MAX_ADD_AROUND} pt {key} doit être compris entre 0 et {MAX_ADD_AROUND} pt +@msg {key} must be [x,y] {key} doit être [x,y] +@msg {key} must be a percentage {min}..100 {key} doit être un pourcentage de {min} à 100 +@msg {key} must be a positive number, not {n} {key} doit être un nombre positif, pas {n} +@msg {key} must be a positive number, not {v} {key} doit être un nombre positif, pas {v} +@msg {key} must be one of {_1} {key} doit valoir l’une de ces valeurs : {_1} +@msg {key} must be {_1}, got {s} {key} doit valoir {_1}, reçu : {s} +@msg {key} must name a floating panel {key} doit désigner un panneau flottant +@msg {key} must name a panel in another group {key} doit désigner un panneau d’un autre groupe +@msg {key} must name a panel that shows {key} doit désigner un panneau affiché +@msg {key} must name a registered panel {key} doit désigner un panneau enregistré +@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) les fichiers {kind} n’ont pas de transparence, et le fichier aurait {what} : aplatissez-la d’abord (object.flattenTransparency) +@msg {kind} images can't be opened (see document.formats) les images {kind} ne peuvent pas être ouvertes (voir document.formats) +@msg {k} `{v}` is not one of the choices {k} `{v}` ne fait pas partie des choix +@msg {k} has control characters{_1} {k} contient des caractères de contrôle{_1} +@msg {k} is too long (at most {MAX_TEXT} characters) {k} est trop long (au plus {MAX_TEXT} caractères) +@msg {k} must be > 0 {k} doit être > 0 +@msg {k} must be a number (pt) or a length such as "210 mm" {k} doit être un nombre (pt) ou une longueur comme "210 mm" +@msg {k} must be a number from {min} to {max} {k} doit être un nombre compris entre {min} et {max} +@msg {k} must be a number or "auto" {k} doit être un nombre ou "auto" +@msg {k} must be a string {k} doit être une chaîne +@msg {k} must be text {k} doit être du texte +@msg {k} must be true or false {k} doit valoir true ou false +@msg {k} must be {{size?, position?}} {k} doit être {{size?, position?}} +@msg {k}: a custom arrowhead's path needs an area to fill and at most {most} anchors and {most} subpaths, each anchor within {far} head units of its tip {k} : le tracé d’une pointe de flèche personnalisée nécessite une zone à remplir et au plus {most} points d’ancrage et {most} sous-tracés, chaque point d’ancrage à moins de {far} unités de pointe de son extrémité +@msg {k}: bad custom arrowhead path: {e} {k} : tracé de pointe de flèche personnalisée incorrect : {e} +@msg {k}: bad custom arrowhead: {e} {k} : pointe de flèche personnalisée incorrecte : {e} +@msg {k}: can't read `{s}` as a length {k} : impossible de lire `{s}` comme une longueur +@msg {k}: custom must be a name {k} : custom doit être un nom +@msg {label} must be a number at most {max} {label} doit être un nombre d’au plus {max} +@msg {label} must be a number from -1296 to 1296 {label} doit être un nombre compris entre -1296 et 1296 +@msg {label} must be a number from 1 to 10000 {label} doit être un nombre compris entre 1 et 10000 +@msg {name} `{s}`: {ids} {name} `{s}` : {ids} +@msg {name} is built in and can't be deleted {name} est intégré et ne peut pas être supprimé +@msg {name} must be {lo}–{hi}{unit} (got {v}) {name} doit être compris entre {lo} et {hi}{unit} (reçu : {v}) +@msg {name}: CRC or size mismatch {name} : CRC ou taille incohérents +@msg {name}: only this Affinity file's embedded preview could be read; use File › Open to see it with its warning, or export SVG or PDF from Affinity before placing {name} : seul l’aperçu incorporé de ce fichier Affinity a pu être lu ; utilisez Fichier › Ouvrir pour l’afficher avec son avertissement, ou exportez en SVG ou PDF depuis Affinity avant de l’importer +@msg {name}: {e} {name} : {e} +@msg {native}; its embedded preview can't be read either ({e}) {native} ; son aperçu incorporé ne peut pas non plus être lu ({e}) +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path} : copy n’accepte un fichier DCS que si ses plaques sont à l’intérieur ; liez-le à nouveau là où se trouvent ses fichiers de plaques +@msg {path}: larger than {_1} MB {path} : dépasse {_1} Mo +@msg {shown} is not an XPath 1.0 function {shown} n’est pas une fonction XPath 1.0 +@msg {std} files are CMYK, and the file would have colours in {_1} les fichiers {std} sont en CMJN, et le fichier aurait des couleurs en {_1} +@msg {std} files embed their fonts, and the file would have one that isn't les fichiers {std} incorporent leurs polices, et le fichier en aurait une qui ne l’est pas +@msg {target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway {target} est le fichier à partir duquel ce document a été lu, et sa lecture a omis des éléments ({what}) : l’écraser les ferait perdre définitivement. Écrivez plutôt un autre fichier, ou passez acknowledgeLoss: true pour le remplacer quand même +@msg {ty} brushes need selected art (or params.{k}) les formes {ty} nécessitent une illustration sélectionnée (ou params.{k}) +@msg {v} is not an asset id {v} n’est pas un identifiant de ressource +@msg {v} names no layer or sublayer {v} ne désigne aucun calque ni sous-calque +@msg {what} can't be intertwined {what} ne peut pas être entrelacé +@msg {what} doesn't fit the paper with its bleed and marks {what} ne tient pas sur le papier avec son fond perdu et ses repères +@msg {what} is not a function {what} n’est pas une fonction +@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what} imprimerait plus de {MAX_COPIES_PER_SHEET} exemplaires sur une feuille : agrandissez-le +@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} s’imprimerait sur plus de {MAX_TILES} mosaïques : réduisez-le +@msg {what} {i} out of range {what} {i} hors limites +@msg Alignment must be one of the choices Alignement doit être l’un des choix proposés +@msg Line Gap must be a number Écart entre les lignes doit être un nombre +@msg `at` must be a point [x, y] `at` doit être un point [x, y] +@msg `points` must hold from 2 to 100000 points `points` doit contenir de 2 à 100000 points +@msg `rect` must be [x, y, width, height] `rect` doit être [x, y, width, height] +@msg `tolerance` must be from 0 to 100 pt `tolerance` doit être compris entre 0 et 100 pt +@msg `warichuAlign` must be one of {_1} `warichuAlign` doit valoir l’une de ces valeurs : {_1} +@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines` doit être un nombre entier compris entre {_1} et {_2} +@msg bad point {v}: expected [x, y] point incorrect {v} : [x, y] attendu +@msg give exactly one of `at`, `rect` and `points` indiquez exactement un paramètre parmi `at`, `rect` et `points` +@msg the line is too long, or the paths it passes too many, to check la ligne est trop longue, ou elle traverse trop de tracés, pour être vérifiée +@msg the paths are too many or too tangled to find where they cross les tracés sont trop nombreux ou trop enchevêtrés pour trouver où ils se croisent +@msg the rectangle or line reaches too many paths to check le rectangle ou la ligne atteint trop de tracés pour être vérifié +@msg {name} must be a number {name} doit être un nombre Banner Hems and Grommets… Ourlets et œillets de banderole… Banner Hems and Grommets Ourlets et œillets de banderole Hem Ourlet diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index dff4c3503c..a31993c84a 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -5942,6 +5942,7 @@ antiAlias Sharp शार्प @msg `{key}` must be a number `{key}` एक संख्या होना चाहिए @msg `{key}` must be a number {_1}..{_2} `{key}` {_1}..{_2} की एक संख्या होना चाहिए @msg `{key}` must be a number not below 0 `{key}` 0 से कम नहीं होने वाली संख्या होना चाहिए +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 0 से कम नहीं होने वाली चार संख्याएँ होनी चाहिए: [tl, tr, br, bl] @msg `{key}` must be a number, not {v} `{key}` एक संख्या होना चाहिए, {v} नहीं @msg `{key}` must be a profile name or null `{key}` को प्रोफ़ाइल नाम या null होना चाहिए @msg `{key}` must be a string `{key}` एक स्ट्रिंग होना चाहिए diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 8549bd5d63..7b5d4eb4ae 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4914,6 +4914,7 @@ imageTrace Outline Körvonal @msg `select` must be true or false `select`: true vagy false kell @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` és `maxAnchors`: egész szám kell @msg `{key}` must be a number not below 0 `{key}`: legalább 0 értékű szám kell +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}`: négy legalább 0 értékű szám kell: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`: [x, y, width, height] kell, négy véges szám, a szélesség és a magasság nem negatív @msg `{min_key}` is greater than `{max_key}` `{min_key}` nagyobb, mint `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` adj meg legalább egyet ezek közül: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2381051c9b..7d74485aac 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4541,6 +4541,7 @@ axis Both Entrambi @msg `select` must be true or false `select` deve essere true o false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devono essere numeri interi @msg `{key}` must be a number not below 0 `{key}` deve essere un numero non inferiore a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` deve essere quattro numeri non inferiori a 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve essere [x, y, width, height]: quattro numeri finiti, con larghezza e altezza non negative @msg `{min_key}` is greater than `{max_key}` `{min_key}` è maggiore di `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica almeno uno tra `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 1458bf2aa1..31526809eb 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5011,6 +5011,7 @@ swatch Bright Violet ビビッドバイオレット @msg `select` must be true or false `select` には true または false を指定してください @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` と `maxAnchors` は整数にしてください @msg `{key}` must be a number not below 0 `{key}` には 0 以上の数値を指定してください +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` には 0 以上の数値を 4 つ指定してください: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` は [x, y, width, height] の形式で指定してください: 有限の数値 4 つで、幅と高さは負の値にできません @msg `{min_key}` is greater than `{max_key}` `{min_key}` が `{max_key}` より大きくなっています @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 次のうち少なくとも 1 つを指定してください: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 9105fe50e7..a6429f06ee 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4186,6 +4186,7 @@ font-technology Variable ცვლადი @msg `select` must be true or false `select` უნდა იყოს true ან false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` და `maxAnchors` უნდა იყოს მთელი რიცხვები @msg `{key}` must be a number not below 0 `{key}` უნდა იყოს რიცხვი, არანაკლებ 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` უნდა იყოს ოთხი რიცხვი, თითოეული არანაკლებ 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` უნდა იყოს [x, y, width, height]: ოთხი სასრული რიცხვი, სიგანე და სიმაღლე არ უნდა იყოს უარყოფითი @msg `{min_key}` is greater than `{max_key}` `{min_key}` მეტია, ვიდრე `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` მიუთითეთ ერთი მაინც: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index fd75f983ce..7141602a3d 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4938,6 +4938,7 @@ theme Appearance 외관 @msg `select` must be true or false `select`은(는) true 또는 false여야 합니다 @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors`와 `maxAnchors`는 정수여야 합니다 @msg `{key}` must be a number not below 0 `{key}`은(는) 0 이상의 숫자여야 합니다 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}`은(는) 0 이상의 숫자 네 개여야 합니다: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`은(는) [x, y, width, height]여야 합니다: 유한한 숫자 네 개이며 너비와 높이는 음수가 아니어야 합니다 @msg `{min_key}` is greater than `{max_key}` `{min_key}`이(가) `{max_key}`보다 큽니다 @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 다음 중 하나 이상을 지정하세요: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -7086,3 +7087,1817 @@ breakText Glyphs 글리프 Grommets 아일렛 Grommet Inset 가장자리에서 아일렛까지 거리 Diameter 지름 +# Status and error messages of commands, scripts, file import and export, and plug-ins. +@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions 엄격 모드 함수에서는 'caller', 'callee', 'arguments'를 사용할 수 없습니다 +@msg A profile supports one Revolve effect. Edit the existing effect's options instead. 프로파일 하나에는 회전체 효과를 하나만 쓸 수 있습니다. 기존 효과의 옵션을 편집하세요. +@msg Add Points at Crossings: a path went missing 교차점에 점 추가: 패스가 사라졌습니다 +@msg Alignment must be one of the choices 정렬은 선택 항목 중 하나여야 합니다 +@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean이 아닌 값에서 Boolean.prototype.{name}을(를) 호출했습니다 +@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB 샘플을 CMYK로 읽을 수 없는 CMYK 이미지는 RGB로 가져왔습니다 +@msg CSS options: {e} CSS 옵션: {e} +@msg CSV has a header but no data rows CSV에 머리글은 있지만 데이터 행이 없습니다 +@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark CSV에 NUL 바이트가 있습니다: UTF-8로 저장하거나 바이트 순서 표시가 있는 UTF-16으로 저장하세요 +@msg CSV is empty CSV가 비어 있습니다 +@msg CSV is larger than {MAX_CSV_BYTES} bytes CSV가 {MAX_CSV_BYTES}바이트보다 큽니다 +@msg CSV is not UTF-16 CSV가 UTF-16이 아닙니다 +@msg CSV is not UTF-8 CSV가 UTF-8이 아닙니다 +@msg CSV would be larger than {MAX_CSV_BYTES} bytes CSV가 {MAX_CSV_BYTES}바이트보다 커집니다 +@msg Cannot write to the location of “{name}”; choose where to save it “{name}”의 위치에 쓸 수 없습니다. 저장할 위치를 선택하세요 +@msg Choose original artwork, not a dimension's generated paths 치수가 만든 패스가 아닌 원본 아트웍을 선택하세요 +@msg Contour: select paths or shapes 윤곽: 패스나 모양을 선택하세요 +@msg Contour: the Offset Path effect is not available 윤곽: 패스 간격 띄우기 효과를 사용할 수 없습니다 +@msg Create needs fills, strokes or both 만들려면 칠이나 선, 또는 둘 다 필요합니다 +@msg DCS files can only be placed linked: use File › Place with Link on DCS 파일은 연결된 상태로만 가져올 수 있습니다: 링크를 켜고 파일 › 가져오기를 사용하세요 +@msg DXF options: {e} DXF 옵션: {e} +@msg DXF scale must be a positive number up to {_1}, not {scale} DXF 비율은 {_1} 이하의 양수여야 합니다: {scale} +@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}`: {allowed}이어야 합니다 +@msg Date.prototype method called on a value that is not a Date object Date 개체가 아닌 값에서 Date.prototype 메서드를 호출했습니다 +@msg Dimension is locked or hidden 치수가 잠겨 있거나 숨겨져 있습니다 +@msg Dimension mode does not match its source 치수 모드가 원본과 맞지 않습니다 +@msg Draw Inside needs exactly one selected path, compound path or text 안쪽에 그리기에는 선택한 패스, 컴파운드 패스 또는 텍스트가 정확히 하나 필요합니다 +@msg EPS Save writes all artwork: use document.export for selected-only EPS EPS 저장은 모든 아트웍을 기록합니다: 선택 항목만 EPS로 기록하려면 document.export를 사용하세요 +@msg EPS flattener: {e} EPS 병합: {e} +@msg EPS options: {e} EPS 옵션: {e} +@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}`: {allowed}이어야 합니다 +@msg Error.prototype.toString was called on a value that is not an object 개체가 아닌 값에서 Error.prototype.toString을 호출했습니다 +@msg Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft 화면용 내보내기는 png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai 또는 vectorcraft로 기록합니다 +@msg Flare click coordinates must be finite numbers 플레어 클릭 좌표는 유한한 숫자여야 합니다 +@msg Function.prototype.toString was called on a value that is not a function 함수가 아닌 값에서 Function.prototype.toString을 호출했습니다 +@msg HTML Text applies to object slices of type only HTML 텍스트는 문자의 개체 슬라이스에만 적용됩니다 +@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) 개체 식별 기준이 XML ID입니다: 이름은 올바른 XML ID여야 합니다(글자, 밑줄 또는 콜론으로 시작하고 공백이 없어야 함) +@msg Image Trace failed: internal error: its thread stopped (please report this bug) 이미지 추적 실패: 내부 오류: 스레드가 멈췄습니다(이 버그를 보고해 주세요) +@msg Image Trace failed: {e} 이미지 추적 실패: {e} +@msg Image Trace finished, but its document was closed 이미지 추적이 끝났지만 문서가 닫혔습니다 +@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again 이미지 추적이 끝났지만 그 사이에 이미지나 설정이 바뀌었습니다: 다시 추적하세요 +@msg Import SVG Filter reads SVG files, not SVGZ SVG 필터 가져오기는 SVGZ가 아닌 SVG 파일을 읽습니다 +@msg Invalid dimension offset 잘못된 치수 오프셋 +@msg Invalid point 잘못된 점 +@msg JSON.parse: {why} (at offset {at}) JSON.parse: {why}(오프셋 {at}) +@msg JSON.stringify cannot write a structure that contains itself JSON.stringify는 자신을 포함하는 구조를 기록할 수 없습니다 +@msg Join: a subpath went missing 연결: 하위 패스가 사라졌습니다 +@msg Line Gap must be a number 행 간격은 숫자여야 합니다 +@msg Lock Workspace is not available in VectorCraft VectorCraft에서는 작업 영역 잠그기를 사용할 수 없습니다 +@msg Move Panel to New Window is not available: floating panels stay in the main window 패널을 새 창으로 이동은 사용할 수 없습니다: 떠 있는 패널은 기본 창 안에 있습니다 +@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString에는 Namespace가 필요합니다 +@msg New Document profiles need the preferences folder, which this run doesn't have 새 문서 프로파일에는 환경 설정 폴더가 필요한데, 이번 실행에는 그 폴더가 없습니다 +@msg Number.prototype.{name} was called on a value that is not a number 숫자가 아닌 값에서 Number.prototype.{name}을(를) 호출했습니다 +@msg Object.create's prototype must be an object or null Object.create의 프로토타입은 개체 또는 null이어야 합니다 +@msg Object.{function} was given a value that is not an object Object.{function}에 개체가 아닌 값을 전달했습니다 +@msg Offset Path: the path is too complex to offset 패스 간격 띄우기: 패스가 너무 복잡해서 간격을 띄울 수 없습니다 +@msg Offset Path: {e} 패스 간격 띄우기: {e} +@msg PDF content is incomplete or exceeds the reader's limits; no document was imported PDF 내용이 불완전하거나 읽기 한도를 넘습니다. 문서를 가져오지 않았습니다 +@msg PDF flattener: {e} PDF 병합: {e} +@msg PDF options: {e} PDF 옵션: {e} +@msg QName.prototype.toString needs a QName QName.prototype.toString에는 QName이 필요합니다 +@msg Recolor Artwork is no longer open 아트웍 색상 재지정이 더 이상 열려 있지 않습니다 +@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp 개체가 아닌 값에서 RegExp.prototype.{name}을(를) 호출했습니다 +@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 SVG가 UTF-8, UTF-16 또는 ISO 8859-1이 아닙니다 +@msg Save writes one artboard: name one, or export several with document.export 저장은 아트보드 하나를 기록합니다: 하나를 지정하거나 document.export로 여러 개를 내보내세요 +@msg Select a dimension first 먼저 치수를 선택하세요 +@msg Select dimensions to update 업데이트할 치수를 선택하세요 +@msg Select only dimensions 치수만 선택하세요 +@msg Select two or more objects, or a group 개체를 두 개 이상 선택하거나 그룹을 선택하세요 +@msg Shaper needs 1–64 source paths 셰이퍼에는 원본 패스가 1–64개 필요합니다 +@msg Stencil Bridges: the selected art is too large or too detailed to bridge 스텐실 브리지: 선택한 아트웍이 너무 크거나 세밀해서 브리지를 만들 수 없습니다 +@msg String.prototype.toString was called on a value that is not a string 문자열이 아닌 값에서 String.prototype.toString을 호출했습니다 +@msg Switch x/y is for scatter graphs x/y 전환은 분산형 그래프에 사용합니다 +@msg Text options: {e} 텍스트 옵션: {e} +@msg The {name} profile could not be read, so the new document has the default settings: {reason} {name} 프로파일을 읽을 수 없어 새 문서에 기본 설정을 사용했습니다: {reason} +@msg Trap: more than {_1} pairs of objects lie close enough to trap, or too many objects share a horizontal range; trap fewer objects at a time 트랩: 트랩할 만큼 가까운 개체 쌍이 {_1}개보다 많거나, 같은 가로 범위에 있는 개체가 너무 많습니다. 한 번에 더 적은 개체를 트랩하세요 +@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off 위딩 상자 및 선: 아트웍 바깥에 들어갈 위딩 선이 없고 위딩 상자가 꺼져 있습니다 +@msg \ at end of pattern 패턴 끝에 \이(가) 있습니다 +@msg `alignOn` cannot be empty `alignOn`은(는) 비워 둘 수 없습니다 +@msg `alignOn` must be a string `alignOn`은(는) 문자열이어야 합니다 +@msg `align` is start, center or end `align`은(는) start, center 또는 end입니다 +@msg `align` must be a string `align`은(는) 문자열이어야 합니다 +@msg `anchors` must be an array of [subpath, anchor] pairs `anchors`은(는) [subpath, anchor] 쌍의 배열이어야 합니다 +@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors`에는 앵커 인덱스(0, 1, 2…)를 나열해야 합니다: {k} +@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors`에는 앵커 인덱스(0, 1, 2…)를 나열해야 합니다: {v} +@msg `answer` for this {kind} dialog must be one of: {_1} 이 {kind} 대화 상자의 `answer`은(는) 다음 중 하나여야 합니다: {_1} +@msg `answers` has more than {MAX_ANSWERS} answers `answers`에 응답이 {MAX_ANSWERS}개보다 많습니다 +@msg `answers` is larger than 1 MiB `answers`이(가) 1 MiB보다 큽니다 +@msg `answers` must be a list of answers `answers`은(는) 응답 목록이어야 합니다 +@msg `artboard` must be the index of an artboard of the document `artboard`은(는) 문서에 있는 아트보드의 인덱스여야 합니다 +@msg `artboardsAt` must be [x, y] `artboardsAt`은(는) [x, y]여야 합니다 +@msg `artboards` must be a list of artboard indices `artboards`은(는) 아트보드 인덱스 목록이어야 합니다 +@msg `artboards` names an artboard the document does not have `artboards`에 문서에 없는 아트보드가 있습니다 +@msg `artboards` names an artboard twice `artboards`에 같은 아트보드가 두 번 있습니다 +@msg `asset` is the asset id (see assets.list) `asset`은(는) 에셋 id입니다(assets.list 참조) +@msg `at` must be [x, y] (got {at}) `at`은(는) [x, y]여야 합니다({at} 입력됨) +@msg `at` must be a byte offset `at`은(는) 바이트 오프셋이어야 합니다 +@msg `at` must be a point [x, y] `at`은(는) 점 [x, y]여야 합니다 +@msg `autoLeading` must be a number `autoLeading`은(는) 숫자여야 합니다 +@msg `background` must be "", "matte" or #rrggbb `background`은(는) "", "matte" 또는 #rrggbb여야 합니다 +@msg `beside` needs `side` `beside`에는 `side`가 필요합니다 +@msg `burasagari` must be "none", "standard" or "forced" `burasagari`은(는) "none", "standard" 또는 "forced"여야 합니다 +@msg `center` must be [x, y] `center`은(는) [x, y]여야 합니다 +@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign`은(는) "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" 또는 "icfBottom"이어야 합니다 +@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection`은(는) "default", "leftToRight" 또는 "rightToLeft"여야 합니다 +@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke`에는 `stroke: false`와 `ids: []`가 필요하며 `stop`은 쓸 수 없습니다 +@msg `clear` must be a list of type object ids, not {v} `clear`은(는) 문자 개체 id 목록이어야 합니다: {v} +@msg `clear` must list type objects; {_1} is not one `clear`에는 문자 개체를 나열해야 합니다. {_1}은(는) 문자 개체가 아닙니다 +@msg `clear` must not list `id` `clear`에 `id`를 넣을 수 없습니다 +@msg `code` must be a whole number `code`은(는) 정수여야 합니다 +@msg `colorMode` must be one of rgb, cmyk `colorMode`은(는) rgb, cmyk 중 하나여야 합니다 +@msg `color` needs a `rule` to make the theme from `color`에는 테마를 만들 `rule`이 필요합니다 +@msg `colors` does not apply to the {name} reduction `colors`은(는) {name} 감소에는 적용되지 않습니다 +@msg `colors` is empty `colors`이(가) 비어 있습니다 +@msg `colors` must be a whole number from 2 to 256 `colors`은(는) 2에서 256 사이의 정수여야 합니다 +@msg `colors` must be at least 9 for the uniform reduction uniform 감소에서는 `colors`이(가) 9 이상이어야 합니다 +@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors`은(는) 1에서 {MAX_THEME_COLORS} 사이여야 합니다({n} 입력됨) +@msg `columnType` is vertical, uniform, repeating or sliding `columnType`은(는) vertical, uniform, repeating 또는 sliding입니다 +@msg `columns` must be a whole number 1..50 `columns`은(는) 1..50의 정수여야 합니다 +@msg `commands` must not be empty `commands`은(는) 비어 있으면 안 됩니다 +@msg `contents` goes with `to` `contents`은(는) `to`와 함께 씁니다 +@msg `corners` must be sharp, round or square, not `{other}` `corners`은(는) sharp, round 또는 square여야 합니다: `{other}` +@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners`에는 앵커 인덱스(0, 1, 2…)를 나열해야 합니다: {v} +@msg `count` must be a whole number from 0 to {MAX_COUNT} `count`은(는) 0에서 {MAX_COUNT} 사이의 정수여야 합니다 +@msg `count` must be a whole number from 1 to 100 `count`은(는) 1에서 100 사이의 정수여야 합니다 +@msg `dataBase64` is larger than the module limit `dataBase64`이(가) 모듈 한도보다 큽니다 +@msg `dataBase64` is not valid base64 `dataBase64`이(가) 올바른 base64가 아닙니다 +@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly`은(는) `ids`, `reverse` 또는 `append`와 함께 쓸 수 없습니다 +@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth`와 `childLimit`은 요약을 잘라 냅니다: `summary: true`를 전달하세요 +@msg `design` is a design name, or null for plain columns `design`은(는) 디자인 이름이며, 일반 막대에는 null입니다 +@msg `design` is a design name, or null for the default marker `design`은(는) 디자인 이름이며, 기본 마커에는 null입니다 +@msg `destinationFolder` is required when destination is `folder` destination이 `folder`이면 `destinationFolder`가 필요합니다 +@msg `destination` must be `none`, `saveAndClose` or `folder` `destination`은(는) `none`, `saveAndClose` 또는 `folder`여야 합니다 +@msg `diameter` must be from 0.1 to 2500 pt `diameter`은(는) 0.1에서 2500 pt 사이여야 합니다 +@msg `digits` must be one of {_1} `digits`은(는) {_1} 중 하나여야 합니다 +@msg `dimImages` is a percentage or false `dimImages`은(는) 백분율 또는 false입니다 +@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction`은(는) "auto", "leftToRight" 또는 "rightToLeft"여야 합니다 +@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount`은(는) 0에서 100 사이의 숫자여야 합니다 +@msg `each` is the value one design stands for, more than 0 `each`은(는) 디자인 하나가 나타내는 값이며 0보다 커야 합니다 +@msg `end` must be "start" or "end" `end`은(는) "start" 또는 "end"여야 합니다 +@msg `ends` applies to joining exactly two paths `ends`은(는) 정확히 두 패스를 연결할 때 적용됩니다 +@msg `event` must be one of {_1} `event`은(는) {_1} 중 하나여야 합니다 +@msg `every` must be "all", "odd" or "even", not {v} `every`은(는) "all", "odd" 또는 "even"이어야 합니다: {v} +@msg `expand` must be true or false `expand`은(는) true 또는 false여야 합니다 +@msg `fidelity` must be from {lo} to {hi} pt `fidelity`은(는) {lo}에서 {hi} pt 사이여야 합니다 +@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files`은(는) 마지막 text.findFontFiles 검색에서 찾은 글꼴 파일의 경로 1개에서 1000개여야 합니다 +@msg `focal` and `radius` cannot be used together `focal`과 `radius`는 함께 쓸 수 없습니다 +@msg `focal` must be an array of [subpath, anchor] pairs `focal`은(는) [subpath, anchor] 쌍의 배열이어야 합니다 +@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal`의 [{si}, {ai}]은(는) 패스의 앵커가 아닙니다 +@msg `folder` must be an absolute folder path `folder`은(는) 폴더의 절대 경로여야 합니다 +@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes`은(는) {"wght"}처럼 축 태그와 값으로 이루어진 개체여야 합니다 +@msg `format` must be a string `format`은(는) 문자열이어야 합니다 +@msg `format` must be one of {_1} `format`은(는) {_1} 중 하나여야 합니다 +@msg `fractions` is chop or scale `fractions`은(는) chop 또는 scale입니다 +@msg `from` and `to` must differ `from`과 `to`는 서로 달라야 합니다 +@msg `from` has an empty frame `from`에 빈 프레임이 있습니다 +@msg `from` must be a non-negative integer `from`은(는) 음이 아닌 정수여야 합니다 +@msg `from` must be area type or type on a path `from`은(는) 영역 문자 또는 패스 위 문자여야 합니다 +@msg `gid` must be a glyph id, not {v} `gid`은(는) 글리프 id여야 합니다: {v} +@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate`은(는) 문자 스타일이 아니라 글자 하나에 속합니다(text.setGlyphAlternate로 설정) +@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate`에는 글리프 선택을 없애는 null만 쓸 수 있습니다(선택은 text.setGlyphAlternate로 지정) +@msg `group` must be one of {_1} `group`은(는) {_1} 중 하나여야 합니다 +@msg `group` must be the Objects on Path group of `member` and `target` `group`은(는) `member`와 `target`이 속한 패스 위 개체 그룹이어야 합니다 +@msg `guide` must be a guide index (guide.list) `guide`은(는) 안내선 인덱스여야 합니다(guide.list) +@msg `hAlign` must be default, left, center or right `hAlign`은(는) default, left, center 또는 right여야 합니다 +@msg `id` is an object id, not {v} `id`은(는) 개체 id입니다: {v} +@msg `id` must be a node id `id`은(는) 노드 id여야 합니다 +@msg `id` must be an object id, not {v} `id`은(는) 개체 id여야 합니다: {v} +@msg `ids` must name Objects on Path groups or objects in them `ids`에는 패스 위 개체 그룹이나 그 안의 개체를 지정해야 합니다 +@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids`에는 id를 최대 {MAX_INSPECT_IDS}개까지 넣을 수 있습니다 +@msg `imageMap` must be none, rectangle or polygon `imageMap`은(는) none, rectangle 또는 polygon이어야 합니다 +@msg `includeArtboardFill` must be true or false `includeArtboardFill`은(는) true 또는 false여야 합니다 +@msg `includeSubdirs` must be true or false `includeSubdirs`은(는) true 또는 false여야 합니다 +@msg `index` must be a point index `index`은(는) 점 인덱스여야 합니다 +@msg `index` must be a whole number or null `index`은(는) 정수 또는 null이어야 합니다 +@msg `indices` must be a non-empty array of item indices `indices`은(는) 항목 인덱스가 하나 이상 있는 배열이어야 합니다 +@msg `item` needs `index` `item`에는 `index`가 필요합니다 +@msg `items` is a list of {kind, item} `items`은(는) {kind, item}의 목록입니다 +@msg `items` is empty `items`이(가) 비어 있습니다 +@msg `items` must be a list of {id, points} `items`은(는) {id, points}의 목록이어야 합니다 +@msg `items` must be a list of {{id, anchors?}}, not {v} `items`은(는) {{id, anchors?}}의 목록이어야 합니다: {v} +@msg `items` must be an array of {id, anchors} `items`은(는) {id, anchors}의 배열이어야 합니다 +@msg `items` must be an array of {{id, corners?}}, not {v} `items`은(는) {{id, corners?}}의 배열이어야 합니다: {v} +@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida`은(는) "none", "short", "medium", "long" 또는 "stylistic"이어야 합니다 +@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore`은(는) 문자 스타일이 아니라 줄의 첫 글자에 속합니다(text.setRangeStyle로 설정) +@msg `kind` is one of {_1} `kind`은(는) {_1} 중 하나입니다 +@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind`은(는) "round", "invertedRound" 또는 "chamfer"여야 합니다: {v} +@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind`은(는) `text`, `visibility`, `image` 또는 `graph`여야 합니다 +@msg `kind` must be a string `kind`은(는) 문자열이어야 합니다 +@msg `kind` must be image, noImage or htmlText `kind`은(는) image, noImage 또는 htmlText여야 합니다 +@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType`은(는) "pushInFirst", "pushOutFirst" 또는 "pushOutOnly"여야 합니다 +@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku`은(는) "none", "hard" 또는 "soft"여야 합니다 +@msg `label` must be a non-empty name of at most 200 characters `label`은(는) 비어 있지 않은 최대 200자의 이름이어야 합니다 +@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language`은(는) "Turkish"나 "tr" 같은 언어 이름 또는 태그여야 합니다 +@msg `leader` must be a string `leader`은(는) 문자열이어야 합니다 +@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel`은(는) "romanBaseline" 또는 "emBoxTop"이어야 합니다 +@msg `leading` must be from 0.1 to 5000, or null for Auto `leading`은(는) 0.1에서 5000 사이이거나, 자동이면 null이어야 합니다 +@msg `length` must be a number of at most 1000000 pt either way `length`은(는) 양쪽 방향으로 최대 1000000 pt의 숫자여야 합니다 +@msg `line` must be true or false `line`은(는) true 또는 false여야 합니다 +@msg `max` must be 1 to {MAX_SUGGESTIONS} `max`은(는) 1에서 {MAX_SUGGESTIONS} 사이여야 합니다 +@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle`({min_angle}°)은 `maxAngle`({max_angle}°)보다 클 수 없습니다 +@msg `mode` must be "handles", "straight" or "smooth" `mode`은(는) "handles", "straight" 또는 "smooth"여야 합니다 +@msg `mode` must be "straight", "bezier" or "arc" `mode`은(는) "straight", "bezier" 또는 "arc"여야 합니다 +@msg `mode` must be one of {_1} `mode`은(는) {_1} 중 하나여야 합니다 +@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi`은(는) "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" 또는 "lineEndHalf"여야 합니다 +@msg `name` must be a name that is not empty `name`은(는) 비어 있지 않은 이름이어야 합니다 +@msg `name` must not be empty `name`은(는) 비어 있으면 안 됩니다 +@msg `names` is a list of preset names `names`은(는) 사전 설정 이름 목록입니다 +@msg `names` lists preset names `names`에는 사전 설정 이름을 나열합니다 +@msg `names` must be a list of names `names`은(는) 이름 목록이어야 합니다 +@msg `names` must be a list of swatch names `names`은(는) 견본 이름 목록이어야 합니다 +@msg `names` must be an array of style names `names`은(는) 스타일 이름의 배열이어야 합니다 +@msg `new` is a design name, or true `new`은(는) 디자인 이름 또는 true입니다 +@msg `origin` must be [x, y] in points `origin`은(는) 포인트 단위의 [x, y]여야 합니다 +@msg `path` must be a folder `path`은(는) 폴더여야 합니다 +@msg `path` must be a path object `path`은(는) 패스 개체여야 합니다 +@msg `picks` must be an array (got {v}) `picks`은(는) 배열이어야 합니다({v} 입력됨) +@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks`에는 선택을 1개에서 {MAX_PICKS}개까지 넣어야 합니다({_1} 입력됨) +@msg `pivot` must be a whole number from 0 to 8 `pivot`은(는) 0에서 8 사이의 정수여야 합니다 +@msg `points` must be a list of [x, y] points `points`은(는) [x, y] 점의 목록이어야 합니다 +@msg `points` must be a list of point indexes `points`은(는) 점 인덱스 목록이어야 합니다 +@msg `points` must be an array of point indices `points`은(는) 점 인덱스의 배열이어야 합니다 +@msg `points` must be point indexes `points`은(는) 점 인덱스여야 합니다 +@msg `points` must hold from 2 to 100000 points `points`에는 점이 2개에서 100000개 있어야 합니다 +@msg `port` must be "in" or "out" `port`은(는) "in" 또는 "out"이어야 합니다 +@msg `position` is above or below `position`은(는) above 또는 below입니다 +@msg `preset` must be a string `preset`은(는) 문자열이어야 합니다 +@msg `radius` must be 0 or more `radius`은(는) 0 이상이어야 합니다 +@msg `ratio` must be from 0.1 to 0.5 `ratio`은(는) 0.1에서 0.5 사이여야 합니다 +@msg `rect` must be [x, y, width, height] `rect`은(는) [x, y, width, height]여야 합니다 +@msg `rect` must be [x, y, width, height] with a positive size `rect`은(는) 크기가 양수인 [x, y, width, height]여야 합니다 +@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect`은(는) [x0, y0, x1, y1]이어야 합니다({r} 입력됨) +@msg `relation` must be overlap, inside or outside `relation`은(는) overlap, inside 또는 outside여야 합니다 +@msg `replace` must be a string `replace`은(는) 문자열이어야 합니다 +@msg `reversed` must be true or false `reversed`은(는) true 또는 false여야 합니다 +@msg `rotate` must be a finite number `rotate`은(는) 유한한 숫자여야 합니다 +@msg `rows` must be lists of numbers or nulls, not {r} `rows`은(는) 숫자 또는 null의 목록이어야 합니다: {r} +@msg `rule` must be nonZero or evenOdd `rule`은(는) nonZero 또는 evenOdd여야 합니다 +@msg `scale` must be a number from 0.001 to 1000 `scale`은(는) 0.001에서 1000 사이의 숫자여야 합니다 +@msg `scale` must be a number in (0, 100] `scale`은(는) (0, 100] 범위의 숫자여야 합니다 +@msg `scope.{key}` must be a non-empty array `scope.{key}`은(는) 비어 있지 않은 배열이어야 합니다 +@msg `scope` must be "document", {artboards} or {layers} `scope`은(는) "document", {artboards} 또는 {layers}여야 합니다 +@msg `scope` must be "selection" or "document", not {other} `scope`은(는) "selection" 또는 "document"여야 합니다: {other} +@msg `script` must be a string `script`은(는) 문자열이어야 합니다 +@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes`은(는) 계열 인덱스가 하나 이상 있는 목록입니다 +@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes`에는 `type` 또는 `valueAxis`가 필요합니다 +@msg `shift` must be a number of points `shift`은(는) 포인트 단위의 숫자여야 합니다 +@msg `showCenter` must be true or false `showCenter`은(는) true 또는 false여야 합니다 +@msg `side` must be "left", "right", "top" or "bottom" `side`은(는) "left", "right", "top" 또는 "bottom"이어야 합니다 +@msg `side` places the artboard: leave out x, y, width and height `side`가 아트보드의 위치를 정합니다: x, y, width, height는 생략하세요 +@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification`은(는) "fullJustify", "alignLeft", "alignCenter" 또는 "alignRight"여야 합니다 +@msg `size` is a percentage of the base text's size, 1 to 100 `size`은(는) 기준 텍스트 크기에 대한 백분율이며 1에서 100 사이입니다 +@msg `source` and `target` are the same object `source`와 `target`이 같은 개체입니다 +@msg `source` is the only path in series group `target` `source`은(는) 계열 그룹 `target`에 있는 유일한 패스입니다 +@msg `source` must be `folder` or `datasets` `source`은(는) `folder` 또는 `datasets`여야 합니다 +@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` 값으로는 상자에 선이 {MAX_COUNT}개보다 많이 생깁니다 +@msg `spacing` must be a number `spacing`은(는) 숫자여야 합니다 +@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing`은(는) 0 이상의 포인트 값이거나 "auto"여야 합니다 +@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing`은(는) 0보다 크고 {MAX_DISTANCE} pt 이하여야 합니다 +@msg `start` must be a finite number `start`은(는) 유한한 숫자여야 합니다 +@msg `starts` must be an array of anchor indices (or nulls) `starts`은(는) 앵커 인덱스(또는 null)의 배열이어야 합니다 +@msg `status` must be linked, embedded, missing or modified, or a list of them `status`은(는) linked, embedded, missing 또는 modified이거나 이들의 목록이어야 합니다 +@msg `stop` must be a stop index `stop`은(는) 정지점 인덱스여야 합니다 +@msg `story` styles the whole story: give no `start` or `end` `story`은(는) 스토리 전체에 스타일을 적용합니다: `start`와 `end`는 지정하지 마세요 +@msg `story` takes the whole story: give no `start` or `end` `story`은(는) 스토리 전체를 대상으로 합니다: `start`와 `end`는 지정하지 마세요 +@msg `strokeMode` applies to strokes (stroke: true) `strokeMode`은(는) 선에 적용됩니다(stroke: true) +@msg `style` must be roman or japanese `style`은(는) roman 또는 japanese여야 합니다 +@msg `subpaths` must be two integer indices `subpaths`은(는) 정수 인덱스 두 개여야 합니다 +@msg `symbol` must be a symbol name `symbol`은(는) 심볼 이름이어야 합니다 +@msg `target` must be "object" or "contents", not {v} `target`은(는) "object" 또는 "contents"여야 합니다: {v} +@msg `target` must be an object of the same Objects on Path group as `member` `target`은(는) `member`와 같은 패스 위 개체 그룹에 있는 개체여야 합니다 +@msg `tint` applies to global and spot colours `tint`은(는) 글로벌 색상과 별색에 적용됩니다 +@msg `tint` goes with a global or spot `swatch` `tint`은(는) 글로벌 색상 또는 별색 `swatch`와 함께 씁니다 +@msg `tint` must be a number (%) `tint`은(는) 숫자(%)여야 합니다 +@msg `tint` needs a global or spot colour `tint`에는 글로벌 색상이나 별색이 필요합니다 +@msg `to` is already threaded `to`은(는) 이미 스레드로 연결되어 있습니다 +@msg `to` is the design's new name `to`은(는) 디자인의 새 이름입니다 +@msg `to` must be [x, y] `to`은(는) [x, y]여야 합니다 +@msg `to` must be a closed path, area type or type on a path other than `from` `to`은(는) `from`이 아닌 닫힌 패스, 영역 문자 또는 패스 위 문자여야 합니다 +@msg `to` must be a whole number `to`은(는) 정수여야 합니다 +@msg `to` takes the rows of one object's stack, not aligned rows `to`에는 정렬된 행이 아니라 한 개체 스택의 행을 지정합니다 +@msg `tolerance` must be from 0 to 100 pt `tolerance`은(는) 0에서 100 pt 사이여야 합니다 +@msg `useLab` must be true or false `useLab`은(는) true 또는 false여야 합니다 +@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign`은(는) default, top, middle, baseline 또는 bottom이어야 합니다 +@msg `values` must be a list `values`은(는) 목록이어야 합니다 +@msg `values` must be an object `values`은(는) 개체여야 합니다 +@msg `values` must name hex, rgb, cmyk, lab or gray `values`에는 hex, rgb, cmyk, lab 또는 gray를 지정해야 합니다 +@msg `values` names a kind twice `values`에 같은 종류가 두 번 있습니다 +@msg `version` must be 0 or {_1} `version`은(는) 0 또는 {_1}이어야 합니다 +@msg `version` must be 0 or {newest} `version`은(는) 0 또는 {newest}이어야 합니다 +@msg `warichuAlign` must be one of {_1} `warichuAlign`은(는) {_1} 중 하나여야 합니다 +@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines`은(는) {_1}에서 {_2} 사이의 정수여야 합니다 +@msg `weight` must be from 0.01 to 1000 pt `weight`은(는) 0.01에서 1000 pt 사이여야 합니다 +@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights`은(는) 행마다 하나씩 숫자 {_1}개의 목록이어야 합니다({v} 입력됨) +@msg `xml` must be a string `xml`은(는) 문자열이어야 합니다 +@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it `{KEY}` 항목 {n}은(는) {_1}의 바이트 {start}에 있는 단락에 앞선 항목과 다른 들여쓰기를 지정합니다 +@msg `{KEY}` entry {n} must be an object `{KEY}` 항목 {n}은(는) 개체여야 합니다 +@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` `{KEY}` 항목 {n}이(가) `leftIndent`와 `firstLineIndent` 중 어느 것도 설정하지 않습니다 +@msg `{KEY}` entry {n}: `id` must be an object id `{KEY}` 항목 {n}: `id`은(는) 개체 id여야 합니다 +@msg `{KEY}` entry {n}: `start` must be a byte offset `{KEY}` 항목 {n}: `start`은(는) 바이트 오프셋이어야 합니다 +@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt `{KEY}` 항목 {n}: `{k}`은(는) -{MAX_PARA}에서 {MAX_PARA} pt 사이의 숫자여야 합니다 +@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} `{KEY}` 항목 {n}: {_1}에는 바이트 {start}에서 시작하는 단락이 없습니다 +@msg `{KEY}` entry {n}: unknown key `{k}` `{KEY}` 항목 {n}: 알 수 없는 키 `{k}` +@msg `{KEY}` entry {n}: {_1} is not a type object `{KEY}` 항목 {n}: {_1}은(는) 문자 개체가 아닙니다 +@msg `{KEY}` is empty `{KEY}`이(가) 비어 있습니다 +@msg `{KEY}` must be a list `{KEY}`은(는) 목록이어야 합니다 +@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}`에는 항목을 최대 {MAX_PARAGRAPH_INDENTS}개까지 넣을 수 있습니다({_1} 입력됨) +@msg `{KEY}` takes no `{other}` beside it `{KEY}`은(는) `{other}`와(과) 함께 쓸 수 없습니다 +@msg `{_1}` can't be edited or removed `{_1}`은(는) 편집하거나 제거할 수 없습니다 +@msg `{_1}` is built in and always listed `{_1}`은(는) 기본 제공 항목이며 항상 목록에 있습니다 +@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}`은(는) 마지막 text.findFontFiles 검색에서 찾은 글꼴 파일이 아닙니다 +@msg `{_1}` would be overwritten by the package: choose a different folder or name 패키지가 `{_1}`을(를) 덮어쓰게 됩니다: 다른 폴더나 이름을 선택하세요 +@msg `{base}` is larger than {_1} MB `{base}`이(가) {_1} MB보다 큽니다 +@msg `{base}` is not in the DCS file's folder `{base}`이(가) DCS 파일의 폴더에 없습니다 +@msg `{file}` has no art to place `{file}`에 가져올 아트웍이 없습니다 +@msg `{file}` is over {_1} MB `{file}`이(가) {_1} MB를 넘습니다 +@msg `{f}` belongs to a VectorCraft that is running `{f}`은(는) 실행 중인 VectorCraft가 사용하고 있습니다 +@msg `{f}` is the copy of a document open now `{f}`은(는) 지금 열려 있는 문서의 복사본입니다 +@msg `{id}` is no writable format (see document.formats) `{id}`은(는) 기록할 수 있는 형식이 아닙니다(document.formats 참조) +@msg `{id}` isn't a loaded library `{id}`은(는) 불러온 라이브러리가 아닙니다 +@msg `{key}` has no colours `{key}`에 색상이 없습니다 +@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}`에 단어가 {MAX_EDIT_WORDS}개보다 많습니다 +@msg `{key}` is a design name `{key}`은(는) 디자인 이름입니다 +@msg `{key}` is a design name or a list of them `{key}`은(는) 디자인 이름 또는 디자인 이름 목록입니다 +@msg `{key}` is longer than {MAX_NAME} characters `{key}`이(가) {MAX_NAME}자보다 깁니다 +@msg `{key}` is out of range `{key}`이(가) 범위를 벗어났습니다 +@msg `{key}` must be 0 or more `{key}`은(는) 0 이상이어야 합니다 +@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}`은(는) 백분율 단위의 [min, desired, max] 또는 {{min?, desired?, max?}}여야 합니다 +@msg `{key}` must be [x, y] numbers `{key}`은(는) 숫자 [x, y]여야 합니다 +@msg `{key}` must be a list of words `{key}`은(는) 단어 목록이어야 합니다 +@msg `{key}` must be a list, not {v} `{key}`은(는) 목록이어야 합니다: {v} +@msg `{key}` must be a non-negative integer `{key}`은(는) 음이 아닌 정수여야 합니다 +@msg `{key}` must be a number or null, not {v} `{key}`은(는) 숫자 또는 null이어야 합니다: {v} +@msg `{key}` must be a number {_1}..{_2} `{key}`은(는) {_1}..{_2}의 숫자여야 합니다 +@msg `{key}` must be a whole number of 0 or more `{key}`은(는) 0 이상의 정수여야 합니다 +@msg `{key}` must be a whole number of at least 0, not {n} `{key}`은(는) 0 이상의 정수여야 합니다: {n} +@msg `{key}` must be an appearance item index (paint order) or null `{key}`은(는) 모양새 항목 인덱스(칠하는 순서) 또는 null이어야 합니다 +@msg `{key}` must be an array of asset ids (see assets.list) `{key}`은(는) 에셋 id의 배열이어야 합니다(assets.list 참조) +@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}`은(는) 0도에서 180도 사이여야 합니다: {a} +@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}`은(는) 0에서 {MAX_DISTANCE} pt 사이여야 합니다 +@msg `{key}` must be from 0.01 to 1000 pt `{key}`은(는) 0.01에서 1000 pt 사이여야 합니다 +@msg `{key}` must be from {_1} to {shift_max} `{key}`은(는) {_1}에서 {shift_max} 사이여야 합니다 +@msg `{key}` must be from {lo} to {hi}, not {v} `{key}`은(는) {lo}에서 {hi} 사이여야 합니다: {v} +@msg `{key}` must be one of auto, always, never `{key}`은(는) auto, always, never 중 하나여야 합니다 +@msg `{key}` must be one of miter, round, bevel `{key}`은(는) miter, round, bevel 중 하나여야 합니다 +@msg `{key}` must be one word of at most {_1} characters `{key}`은(는) 최대 {_1}자의 단어 하나여야 합니다 +@msg `{key}` must hold single words of at most {_1} characters `{key}`에는 최대 {_1}자의 단어를 하나씩 넣어야 합니다 +@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}`에는 패스 위 개체 그룹의 패스에 부착된 개체를 지정해야 합니다 +@msg `{key}` names an object that is hidden or locked `{key}`에 지정한 개체가 숨겨졌거나 잠겨 있습니다 +@msg `{key}`: not a colour: {v} `{key}`: 색상이 아닙니다: {v} +@msg `{k}.family` is required `{k}.family`가 필요합니다 +@msg `{k}` is a number 0..1 `{k}`은(는) 0..1의 숫자입니다 +@msg `{k}` is true or false `{k}`은(는) true 또는 false입니다 +@msg `{k}` isn't an export setting: {_1} `{k}`은(는) 내보내기 설정이 아닙니다: {_1} +@msg `{k}` must be a byte offset `{k}`은(는) 바이트 오프셋이어야 합니다 +@msg `{k}` must be a number `{k}`은(는) 숫자여야 합니다 +@msg `{k}` must be a number of points `{k}`은(는) 포인트 단위의 숫자여야 합니다 +@msg `{k}` must be a string `{k}`은(는) 문자열이어야 합니다 +@msg `{k}` must be a whole number `{k}`은(는) 정수여야 합니다 +@msg `{k}` must be from {lo} to {hi} for {key} {key}에서는 `{k}`이(가) {lo}에서 {hi} 사이여야 합니다 +@msg `{k}` must be {what}, not {v} `{k}`은(는) {what}이어야 합니다: {v} +@msg `{l}` isn't a global or spot colour `{l}`은(는) 글로벌 색상이나 별색이 아닙니다 +@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}`은(는) 비트맵이 아닙니다: clipboard.importSvg, importPdf 또는 importText를 사용하세요 +@msg `{name}` can't be deleted `{name}`은(는) 삭제할 수 없습니다 +@msg `{name}` can't be duplicated `{name}`은(는) 복제할 수 없습니다 +@msg `{name}` can't be edited `{name}`은(는) 편집할 수 없습니다 +@msg `{name}` can't be edited this way `{name}`은(는) 이 방법으로 편집할 수 없습니다 +@msg `{name}` can't be renamed `{name}`은(는) 이름을 바꿀 수 없습니다 +@msg `{name}` can't name a folder `{name}`은(는) 폴더 이름으로 쓸 수 없습니다 +@msg `{name}` has no art to place `{name}`에 가져올 아트웍이 없습니다 +@msg `{name}` has no text to place `{name}`에 가져올 텍스트가 없습니다 +@msg `{name}` in the Word document is not UTF-8 or UTF-16 text Word 문서의 `{name}`이(가) UTF-8 또는 UTF-16 텍스트가 아닙니다 +@msg `{name}` in the Word document is not well-formed XML: {e} Word 문서의 `{name}`이(가) 올바른 형식의 XML이 아닙니다: {e} +@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep Word 문서의 `{name}`에 요소가 {MAX_NESTING}단계보다 깊게 중첩되어 있습니다 +@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}`은(는) 기본 제공 사전 설정이며 읽기 전용입니다: 다른 이름으로 저장하세요 +@msg `{name}` is a built-in preset: save under another name `{name}`은(는) 기본 제공 사전 설정입니다: 다른 이름으로 저장하세요 +@msg `{name}` is a built-in profile and stays `{name}`은(는) 기본 제공 프로파일이므로 그대로 남습니다 +@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}`은(는) 별도의 파일이며 DCS 파일의 경로 옆에서만 찾을 수 있습니다 +@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}`은(는) `{base}`의 색조입니다: `{base}`을(를) 편집하세요(색조 견본은 이름만 바꿀 수 있습니다) +@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}`은(는) 작업 공간입니다: 먼저 edit.colorSettings로 다른 작업 공간을 선택하세요 +@msg `{name}` is built in and stays `{name}`은(는) 기본 제공 항목이므로 그대로 남습니다 +@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}`에는 같은 종류의 페인트(색상, 그레이디언트 또는 패턴)만 지정할 수 있습니다 +@msg `{name}`: {_1}: {e} `{name}`: {_1}: {e} +@msg `{n}` can't be deleted `{n}`은(는) 삭제할 수 없습니다 +@msg `{n}` can't be merged `{n}`은(는) 병합할 수 없습니다 +@msg `{n}` can't be moved `{n}`은(는) 이동할 수 없습니다 +@msg `{n}` is not a colour swatch `{n}`은(는) 색상 견본이 아닙니다 +@msg `{n}` isn't a solid colour `{n}`은(는) 단색이 아닙니다 +@msg `{n}`: colour groups hold solid colours only `{n}`: 색상 그룹에는 단색만 넣을 수 있습니다 +@msg `{other}` doesn't trace an image `{other}`은(는) 이미지를 추적하지 않습니다 +@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}`: 이 단계의 참조가 {_1} MB보다 많은 값을 복사해 넣습니다 +@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}`은(는) 축 태그가 아닙니다: 태그는 "wght"처럼 네 글자입니다 +@msg `{to}` is empty or already used `{to}`이(가) 비어 있거나 이미 사용 중입니다 +@msg `{u}` is no Unicode character `{u}`은(는) 유니코드 문자가 아닙니다 +@msg `{what}` is longer than {MAX_TEXT} bytes `{what}`이(가) {MAX_TEXT}바이트보다 깁니다 +@msg `{what}` must be text `{what}`은(는) 텍스트여야 합니다 +@msg `{word}` isn't in the custom dictionary `{word}`은(는) 사용자 사전에 없습니다 +@msg a CMap has too many mappings CMap에 매핑이 너무 많습니다 +@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard DXF 도면에는 원점이 하나입니다: 아트보드마다 도면을 하나씩 만들려면 useArtboards: true를 전달하세요 +@msg a PDF presets file with over {_1} presets 사전 설정이 {_1}개를 넘는 PDF 사전 설정 파일입니다 +@msg a \u escape needs four hexadecimal digits \u 이스케이프에는 16진수 네 자리가 필요합니다 +@msg a batch preview cannot switch documents 일괄 처리 미리 보기에서는 문서를 전환할 수 없습니다 +@msg a brush named `{nn}` exists `{nn}` 브러시가 이미 있습니다 +@msg a comma in an XPath expression must separate function arguments XPath 식의 쉼표는 함수 인수를 구분해야 합니다 +@msg a container of the listed objects is gone 나열한 개체의 컨테이너 하나가 없어졌습니다 +@msg a content stream can't be read 콘텐츠 스트림을 읽을 수 없습니다 +@msg a content stream isn't a stream 콘텐츠 스트림이 스트림이 아닙니다 +@msg a design holds at most {_1} objects 디자인에는 개체를 최대 {_1}개까지 넣을 수 있습니다 +@msg a design needs a name 디자인에는 이름이 필요합니다 +@msg a dictionary grew too large 사전이 너무 커졌습니다 +@msg a document holds at most {_1} SVG filters 문서에는 SVG 필터를 최대 {_1}개까지 둘 수 있습니다 +@msg a document keeps at most {_1} graph designs 문서에는 그래프 디자인을 최대 {_1}개까지 둘 수 있습니다 +@msg a document keeps at most {_1} saved selections 문서에는 저장된 선택 영역을 최대 {_1}개까지 둘 수 있습니다 +@msg a document keeps at most {_1} views 문서에는 보기를 최대 {_1}개까지 둘 수 있습니다 +@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files 문서에는 JavaScript 파일을 최대 {MAX_SVG_SCRIPTS}개까지 연결할 수 있습니다 +@msg a document with unsaved changes has no recovery copy 저장하지 않은 변경 내용이 있는 문서에는 복구 복사본이 없습니다 +@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes feConvolveMatrix 필터의 order가 {x}×{y}여서 usvg의 커널 크기 곱셈이 오버플로됩니다 +@msg a font's data is too large 글꼴 데이터가 너무 큽니다 +@msg a font's subroutines are too large 글꼴의 서브루틴이 너무 큽니다 +@msg a function declaration without a name 이름이 없는 함수 선언입니다 +@msg a gradient preset is already called {n} {n}(이)라는 그레이디언트 사전 설정이 이미 있습니다 +@msg a graph has at most {_1} series, so it transposes with at most that many categories 그래프에는 계열이 최대 {_1}개이므로 범주가 그 개수 이하일 때만 행과 열을 바꿀 수 있습니다 +@msg a host value has too many entries 호스트 값에 항목이 너무 많습니다 +@msg a host value is nested too deep 호스트 값이 너무 깊게 중첩되어 있습니다 +@msg a layer can't be merged into its own sublayer 레이어는 자신의 하위 레이어에 병합할 수 없습니다 +@msg a layer's form box isn't an array 레이어 폼의 상자가 배열이 아닙니다 +@msg a layer's form can't be read 레이어의 폼을 읽을 수 없습니다 +@msg a layer's form has no box 레이어의 폼에 상자가 없습니다 +@msg a layer's form has no dictionary 레이어의 폼에 사전이 없습니다 +@msg a legend draws at most {MAX_CHIPS} swatches 범례에는 견본을 최대 {MAX_CHIPS}개까지 그립니다 +@msg a name names one asset: give multiple: false, or one object 이름은 에셋 하나를 가리킵니다: multiple: false를 지정하거나 개체 하나를 지정하세요 +@msg a namespace declaration uses a reserved prefix or URI 네임스페이스 선언에 예약된 접두사나 URI를 사용했습니다 +@msg a namespace qualifier is undefined 네임스페이스 한정자가 정의되지 않았습니다 +@msg a namespace with an empty URI cannot have a prefix URI가 빈 네임스페이스에는 접두사를 붙일 수 없습니다 +@msg a number has no digits 숫자에 자릿수가 없습니다 +@msg a number's exponent has no digits 숫자의 지수에 자릿수가 없습니다 +@msg a number's fraction has no digits 숫자의 소수부에 자릿수가 없습니다 +@msg a page can't be read 페이지를 읽을 수 없습니다 +@msg a page object is used by something else 페이지 개체를 다른 항목이 사용하고 있습니다 +@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) 페이지 미리 보기 이미지는 한 변이 1에서 {THUMBNAIL_SIZE}인 RGB 픽셀이어야 합니다({w} × {h} 입력됨) +@msg a pattern layer doesn't end what it starts 패턴 레이어가 시작한 것을 끝내지 않습니다 +@msg a pattern layer's colors hold something else 패턴 레이어의 색상에 다른 것이 들어 있습니다 +@msg a pattern named `{n}` exists `{n}` 패턴이 이미 있습니다 +@msg a plug-in with id {_1} is already installed id가 {_1}인 플러그인이 이미 설치되어 있습니다 +@msg a preset named `{n}` exists `{n}` 사전 설정이 이미 있습니다 +@msg a print presets file with over {_1} presets 사전 설정이 {_1}개를 넘는 인쇄 사전 설정 파일입니다 +@msg a profile is already called {n} {n}(이)라는 프로파일이 이미 있습니다 +@msg a property descriptor cannot have both a value or writable and a get or set 속성 설명자에는 value 또는 writable과 get 또는 set을 함께 둘 수 없습니다 +@msg a property descriptor must be an object 속성 설명자는 개체여야 합니다 +@msg a property's {name} must be a function or undefined 속성의 {name}은(는) 함수 또는 undefined여야 합니다 +@msg a row can't go into itself 행을 자기 자신 안으로 옮길 수 없습니다 +@msg a series goes on the left or the right value axis 계열은 왼쪽 또는 오른쪽 값 축에 놓입니다 +@msg a start must be an anchor index ≥ 0 or null 시작은 0 이상의 앵커 인덱스 또는 null이어야 합니다 +@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied 그 안의 스트림을 디코딩하면 256 MB를 넘거나 디코딩 매개변수를 적용할 수 없습니다 +@msg a string holds a control character 문자열에 제어 문자가 있습니다 +@msg a string holds an escape sequence JSON does not have 문자열에 JSON에 없는 이스케이프 시퀀스가 있습니다 +@msg a string is not closed 문자열이 닫히지 않았습니다 +@msg a style named `{name}` already exists `{name}` 스타일이 이미 있습니다 +@msg a style named `{to}` already exists `{to}` 스타일이 이미 있습니다 +@msg a swatch needs a colour, gradient or pattern 견본에는 색상, 그레이디언트 또는 패턴이 필요합니다 +@msg a symbol named `{n}` already exists `{n}` 심볼이 이미 있습니다 +@msg a theme has 1 to {THEME_SIZE} colours 테마에는 색상이 1개에서 {THEME_SIZE}개 있습니다 +@msg a theme is already called {name} (replace: "{name}" overwrites it) {name}(이)라는 테마가 이미 있습니다(replace: "{name}"을(를) 지정하면 덮어씁니다) +@msg a toolbar name has at most {MAX_NAME} characters 도구 막대 이름은 최대 {MAX_NAME}자입니다 +@msg a view named `{_1}` exists `{_1}` 보기가 이미 있습니다 +@msg a view named `{name}` exists `{name}` 보기가 이미 있습니다 +@msg a workspace keeps at most {MAX} toolbars 작업 영역에는 도구 막대를 최대 {MAX}개까지 둘 수 있습니다 +@msg a {_1} series can't go in a {_2} graph {_1} 계열은 {_2} 그래프에 넣을 수 없습니다 +@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v}: "pdf"(PDF 호환, 기본값) 또는 "7"(이전 .ai, 버전 7)이어야 합니다 +@msg already editing an opacity mask 이미 불투명 마스크를 편집하고 있습니다 +@msg already editing another object's opacity mask 이미 다른 개체의 불투명 마스크를 편집하고 있습니다 +@msg already in pattern editing mode 이미 패턴 편집 모드입니다 +@msg an SVG filter is named by its id: give `xml` with `name` SVG 필터의 이름은 id입니다: `name`과 함께 `xml`을 지정하세요 +@msg an XML element has two attributes with the same name XML 요소에 이름이 같은 속성이 두 개 있습니다 +@msg an XML value cannot be inserted into itself or its descendants XML 값을 자기 자신이나 그 하위 항목에 삽입할 수 없습니다 +@msg an XML value's children cannot be assigned by position XML 값의 자식은 위치로 대입할 수 없습니다 +@msg an XML value's children cannot be deleted by position XML 값의 자식은 위치로 삭제할 수 없습니다 +@msg an XMLList converts to XML only when it has one item XMLList는 항목이 하나일 때만 XML로 변환됩니다 +@msg an XPath node test is missing XPath 노드 테스트가 없습니다 +@msg an XPath operand is missing XPath 피연산자가 없습니다 +@msg an XPath operator is missing XPath 연산자가 없습니다 +@msg an XPath predicate or step cannot follow here 여기에는 XPath 조건자나 단계가 올 수 없습니다 +@msg an XPath predicate or step follows a value that is not a node-set 노드 집합이 아닌 값 뒤에 XPath 조건자나 단계가 있습니다 +@msg an XPath step is missing XPath 단계가 없습니다 +@msg an artboard is listed twice 아트보드가 두 번 나열되었습니다 +@msg an empty artboard has no width or height to scale to 빈 아트보드에는 맞춰 비율을 조정할 너비나 높이가 없습니다 +@msg an image couldn't be decoded and is missing from the picture 이미지를 디코딩할 수 없어 그림에서 빠졌습니다 +@msg an image is too large for DXF and was left out 이미지가 DXF에 쓰기에는 너무 커서 제외되었습니다 +@msg an image of {_1} MB, at most {_2} MB 이미지가 {_1} MB입니다(최대 {_2} MB) +@msg an image of {declared_w} × {declared_h} pixels is too large to draw {declared_w} × {declared_h} 픽셀 이미지는 너무 커서 그릴 수 없습니다 +@msg an image of {width} × {height} pixels is too large to draw {width} × {height} 픽셀 이미지는 너무 커서 그릴 수 없습니다 +@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) {w} × {h} 픽셀 이미지는 너무 커서 변환할 수 없습니다(최대 {_1} 픽셀) +@msg an index must be a whole number 인덱스는 정수여야 합니다 +@msg an inline graphic has no glyphs 인라인 그래픽에 글리프가 없습니다 +@msg an invisible text form can't be read 보이지 않는 텍스트 폼을 읽을 수 없습니다 +@msg an object is missing 개체가 없습니다 +@msg an object's header is missing 개체의 헤더가 없습니다 +@msg an overprinting form can't be read 오버프린트 폼을 읽을 수 없습니다 +@msg an unread object has no readable bounds 읽지 않은 개체에 읽을 수 있는 경계가 없습니다 +@msg an unread object holds a frame of threaded type 읽지 않은 개체에 스레드 문자의 프레임이 있습니다 +@msg an unread object is missing from its layer 읽지 않은 개체가 레이어에 없습니다 +@msg anchor `smooth` must be true or false 앵커의 `smooth`은(는) true 또는 false여야 합니다 +@msg anchor `{key}` must be [x, y] 앵커의 `{key}`은(는) [x, y]여야 합니다 +@msg anchor `{key}` must be a number 앵커의 `{key}`은(는) 숫자여야 합니다 +@msg angle and roundness must be finite 각도와 원형률은 유한해야 합니다 +@msg angles must be a list as long as pins (degrees or null) angles는 pins와 길이가 같은 목록이어야 합니다(도 또는 null) +@msg appearance must be flat|center|edge appearance는 flat|center|edge여야 합니다 +@msg apply is "fill" or "stroke", not `{other}` apply는 "fill" 또는 "stroke"입니다: `{other}` +@msg apply was called on a value that is not a function 함수가 아닌 값에서 apply를 호출했습니다 +@msg area must be a closed polygon of 3 to 100000 [x, y] points area는 [x, y] 점 3개에서 100000개로 이루어진 닫힌 다각형이어야 합니다 +@msg area type in a thread cannot use Dynamic Text: {frames} 스레드로 연결된 영역 문자에는 동적 텍스트를 사용할 수 없습니다: {frames} +@msg area type needs a path that encloses an area 영역 문자에는 영역을 둘러싸는 패스가 필요합니다 +@msg area type {_1} is named twice 영역 문자 {_1}이(가) 두 번 지정되었습니다 +@msg array literal too long 배열 리터럴이 너무 깁니다 +@msg arrowAlign must be extend|tip, got {o} arrowAlign은 extend|tip이어야 합니다({o} 입력됨) +@msg arrowScale must be [start, end] arrowScale은 [start, end]여야 합니다 +@msg artboard is a 0-based index artboard는 0부터 시작하는 인덱스입니다 +@msg artboard must be a 0-based index artboard는 0부터 시작하는 인덱스여야 합니다 +@msg artboard {i} doesn't print 아트보드 {i}은(는) 인쇄되지 않습니다 +@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout은 {layout?, columns?, spacing?, rightToLeft?}여야 합니다 +@msg artboardLayout.columns must be 1 or more artboardLayout.columns는 1 이상이어야 합니다 +@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout은 gridByRow, gridByColumn, row 또는 column이어야 합니다 +@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft는 true 또는 false여야 합니다 +@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing은 0–{MAX_SPACING} pt여야 합니다 +@msg artboards must be a non-empty list artboards는 비어 있지 않은 목록이어야 합니다 +@msg artboards must be a number artboards는 숫자여야 합니다 +@msg asset `{_1}` has no art to export 에셋 `{_1}`에 내보낼 아트웍이 없습니다 +@msg assignment to a constant binding 상수 바인딩에 대입했습니다 +@msg at least one row that isn't excluded needs a weight above 0 제외되지 않은 행 중 하나 이상은 가중치가 0보다 커야 합니다 +@msg at most {MAX_KEYWORDS} keywords 키워드는 최대 {MAX_KEYWORDS}개입니다 +@msg at most {MAX_MATCHES} matches are kept 일치 항목은 최대 {MAX_MATCHES}개까지 보관합니다 +@msg at most {MAX_POINTS} points 점은 최대 {MAX_POINTS}개입니다 +@msg at most {MAX_QUEUE} files 파일은 최대 {MAX_QUEUE}개입니다 +@msg at must be [x, y] at은 [x, y]여야 합니다 +@msg at/rect lie outside the canvas at/rect가 캔버스 밖에 있습니다 +@msg attribute must be size|leading|tracking|kerning|baselineShift attribute는 size|leading|tracking|kerning|baselineShift여야 합니다 +@msg attrs must be an object attrs는 개체여야 합니다 +@msg automation path rejected: the folder's name isn't valid text 자동화 경로가 거부되었습니다: 폴더 이름이 올바른 텍스트가 아닙니다 +@msg automation path rejected: {_1} isn't valid text 자동화 경로가 거부되었습니다: {_1}이(가) 올바른 텍스트가 아닙니다 +@msg automation path rejected: {why}: {path} 자동화 경로가 거부되었습니다: {why}: {path} +@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis는 "vertical", "horizontal" 또는 도 단위 각도여야 합니다: `{given}` +@msg backgroundContents must be transparent or white backgroundContents는 transparent 또는 white여야 합니다 +@msg bad AES key 잘못된 AES 키 +@msg bad `runs`: {e} 잘못된 `runs`: {e} +@msg bad arrowScale 잘못된 arrowScale +@msg bad base64 잘못된 base64 +@msg bad color {c} 잘못된 색상 {c} +@msg bad colour 잘못된 색상 +@msg bad colour in `colors` `colors`에 잘못된 색상이 있습니다 +@msg bad colour {v} 잘못된 색상 {v} +@msg bad colour {v} in groupColors groupColors에 잘못된 색상 {v}이(가) 있습니다 +@msg bad cross-reference entry 잘못된 상호 참조 항목 +@msg bad cross-reference table 잘못된 상호 참조 테이블 +@msg bad current colour {v} 잘못된 현재 색상 {v} +@msg bad dataBase64 잘못된 dataBase64 +@msg bad edit 잘못된 편집 +@msg bad fill {v} 잘못된 칠 {v} +@msg bad fit: {e} 잘못된 fit: {e} +@msg bad model {other} (keep, new or document) 잘못된 model {other}(keep, new 또는 document) +@msg bad new colour {v} 잘못된 새 색상 {v} +@msg bad object 잘못된 개체 +@msg bad object number 잘못된 개체 번호 +@msg bad offset 잘못된 오프셋 +@msg bad path data: {e} 잘못된 패스 데이터: {e} +@msg bad point {v}: expected [x, y] 잘못된 점 {v}: [x, y]여야 합니다 +@msg bad point {v}: expected [x, y] or [x, y, pressure] 잘못된 점 {v}: [x, y] 또는 [x, y, pressure]여야 합니다 +@msg bad reference 잘못된 참조 +@msg bad trailer size 잘못된 트레일러 크기 +@msg bad {k} colour 잘못된 {k} 색상 +@msg batches cannot nest 일괄 처리는 중첩할 수 없습니다 +@msg bind something to a variable first 먼저 변수에 항목을 바인딩하세요 +@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed는 0–{MAX_BLEED} pt여야 합니다: 숫자, [top, bottom, left, right] 또는 {{top, bottom, left, right}} +@msg blend must be a mode name blend는 모드 이름이어야 합니다 +@msg both sides of an XPath | must select nodes XPath |의 양쪽은 모두 노드를 선택해야 합니다 +@msg brush must be a name or null brush는 이름 또는 null이어야 합니다 +@msg by must be a number by는 숫자여야 합니다 +@msg can't add page thumbnails: {why} 페이지 미리 보기 이미지를 추가할 수 없습니다: {why} +@msg can't add the editing data for other apps: {why} 다른 앱용 편집 데이터를 추가할 수 없습니다: {why} +@msg can't edit the written PDF: {why} 기록한 PDF를 편집할 수 없습니다: {why} +@msg can't encrypt the PDF: {why} PDF를 암호화할 수 없습니다: {why} +@msg can't linearise the PDF: {why} PDF를 선형화할 수 없습니다: {why} +@msg can't open `{_1}`: {_2} `{_1}`을(를) 열 수 없습니다: {_2} +@msg can't open `{_1}`: {e} `{_1}`을(를) 열 수 없습니다: {e} +@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) `{name}`을(를) 열 수 없습니다: VectorCraft가 읽는 형식이 아닙니다(document.formats 참조) +@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) `{name}`을(를) 가져올 수 없습니다: VectorCraft가 읽는 형식이 아닙니다(document.formats 참조) +@msg can't read back the written PDF: {e} 기록한 PDF를 다시 읽을 수 없습니다: {e} +@msg can't read this Word document: {why} 이 Word 문서를 읽을 수 없습니다: {why} +@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp .{e} 이미지는 기록할 수 없습니다: .png, .jpg, .gif, .webp, .tif 또는 .bmp를 사용하세요 +@msg can't write PDF layers, overprint or invisible text: {why} PDF 레이어, 오버프린트 또는 보이지 않는 텍스트를 기록할 수 없습니다: {why} +@msg can't write the file's copyright: {why} 파일의 저작권을 기록할 수 없습니다: {why} +@msg can't write the file's modification date: {why} 파일의 수정 날짜를 기록할 수 없습니다: {why} +@msg can't write the pages' user units: {why} 페이지의 사용자 단위를 기록할 수 없습니다: {why} +@msg canceled 취소됨 +@msg cannot convert the object to a primitive value 개체를 기본 값으로 변환할 수 없습니다 +@msg cannot convert undefined or null to an object undefined 또는 null은 개체로 변환할 수 없습니다 +@msg cannot distort a zero-size or non-finite bounding box 크기가 0이거나 유한하지 않은 테두리 상자는 왜곡할 수 없습니다 +@msg cannot move an object into itself 개체를 자기 자신 안으로 이동할 수 없습니다 +@msg case must be upper|lower|title|sentence case는 upper|lower|title|sentence여야 합니다 +@msg category `{c}`: one of {_1} category `{c}`: {_1} 중 하나여야 합니다 +@msg center must be [x, y] center는 [x, y]여야 합니다 +@msg character dimensions cannot be represented with the source text transform 원본 텍스트 변형으로는 문자 치수를 나타낼 수 없습니다 +@msg character dimensions cannot be represented with the target text transform 대상 텍스트 변형으로는 문자 치수를 나타낼 수 없습니다 +@msg choose at least one artboard 아트보드를 하나 이상 선택하세요 +@msg choose at least one status 상태를 하나 이상 선택하세요 +@msg clip must be true or false clip은 true 또는 false여야 합니다 +@msg clippingMask must be true or false clippingMask는 true 또는 false여야 합니다 +@msg closed must be a boolean closed는 부울이어야 합니다 +@msg collapsed must be true or false collapsed는 true 또는 false여야 합니다 +@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile: “{profile}”(이)라는 RGB 또는 CMYK 프로파일이 없습니다 +@msg colorMode must be rgb or cmyk, not `{m}` colorMode는 rgb 또는 cmyk여야 합니다: `{m}` +@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel은 {own}(문서의 모드), grayscale 또는 bitmap이어야 합니다 +@msg colour groups can't go into colour groups 색상 그룹은 색상 그룹 안에 넣을 수 없습니다 +@msg colour, mode and spot apply to solid-colour swatches only colour, mode, spot은 단색 견본에만 적용됩니다 +@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read {column}열: `{cell}`은(는) 그래프 데이터 파일의 `%` 열이며, 이 열은 읽지 않습니다 +@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable {column}열: `{cell}`이(가) 가리키는 `{name}`은(는) {_1} 변수입니다 +@msg column {column}: `{cell}` names no variable {column}열: `{cell}`이(가) 가리키는 변수가 없습니다 +@msg column {column}: the variable name is longer than {MAX_NAME} characters {column}열: 변수 이름이 {MAX_NAME}자보다 깁니다 +@msg column: true floats the whole column and takes no onto, above or below column: true는 열 전체를 띄우며 onto, above, below와 함께 쓸 수 없습니다 +@msg command must be a shape.* command command는 shape.* 명령이어야 합니다 +@msg commands[{i}]: missing `command` commands[{i}]: `command`가 없습니다 +@msg composer must be "singleLine" or "everyLine" composer는 "singleLine" 또는 "everyLine"이어야 합니다 +@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes 컴파운드 모양에는 패스, 컴파운드 패스, 그룹, 컴파운드 모양, 문자, 블렌드, 엔벨로프를 넣을 수 있습니다 +@msg compound {_1} must contain exactly one open path to join 컴파운드 {_1}에는 연결할 열린 패스가 정확히 하나 있어야 합니다 +@msg connect needs a path other than the one extended connect에는 연장한 패스가 아닌 다른 패스가 필요합니다 +@msg connect needs id connect에는 id가 필요합니다 +@msg connect path has no open end connect 패스에 열린 끝이 없습니다 +@msg continuation must be an array of path chunks continuation은 패스 조각의 배열이어야 합니다 +@msg continuation requires points continuation에는 points가 필요합니다 +@msg copy needs path: the file to copy into the document's folder copy에는 path가 필요합니다: 문서의 폴더에 복사할 파일입니다 +@msg copy takes images only: relink placed documents where their files are copy는 이미지에만 쓸 수 있습니다: 가져온 문서는 파일이 있는 위치로 다시 연결하세요 +@msg copying a file isn't available on the web 웹에서는 파일을 복사할 수 없습니다 +@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus는 unknown, copyrighted 또는 publicDomain이어야 합니다 +@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl은 URL이어야 합니다(공백 없이 최대 2048자) +@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners는 [x, y] 점 4개여야 합니다(TL, TR, BR, BL) +@msg counterPer `{c}`: item or file counterPer `{c}`: item 또는 file이어야 합니다 +@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}`: art, trim, bleed, media 상자는 PDF 페이지에만 있습니다. "crop" 또는 "bounding"을 사용하세요 +@msg crop rectangle doesn't overlap the image 자르기 사각형이 이미지와 겹치지 않습니다 +@msg cross-reference and object streams aren't supported 상호 참조 스트림과 개체 스트림은 지원하지 않습니다 +@msg damaged compressed data: {e} 압축 데이터가 손상되었습니다: {e} +@msg dash lengths must be numbers, got {v} 대시 길이는 숫자여야 합니다({v} 입력됨) +@msg dash must be a list of lengths or null, got {v} dash는 길이 목록 또는 null이어야 합니다({v} 입력됨) +@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) data 키 `{k}`: 글자, 숫자, -, _, .만 쓸 수 있습니다(name 또는 vc-…는 안 됨) +@msg data must be an object of key: value, not {v} data는 key: value로 이루어진 개체여야 합니다: {v} +@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare 데이터 세트 `{name}`에 라이브러리가 선언하지 않은 `{var}`의 값이 있습니다 +@msg data set `{name}` holds two values for `{var}` 데이터 세트 `{name}`에 `{var}`의 값이 두 개 있습니다 +@msg data set `{name}` is declared twice 데이터 세트 `{name}`이(가) 두 번 선언되었습니다 +@msg data takes at most {MAX_DATA} keys at once data에는 한 번에 키를 최대 {MAX_DATA}개까지 넣을 수 있습니다 +@msg dataset `{name}` already exists 데이터 세트 `{name}`이(가) 이미 있습니다 +@msg dataset `{to}` already exists 데이터 세트 `{to}`이(가) 이미 있습니다 +@msg date `{d}`: YYYY-MM-DD date `{d}`: YYYY-MM-DD 형식이어야 합니다 +@msg decoded data is too large 디코딩한 데이터가 너무 큽니다 +@msg define a variable first 먼저 변수를 정의하세요 +@msg detect must be true or false detect는 true 또는 false여야 합니다 +@msg direct-select anchor points of a path 패스의 앵커 포인트를 직접 선택하세요 +@msg distance must be > 0 distance는 0보다 커야 합니다 +@msg dx and dy give the arrow's direction dx와 dy는 화살표의 방향을 지정합니다 +@msg dx and dy must be numbers dx와 dy는 숫자여야 합니다 +@msg dxf fitTo must be [width, height], positive dxf fitTo는 양수인 [width, height]여야 합니다 +@msg dxf must be an object of DXF import options dxf는 DXF 가져오기 옵션 개체여야 합니다 +@msg dxf scale must be a positive number up to {_1}, not {s} dxf scale은 {_1} 이하의 양수여야 합니다: {s} +@msg dxf unit `{u}`: a unit such as mm, cm, in or pt dxf unit `{u}`: mm, cm, in, pt 같은 단위여야 합니다 +@msg dxf: {e} dxf: {e} +@msg each format is an object {format, scale?, suffix?} 각 형식은 {format, scale?, suffix?} 개체입니다 +@msg each item needs an id 각 항목에는 id가 필요합니다 +@msg each match needs `kind` 각 일치 항목에는 `kind`가 필요합니다 +@msg each match needs a `name` 각 일치 항목에는 `name`이 필요합니다 +@msg each of `fonts` is {family, style?}; 1 to 1000 of them `fonts`의 각 항목은 {family, style?}이며 1개에서 1000개까지 지정합니다 +@msg each of `items` is {kind, item} `items`의 각 항목은 {kind, item}입니다 +@msg each of `items` is {{id, corners?}} with an object id, not {it} `items`의 각 항목은 개체 id가 있는 {{id, corners?}}입니다: {it} +@msg each path chunk must contain 1 to 4096 points 각 패스 조각에는 점이 1개에서 4096개 있어야 합니다 +@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) 각 선택은 {{at: [x, y]}} 또는 {{rect: [x0, y0, x1, y1]}}입니다({v} 입력됨) +@msg each row needs `from` 각 행에는 `from`이 필요합니다 +@msg each stop needs a position 각 정지점에는 위치가 필요합니다 +@msg each subpath needs an anchors array 각 하위 패스에는 anchors 배열이 필요합니다 +@msg each weight must be a number of 0 or more (got {v}) 각 가중치는 0 이상의 숫자여야 합니다({v} 입력됨) +@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType은 easeIn, easeOut, easeInOut 또는 easeOutIn이어야 합니다 +@msg editingData must be true or false editingData는 true 또는 false여야 합니다 +@msg editingData must be true, false or "always" editingData는 true, false 또는 "always"여야 합니다 +@msg edits overlap 편집 범위가 겹칩니다 +@msg effects of objects whose appearances differ can't be moved together 모양새가 서로 다른 개체의 효과는 함께 이동할 수 없습니다 +@msg empty header in column {_1} {_1}열의 머리글이 비어 있습니다 +@msg encoding `{_1}`: utf8 or utf16 encoding `{_1}`: utf8 또는 utf16이어야 합니다 +@msg ends must be two of "first" and "last" ends는 "first"와 "last" 중에서 고른 두 개여야 합니다 +@msg every numbered name for the result is taken 결과에 붙일 번호 이름이 모두 사용 중입니다 +@msg expanded SVG styles exceed the import size limit 확장한 SVG 스타일이 가져오기 크기 한도를 넘습니다 +@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes 내보내기 사전 설정 파일이 {MAX_PRESET_FILE_BYTES}바이트보다 큽니다 +@msg exportText must be editable or appearance exportText는 editable 또는 appearance여야 합니다 +@msg exportType must be movieClip or graphic, not `{s}` exportType은 movieClip 또는 graphic이어야 합니다: `{s}` +@msg extend needs id extend에는 id가 필요합니다 +@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges에는 태그를 최대 {MAX_FEATURE_CHANGES}개까지 나열할 수 있습니다 +@msg featureChanges must be a list of tags featureChanges는 태그 목록이어야 합니다 +@msg features must be a list of tags features는 태그 목록이어야 합니다 +@msg fidelity must be 0..100 fidelity는 0..100이어야 합니다 +@msg files must be {name, dataBase64} files는 {name, dataBase64}여야 합니다 +@msg finally table {t} out of range finally 테이블 {t}이(가) 범위를 벗어났습니다 +@msg fit must be none|autoHeight|shrinkText|block, got {id} fit은 none|autoHeight|shrinkText|block이어야 합니다({id} 입력됨) +@msg flags cannot be given with a RegExp object RegExp 개체와 함께 플래그를 지정할 수 없습니다 +@msg flattener: {e} 병합: {e} +@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}`: high, medium, low 또는 저장된 사전 설정이어야 합니다(flattener.presets.list 참조) +@msg flipProfile must be along|across, got {o} flipProfile은 along|across여야 합니다({o} 입력됨) +@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}`: none, rows, format, item 또는 itemFormat이어야 합니다 +@msg folders deeper than {MAX_BATCH_DEPTH} levels 폴더가 {MAX_BATCH_DEPTH}단계보다 깊습니다 +@msg folders deeper than {MAX_DEPTH} levels 폴더가 {MAX_DEPTH}단계보다 깊습니다 +@msg font family `{family}` is not available 글꼴 모음 `{family}`을(를) 사용할 수 없습니다 +@msg font family `{tf}` is not available 글꼴 모음 `{tf}`을(를) 사용할 수 없습니다 +@msg format `{f}`: pdf or postscript format `{f}`: pdf 또는 postscript여야 합니다 +@msg format `{f}`: text or json format `{f}`: text 또는 json이어야 합니다 +@msg format is `{format}` format은 `{format}`입니다 +@msg formats must be an array of rows, not {v} formats는 행의 배열이어야 합니다: {v} +@msg frequency must be between 1 and 300 lines per inch frequency는 인치당 1줄에서 300줄 사이여야 합니다 +@msg from and to must be finite from과 to는 유한해야 합니다 +@msg from must be [x0, y0, x1, y1] from은 [x0, y0, x1, y1]이어야 합니다 +@msg function declaration index out of range 함수 선언 인덱스가 범위를 벗어났습니다 +@msg give `before` or `after`, not both `before`와 `after` 중 하나만 지정하세요 +@msg give `char`, `text`, `code` or `unicode` `char`, `text`, `code` 또는 `unicode`를 지정하세요 +@msg give `colors`, or `color` and `rule` `colors`를 지정하거나 `color`와 `rule`을 지정하세요 +@msg give `count` or `spacing`, not both `count`와 `spacing` 중 하나만 지정하세요 +@msg give `event`, such as onclick onclick 같은 `event`를 지정하세요 +@msg give `file`, the JavaScript file's path or URL JavaScript 파일의 경로나 URL인 `file`을 지정하세요 +@msg give `fill` and/or `stroke` `fill`이나 `stroke`, 또는 둘 다 지정하세요 +@msg give `fill`, `stroke` or both `fill`이나 `stroke`, 또는 둘 다 지정하세요 +@msg give `folder` `folder`를 지정하세요 +@msg give `fonts`, or open a document `fonts`를 지정하거나 문서를 여세요 +@msg give `from` or `indices`, not both `from`과 `indices` 중 하나만 지정하세요 +@msg give `id` (no text is being edited) `id`를 지정하세요(편집 중인 텍스트가 없습니다) +@msg give `id` and `anchors`, or `items`, not both `id`와 `anchors`를 지정하거나 `items`를 지정하세요. 둘 다 지정할 수는 없습니다 +@msg give `id` or `guide`, not both `id`와 `guide` 중 하나만 지정하세요 +@msg give `length` or `to` `length` 또는 `to`를 지정하세요 +@msg give `matches`: [{kind, name, target}] `matches`를 지정하세요: [{kind, name, target}] +@msg give `name` (or edit a pattern) `name`을 지정하세요(또는 패턴을 편집하세요) +@msg give `name` or `names` `name` 또는 `names`를 지정하세요 +@msg give `name`, an SVG filter's name SVG 필터의 이름인 `name`을 지정하세요 +@msg give `path` (a .wasm file) or `dataBase64` (the module) `path`(.wasm 파일) 또는 `dataBase64`(모듈)를 지정하세요 +@msg give `path`, `data` or `dataBase64` `path`, `data` 또는 `dataBase64`를 지정하세요 +@msg give `path`, `data`, `dataBase64` or `csv` `path`, `data`, `dataBase64` 또는 `csv`를 지정하세요 +@msg give `path`, `xml` or `dataBase64` `path`, `xml` 또는 `dataBase64`를 지정하세요 +@msg give `path`, an SVG file, or `svg`, SVG text SVG 파일인 `path` 또는 SVG 텍스트인 `svg`를 지정하세요 +@msg give `script`, the JavaScript the event runs ("" removes the event) 이벤트가 실행할 JavaScript인 `script`를 지정하세요(""는 이벤트를 제거합니다) +@msg give `text` (the reading) or `remove` `text`(읽는 법) 또는 `remove`를 지정하세요 +@msg give `x` and `y` (and a `tolerance` of 0 or more) `x`와 `y`를 지정하세요(`tolerance`는 0 이상) +@msg give `xml`, one SVG element SVG 요소 하나인 `xml`을 지정하세요 +@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments mode를 지정하거나 r/g/b, c/m/y/k, gray 또는 tint 조정값을 지정하세요 +@msg give a name, or paint the active proxy with a saved preset 이름을 지정하거나 활성 프록시를 저장된 사전 설정으로 칠하세요 +@msg give a name, or select a stroke with a saved profile 이름을 지정하거나 저장된 프로파일이 있는 선을 선택하세요 +@msg give a pattern name or select an object painted with a pattern 패턴 이름을 지정하거나 패턴으로 칠한 개체를 선택하세요 +@msg give at least one of clip, invert, disabled, linked clip, invert, disabled, linked 중 하나 이상을 지정하세요 +@msg give color, none, swatch or gradient color, none, swatch 또는 gradient를 지정하세요 +@msg give dataBase64 dataBase64를 지정하세요 +@msg give either at (a click) or area (a path drawn around the overlaps) at(클릭) 또는 area(겹친 부분 둘레에 그린 패스) 중 하나를 지정하세요 +@msg give exactly one of `at`, `rect` and `points` `at`, `rect`, `points` 중 정확히 하나를 지정하세요 +@msg give exactly one of `path`, `data` and `dataBase64` `path`, `data`, `dataBase64` 중 정확히 하나를 지정하세요 +@msg give formats or a preset, not both formats와 사전 설정 중 하나만 지정하세요 +@msg give index or indexes, not both index와 indexes 중 하나만 지정하세요 +@msg give newName or delete newName 또는 delete를 지정하세요 +@msg give offset or by offset 또는 by를 지정하세요 +@msg give one element 요소 하나를 지정하세요 +@msg give one of `to`, `rect` or `at` `to`, `rect`, `at` 중 하나를 지정하세요 +@msg give one of assets, layers, selection or fullDocument assets, layers, selection, fullDocument 중 하나를 지정하세요 +@msg give only one of onto, above, below, icon and a column place onto, above, below, icon, 열 위치 중 하나만 지정하세요 +@msg give opacity or blend opacity 또는 blend를 지정하세요 +@msg give origin: [x, y], or reset: true origin: [x, y] 또는 reset: true를 지정하세요 +@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note overprintFill, overprintStroke, showCenter, imageMap, url 또는 note를 지정하세요 +@msg give path (a file) or folder path(파일) 또는 folder를 지정하세요 +@msg give path, or name and dataBase64 path를 지정하거나 name과 dataBase64를 지정하세요 +@msg give paths or files paths 또는 files를 지정하세요 +@msg give rows, rowHeight, columns or columnWidth rows, rowHeight, columns 또는 columnWidth를 지정하세요 +@msg give svg, or dataBase64 of an SVG or SVGZ file svg를 지정하거나 SVG 또는 SVGZ 파일의 dataBase64를 지정하세요 +@msg give text text를 지정하세요 +@msg give the asset a name 에셋에 이름을 지정하세요 +@msg give the preset a `name` 사전 설정에 `name`을 지정하세요 +@msg give the preset a name 사전 설정에 이름을 지정하세요 +@msg give the preset a name (1–64 characters) 사전 설정에 이름을 지정하세요(1–64자) +@msg give the settings to change, e.g. {"formats"}]} 변경할 설정을 지정하세요(예: {"formats"}]}) +@msg give the style a name 스타일에 이름을 지정하세요 +@msg give two or more style `names` 스타일 `names`를 두 개 이상 지정하세요 +@msg give two or more swatches 견본을 두 개 이상 지정하세요 +@msg give x, y, or a handle's length or angle x, y 또는 핸들의 길이나 각도를 지정하세요 +@msg give {key, value} or {values} {key, value} 또는 {values}를 지정하세요 +@msg global must be true or false global은 true 또는 false여야 합니다 +@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) 글자의 스타일(예: 모두 대문자, 작은 대문자 또는 숫자 체계)에서는 {ch}에 글리프 {_1}을(를) 그리지 않습니다 +@msg glyph {g} is not an alternate of {ch} in {_1} {_2} 글리프 {g}은(는) {_1} {_2}에서 {ch}의 대체 글리프가 아닙니다 +@msg gradient must be "objects" or "mesh" gradient는 "objects" 또는 "mesh"여야 합니다 +@msg grays must be "profile" or "black", not `{g}` grays는 "profile" 또는 "black"이어야 합니다: `{g}` +@msg gridColors must be a preset name or two colours gridColors는 사전 설정 이름 또는 색상 두 개여야 합니다 +@msg gridSize must be small, medium or large gridSize는 small, medium 또는 large여야 합니다 +@msg guide {g} is not selected 안내선 {g}이(가) 선택되어 있지 않습니다 +@msg handle must be 0..3 handle은 0..3이어야 합니다 +@msg handler at {_1} expects {_2} values but its range starts with {d} {_1}의 처리기는 값 {_2}개가 필요하지만 그 범위는 {d}개로 시작합니다 +@msg handler depth too large 처리기 깊이가 너무 큽니다 +@msg handler range {_1}..{_2} out of range 처리기 범위 {_1}..{_2}이(가) 범위를 벗어났습니다 +@msg hiddenLayers must be true or false hiddenLayers는 true 또는 false여야 합니다 +@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v}: "layer"(기본값), "omit" 또는 "visible"이어야 합니다 +@msg highlight a layer to merge into 병합해 넣을 레이어를 강조 표시하세요 +@msg highlight two or more rows in one layer or group 한 레이어나 그룹에서 행을 두 개 이상 강조 표시하세요 +@msg highlight two or more rows to merge 병합할 행을 두 개 이상 강조 표시하세요 +@msg hvcC hvcC +@msg icon docks a panel in the icon column: use window.panel.dock icon은 패널을 아이콘 열에 도킹합니다: window.panel.dock을 사용하세요 +@msg id must be a dimension id id는 치수 id여야 합니다 +@msg image has no bounds 이미지에 경계가 없습니다 +@msg image {_1} is linked already 이미지 {_1}은(는) 이미 연결되어 있습니다 +@msg imageMap `{m}`: none, client or server imageMap `{m}`: none, client 또는 server여야 합니다 +@msg images in a spot ink whose samples couldn't be read were imported in RGB 샘플을 읽을 수 없는 별색 잉크 이미지는 RGB로 가져왔습니다 +@msg images past the most pixels one file may hold were left out 파일 하나에 담을 수 있는 최대 픽셀 수를 넘는 이미지는 제외되었습니다 +@msg images too large to read were left out 너무 커서 읽을 수 없는 이미지는 제외되었습니다 +@msg index out of range 인덱스가 범위를 벗어났습니다 +@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes에는 항목을 최대 {MAX_GUIDE_INDEXES}개까지 넣을 수 있습니다 +@msg indexes must be an array of guide indexes indexes는 안내선 인덱스의 배열이어야 합니다 +@msg instances must be 1..1000 instances는 1..1000이어야 합니다 +@msg instruction out of range 명령이 범위를 벗어났습니다 +@msg instruction {pc} ({op}) pops below the stack 명령 {pc}({op})이(가) 스택 바닥 아래에서 값을 꺼냅니다 +@msg instruction {pc} reached with stack depths {seen} and {d} 명령 {pc}에 스택 깊이 {seen}와(과) {d}(으)로 도달했습니다 +@msg invalid `color` 잘못된 `color` +@msg invalid anchor index {v}: expected a non-negative integer 잘못된 앵커 인덱스 {v}: 음이 아닌 정수여야 합니다 +@msg invalid anchor reference {v}: expected [subpath, anchor] 잘못된 앵커 참조 {v}: [subpath, anchor]여야 합니다 +@msg invalid angle 잘못된 각도 +@msg invalid array length 잘못된 배열 길이 +@msg invalid brush: {e} 잘못된 브러시: {e} +@msg invalid color 잘못된 색상 +@msg invalid colour {c} 잘못된 색상 {c} +@msg invalid delta 잘못된 델타 +@msg invalid group 잘못된 그룹 +@msg invalid object id {v}: expected a non-negative integer 잘못된 개체 id {v}: 음이 아닌 정수여야 합니다 +@msg invalid point 잘못된 점 +@msg invalid regular expression flag '{_1}' 잘못된 정규식 플래그 '{_1}' +@msg invalid regular expression: {m} 잘못된 정규식: {m} +@msg invalid size 잘못된 크기 +@msg invalid t 잘못된 t +@msg invalid time value 잘못된 시간 값 +@msg invalid tolerance 잘못된 허용치 +@msg is a confirm's answer, whose `value` must be true or false confirm의 응답이며 `value`은(는) true 또는 false여야 합니다 +@msg is a prompt's answer, whose `value` must be a string or null prompt의 응답이며 `value`은(는) 문자열 또는 null이어야 합니다 +@msg is an alert's answer, whose `value` must be "ok" alert의 응답이며 `value`은(는) "ok"여야 합니다 +@msg is longer than 16 KiB 16 KiB보다 깁니다 +@msg it draws its image through a clipping path 이미지를 클리핑 패스를 통해 그립니다 +@msg it draws no image 이미지를 그리지 않습니다 +@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes {MAX_DICTIONARY_FILE_BYTES}바이트보다 큽니다 +@msg it isn't a file 파일이 아닙니다 +@msg it names {_1} plates, and at most {MAX_PLATES} are read 색판 {_1}개를 지정하지만 최대 {MAX_PLATES}개까지만 읽습니다 +@msg its art is too long or too detailed for a head 아트웍이 화살촉으로 쓰기에는 너무 길거나 세밀합니다 +@msg its art paints nothing 아트웍이 아무것도 칠하지 않습니다 +@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) 아트보드가 모두 캔버스 밖에 있습니다(원점에서 {_1} pt 넘게 떨어져 있음) +@msg its clipping masks are nested too deeply 클리핑 마스크가 너무 깊게 중첩되어 있습니다 +@msg its image is too large to read 이미지가 너무 커서 읽을 수 없습니다 +@msg its metadata has no description 메타데이터에 설명이 없습니다 +@msg its metadata has no direct length 메타데이터에 직접 길이가 없습니다 +@msg its plates are too large: more than {MAX_SAMPLES} pixels together 색판이 너무 큽니다: 모두 합쳐 {MAX_SAMPLES} 픽셀을 넘습니다 +@msg its plates are too large: {_1} plates of {w} × {h} pixels 색판이 너무 큽니다: {w} × {h} 픽셀 색판 {_1}개 +@msg its plates can't be kept together 색판을 함께 보관할 수 없습니다 +@msg its shapes could not be combined into one 모양을 하나로 결합할 수 없습니다 +@msg join two different paths 서로 다른 두 패스를 연결하세요 +@msg jump to {pc}, past the end 끝을 지나 {pc}(으)로 점프합니다 +@msg justify must be auto|left|center|right|justifyAll justify는 auto|left|center|right|justifyAll이어야 합니다 +@msg kbps must be a positive number, not {kbps} kbps는 양수여야 합니다: {kbps} +@msg keywords must be a list of words or a comma-separated string keywords는 단어 목록이거나 쉼표로 구분한 문자열이어야 합니다 +@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords는 제어 문자 없이 최대 {MAX_KEYWORD}자여야 합니다 +@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind는 1, 2 또는 3이어야 합니다. 아니면 사전 설정 `name`을 지정하세요(perspective.presets.list 참조) +@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind는 graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle 또는 paraStyle이어야 합니다 +@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout은 "on", "off", "neutral" 또는 부울이어야 합니다: {v} +@msg layer `{name}` has no art to export 레이어 `{name}`에 내보낼 아트웍이 없습니다 +@msg layer must name a layer or sublayer layer에는 레이어나 하위 레이어를 지정해야 합니다 +@msg layers can't be intertwined: select the objects in them 레이어는 교차할 수 없습니다: 레이어 안의 개체를 선택하세요 +@msg layers go only at the top level or in layers, objects in layers and groups 레이어는 최상위나 레이어 안에만, 개체는 레이어와 그룹 안에만 둘 수 있습니다 +@msg layers must be true or false layers는 true 또는 false여야 합니다 +@msg layout must be gridByRow, gridByColumn, row or column layout은 gridByRow, gridByColumn, row 또는 column이어야 합니다 +@msg leading must be a number or "auto" leading은 숫자 또는 "auto"여야 합니다 +@msg left/right must be non-negative widths left/right는 음이 아닌 너비여야 합니다 +@msg legacyNameEncoding must be one of {_1} legacyNameEncoding은 {_1} 중 하나여야 합니다 +@msg level `{t}`: 2 or 3 level `{t}`: 2 또는 3이어야 합니다 +@msg line {line}: more than {MAX_CSV_COLS} columns {line}행: 열이 {MAX_CSV_COLS}개보다 많습니다 +@msg line {line}: unclosed quote {line}행: 따옴표가 닫히지 않았습니다 +@msg line {line}: value for `{var}` must match its variable kind {line}행: `{var}`의 값은 변수 종류와 맞아야 합니다 +@msg lineEndings `{other}`: lf or crlf lineEndings `{other}`: lf 또는 crlf여야 합니다 +@msg loading profiles from disk isn't available in the browser 브라우저에서는 디스크에서 프로파일을 불러올 수 없습니다 +@msg localize: the first argument must be a localization object or a string localize: 첫 번째 인수는 지역화 개체 또는 문자열이어야 합니다 +@msg margin: a {m} pt margin leaves no imageable area on the paper margin: 여백이 {m} pt이면 용지에 인쇄 가능 영역이 남지 않습니다 +@msg matrix must be finite and invertible, depth finite matrix는 유한하고 역행렬이 있어야 하며 depth는 유한해야 합니다 +@msg matte: {e} matte: {e} +@msg member is for order front, forward, backward or back member는 order가 front, forward, backward 또는 back일 때 씁니다 +@msg missing `artboards` `artboards`가 없습니다 +@msg missing `at` [x, y] `at` [x, y]가 없습니다 +@msg missing `attrs` `attrs`가 없습니다 +@msg missing `color` or `opacity` `color` 또는 `opacity`가 없습니다 +@msg missing `colors` `colors`가 없습니다 +@msg missing `commands` `commands`가 없습니다 +@msg missing `destinationFolder` `destinationFolder`가 없습니다 +@msg missing `effect` id `effect` id가 없습니다 +@msg missing `family` `family`가 없습니다 +@msg missing `find` `find`가 없습니다 +@msg missing `gid` (a glyph id, or null for the default glyph) `gid`가 없습니다(글리프 id, 기본 글리프는 null) +@msg missing `group` `group`이 없습니다 +@msg missing `id` `id`가 없습니다 +@msg missing `id`: give one, or select one type object `id`가 없습니다: id를 지정하거나 문자 개체 하나를 선택하세요 +@msg missing `index` (a point index, or null to clear) `index`가 없습니다(점 인덱스, 지우려면 null) +@msg missing `index` (a stop index, or null to clear) `index`가 없습니다(정지점 인덱스, 지우려면 null) +@msg missing `index` (an item index or null) `index`가 없습니다(항목 인덱스 또는 null) +@msg missing `item` `item`이 없습니다 +@msg missing `library` `library`가 없습니다 +@msg missing `library` (see graphicStyle.libraries) `library`가 없습니다(graphicStyle.libraries 참조) +@msg missing `library` (see swatch.library.list) `library`가 없습니다(swatch.library.list 참조) +@msg missing `map` (rows [{from, to}] or {current}) `map`이 없습니다(행 [{from, to}] 또는 {current}) +@msg missing `matrix` [a,b,c,d,e,f] `matrix` [a,b,c,d,e,f]가 없습니다 +@msg missing `name` (see gradient.presets.list) `name`이 없습니다(gradient.presets.list 참조) +@msg missing `offset` (a distance in points) `offset`이 없습니다(포인트 단위 거리) +@msg missing `path` `path`가 없습니다 +@msg missing `path` (a path object id) `path`가 없습니다(패스 개체 id) +@msg missing `picks` `picks`가 없습니다 +@msg missing `rule` `rule`이 없습니다 +@msg missing `source` id `source` id가 없습니다 +@msg missing `start` (the byte offset of a character) `start`가 없습니다(글자의 바이트 오프셋) +@msg missing `status`: linked, embedded, missing or modified `status`가 없습니다: linked, embedded, missing 또는 modified +@msg missing `target` id `target` id가 없습니다 +@msg missing `text` `text`가 없습니다 +@msg missing `to` `to`가 없습니다 +@msg missing `to` (an index) `to`가 없습니다(인덱스) +@msg missing `to`: a non-negative integer `to`가 없습니다: 음이 아닌 정수 +@msg missing `type` `type`이 없습니다 +@msg missing `value` `value`가 없습니다 +@msg missing `word` `word`가 없습니다 +@msg missing `{key}` `{key}`이(가) 없습니다 +@msg missing `{k}` `{k}`이(가) 없습니다 +@msg missing anchor 앵커가 없습니다 +@msg missing anchors anchors가 없습니다 +@msg missing anchors [[subpath, anchor]…] anchors [[subpath, anchor]…]가 없습니다 +@msg missing array `{key}` 배열 `{key}`이(가) 없습니다 +@msg missing case case가 없습니다 +@msg missing color color가 없습니다 +@msg missing commands commands가 없습니다 +@msg missing destination folder 대상 폴더가 없습니다 +@msg missing from from이 없습니다 +@msg missing group group이 없습니다 +@msg missing id or ids id 또는 ids가 없습니다 +@msg missing ids ids가 없습니다 +@msg missing indexes indexes가 없습니다 +@msg missing matrix [a,b,c,d,e,f] matrix [a,b,c,d,e,f]가 없습니다 +@msg missing name name이 없습니다 +@msg missing op op가 없습니다 +@msg missing or invalid `color` `color`가 없거나 잘못되었습니다 +@msg missing or unknown `tool` `tool`이 없거나 알 수 없는 값입니다 +@msg missing point 점이 없습니다 +@msg missing point [x,y] 점 [x,y]가 없습니다 +@msg missing point `{key}` ([x, y]) 점 `{key}`([x, y])이(가) 없습니다 +@msg missing points points가 없습니다 +@msg missing points [[x,y]…] points [[x,y]…]가 없습니다 +@msg missing segment 세그먼트가 없습니다 +@msg missing stops stops가 없습니다 +@msg missing subpaths subpaths가 없습니다 +@msg missing target target이 없습니다 +@msg missing to to가 없습니다 +@msg missing x, y x, y가 없습니다 +@msg mode is set, add or toggle, not `{other}` mode는 set, add 또는 toggle입니다: `{other}` +@msg mode must be "cmyk" or "rgb" mode는 "cmyk" 또는 "rgb"여야 합니다 +@msg mode must be area|onPath, got {o} mode는 area|onPath여야 합니다({o} 입력됨) +@msg more than 10000 linked files: package dependencies in smaller parts 연결된 파일이 10000개보다 많습니다: 종속 파일을 더 작게 나누어 패키지하세요 +@msg more than 256 commands 명령이 256개보다 많습니다 +@msg more than {MAX_BATCH_FILES} data sets 데이터 세트가 {MAX_BATCH_FILES}개보다 많습니다 +@msg more than {MAX_BATCH_FILES} files 파일이 {MAX_BATCH_FILES}개보다 많습니다 +@msg more than {MAX_CSV_ROWS} data rows 데이터 행이 {MAX_CSV_ROWS}개보다 많습니다 +@msg more than {MAX_FILES} images 이미지가 {MAX_FILES}개보다 많습니다 +@msg more than {MAX_IMPORT} presets 사전 설정이 {MAX_IMPORT}개보다 많습니다 +@msg moved must be [[x,y]…] moved는 [[x,y]…]여야 합니다 +@msg must be an object with `kind` and `value` `kind`와 `value`가 있는 개체여야 합니다 +@msg must have `kind` alert, confirm or prompt `kind`가 alert, confirm 또는 prompt여야 합니다 +@msg must have exactly `kind` and `value` `kind`와 `value`만 있어야 합니다 +@msg name (a toolbar's name) is required name(도구 막대 이름)이 필요합니다 +@msg name (a toolbar's name) must be a string name(도구 막대 이름)은 문자열이어야 합니다 +@msg name must be a toolbar's name name은 도구 막대 이름이어야 합니다 +@msg names is empty names가 비어 있습니다 +@msg names must be an array of symbol names names는 심볼 이름의 배열이어야 합니다 +@msg need `anchor` or `segment` + `t` `anchor` 또는 `segment` + `t`가 필요합니다 +@msg need at least one anchor 앵커가 하나 이상 필요합니다 +@msg need at least one point 점이 하나 이상 필요합니다 +@msg need at least two points 점이 두 개 이상 필요합니다 +@msg needs a closing quote after the name 이름 뒤에 닫는 따옴표가 필요합니다 +@msg needs a name 이름이 필요합니다 +@msg needs a name of at most 64 bytes 최대 64바이트의 이름이 필요합니다 +@msg needs a name of letters, digits and _, or a quoted name 글자, 숫자, _로 이루어진 이름이나 따옴표로 묶은 이름이 필요합니다 +@msg needs a name without control characters 제어 문자가 없는 이름이 필요합니다 +@msg needs points points가 필요합니다 +@msg nesting too deep 중첩이 너무 깊습니다 +@msg nineSlice must be true or false nineSlice는 true 또는 false여야 합니다 +@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides는 [left, top, right, bottom]이어야 하고, 각 값은 -16..16이며 left + right < 1, top + bottom < 1이어야 합니다 +@msg no HP-GL format HP-GL 형식이 없습니다 +@msg no New Document profile called `{shown}` ({_1}) `{shown}`(이)라는 새 문서 프로파일이 없습니다({_1}) +@msg no PDF preset named `{n}` `{n}` PDF 사전 설정이 없습니다 +@msg no SVG filter at index {k} 인덱스 {k}에 SVG 필터가 없습니다 +@msg no action set is current 현재 액션 세트가 없습니다 +@msg no active data set 활성 데이터 세트가 없습니다 +@msg no appearance item {index} 모양새 항목 {index}이(가) 없습니다 +@msg no arrowheads file here 여기에는 화살촉 파일이 없습니다 +@msg no artboard {_1} 아트보드 {_1}이(가) 없습니다 +@msg no artboard {artboard} (0-based; the document has {_1}) 아트보드 {artboard}이(가) 없습니다(0부터 시작, 문서의 아트보드 수: {_1}) +@msg no asset {id} 에셋 {id}이(가) 없습니다 +@msg no asset {id} (see assets.list) 에셋 {id}이(가) 없습니다(assets.list 참조) +@msg no assets to export 내보낼 에셋이 없습니다 +@msg no assets yet: collect art for export first (assets.add) 아직 에셋이 없습니다: 먼저 내보낼 아트웍을 수집하세요(assets.add) +@msg no brush named `{n}` `{n}` 브러시가 없습니다 +@msg no character at byte {start} 바이트 {start}에 글자가 없습니다 +@msg no character has code {code} 코드가 {code}인 글자가 없습니다 +@msg no colour 색상이 없습니다 +@msg no colour group `{group}` 색상 그룹 `{group}`이(가) 없습니다 +@msg no colour group `{name}` 색상 그룹 `{name}`이(가) 없습니다 +@msg no custom arrowhead {name} (see stroke.arrowheads.list) 사용자 정의 화살촉 {name}이(가) 없습니다(stroke.arrowheads.list 참조) +@msg no data sets 데이터 세트가 없습니다 +@msg no dataset named `{from}` `{from}` 데이터 세트가 없습니다 +@msg no dataset named `{name}` `{name}` 데이터 세트가 없습니다 +@msg no datasets 데이터 세트가 없습니다 +@msg no design named `{name}` `{name}` 디자인이 없습니다 +@msg no design named `{n}` `{n}` 디자인이 없습니다 +@msg no design named `{old}` `{old}` 디자인이 없습니다 +@msg no encoder for {_1} yet 아직 {_1} 인코더가 없습니다 +@msg no export preset `{name}` (see document.exportPresets.list) 내보내기 사전 설정 `{name}`이(가) 없습니다(document.exportPresets.list 참조) +@msg no file to open 열 파일이 없습니다 +@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder 폴더가 없습니다: `path`를 전달하거나 환경 설정 › 플러그인 & 스크래치 디스크 › 추가 플러그인 폴더를 설정하세요 +@msg no fonts to look for: they are all available 찾을 글꼴이 없습니다: 모든 글꼴을 사용할 수 있습니다 +@msg no formats to export 내보낼 형식이 없습니다 +@msg no gradient preset `{name}` (see gradient.presets.list) 그레이디언트 사전 설정 `{name}`이(가) 없습니다(gradient.presets.list 참조) +@msg no graphic style `{n}` in `{_1}` `{_1}`에 그래픽 스타일 `{n}`이(가) 없습니다 +@msg no graphic style library `{key}` (see graphicStyle.libraries) 그래픽 스타일 라이브러리 `{key}`이(가) 없습니다(graphicStyle.libraries 참조) +@msg no guide selected 선택한 안내선이 없습니다 +@msg no guide selected (or give an index) 선택한 안내선이 없습니다(또는 인덱스를 지정하세요) +@msg no guide {i} 안내선 {i}이(가) 없습니다 +@msg no images in the folder 폴더에 이미지가 없습니다 +@msg no instructions 명령이 없습니다 +@msg no item at index {_1} 인덱스 {_1}에 항목이 없습니다 +@msg no item at index {from} 인덱스 {from}에 항목이 없습니다 +@msg no item at one of the indices {idx} 인덱스 {idx} 중 하나에 항목이 없습니다 +@msg no layer 레이어가 없습니다 +@msg no layer to rename 이름을 바꿀 레이어가 없습니다 +@msg no layer {id} (see layer.list) 레이어 {id}이(가) 없습니다(layer.list 참조) +@msg no layers to export 내보낼 레이어가 없습니다 +@msg no library `{key}` 라이브러리 `{key}`이(가) 없습니다 +@msg no metafile format 메타파일 형식이 없습니다 +@msg no object {_1} 개체 {_1}이(가) 없습니다 +@msg no objects 개체가 없습니다 +@msg no parent layer 상위 레이어가 없습니다 +@msg no perspective grid preset named `{b}` (see perspective.presets.list) `{b}` 원근 격자 사전 설정이 없습니다(perspective.presets.list 참조) +@msg no perspective grid preset named `{n}` `{n}` 원근 격자 사전 설정이 없습니다 +@msg no point selected (give `index`) 선택한 점이 없습니다(`index`를 지정하세요) +@msg no preset called `{from}` (see file.newPresets) `{from}` 사전 설정이 없습니다(file.newPresets 참조) +@msg no preset called `{name}` (see file.newPresets) `{name}` 사전 설정이 없습니다(file.newPresets 참조) +@msg no preset named `{n}` `{n}` 사전 설정이 없습니다 +@msg no print preset named `{b}` (see print.presets.list) `{b}` 인쇄 사전 설정이 없습니다(print.presets.list 참조) +@msg no print preset named `{name}` (presets: {_1}) `{name}` 인쇄 사전 설정이 없습니다(사전 설정: {_1}) +@msg no print preset named `{n}` `{n}` 인쇄 사전 설정이 없습니다 +@msg no profile named `{name}` `{name}` 프로파일이 없습니다 +@msg no random numbers ({e}) 난수를 얻을 수 없습니다({e}) +@msg no readable files in the folder 폴더에 읽을 수 있는 파일이 없습니다 +@msg no recovery copy `{f}` (see file.recovery.list) 복구 복사본 `{f}`이(가) 없습니다(file.recovery.list 참조) +@msg no rows 행이 없습니다 +@msg no saved PDF preset named `{name}` `{name}`(이)라는 저장된 PDF 사전 설정이 없습니다 +@msg no saved export preset `{name}` 저장된 내보내기 사전 설정 `{name}`이(가) 없습니다 +@msg no saved gradient preset called {name} {name}(이)라는 저장된 그레이디언트 사전 설정이 없습니다 +@msg no saved gradient presets 저장된 그레이디언트 사전 설정이 없습니다 +@msg no saved preset called `{name}` `{name}`(이)라는 저장된 사전 설정이 없습니다 +@msg no saved presets to export (name [Default] in `names` to export it) 내보낼 저장된 사전 설정이 없습니다([Default]를 내보내려면 `names`에 지정하세요) +@msg no saved presets to export (name built-in ones in `names`) 내보낼 저장된 사전 설정이 없습니다(기본 제공 사전 설정은 `names`에 지정하세요) +@msg no saved print preset named `{name}` `{name}`(이)라는 저장된 인쇄 사전 설정이 없습니다 +@msg no saved profile called {name} {name}(이)라는 저장된 프로파일이 없습니다 +@msg no saved selection `{name}` 저장된 선택 영역 `{name}`이(가) 없습니다 +@msg no saved theme called {name} (see colorTheme.list) {name}(이)라는 저장된 테마가 없습니다(colorTheme.list 참조) +@msg no saved views 저장된 보기가 없습니다 +@msg no selected or given object has a visible 3D effect 선택하거나 지정한 개체 중에 보이는 3D 효과가 있는 개체가 없습니다 +@msg no series {i} 계열 {i}이(가) 없습니다 +@msg no slice {n} in the image (see slice.list) 이미지에 슬라이스 {n}이(가) 없습니다(slice.list 참조) +@msg no source art 원본 아트웍이 없습니다 +@msg no stop {i} (the gradient has {n}) 정지점 {i}이(가) 없습니다(그레이디언트의 정지점 수: {n}) +@msg no style `{name}` 스타일 `{name}`이(가) 없습니다 +@msg no such artboard {v} 해당 아트보드가 없습니다: {v} +@msg no such assets (see assets.list) 해당 에셋이 없습니다(assets.list 참조) +@msg no such frame anchor, or the frame would reach past the canvas 해당 프레임 앵커가 없거나 프레임이 캔버스를 벗어납니다 +@msg no such objects 해당 개체가 없습니다 +@msg no such path anchor, or the path would reach past the canvas 해당 패스 앵커가 없거나 패스가 캔버스를 벗어납니다 +@msg no such rows 해당 행이 없습니다 +@msg no swatch `{keep}` 견본 `{keep}`이(가) 없습니다 +@msg no swatch `{n}` 견본 `{n}`이(가) 없습니다 +@msg no swatch `{n}` (swatches and colour groups move separately) 견본 `{n}`이(가) 없습니다(견본과 색상 그룹은 따로 이동합니다) +@msg no swatch library `{key}` (see swatch.library.list) 견본 라이브러리 `{key}`이(가) 없습니다(swatch.library.list 참조) +@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) 견본 라이브러리 `{key}`이(가) 없습니다(swatch.library.list 참조, "document": 문서의 견본) +@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) 견본 라이브러리 `{key}`이(가) 없습니다(swatch.library.list 참조, "{DOCUMENT_SWATCHES}": 문서의 견본) +@msg no swatch named `{n}` `{n}` 견본이 없습니다 +@msg no swatch or colour group `{n}` 견본 또는 색상 그룹 `{n}`이(가) 없습니다 +@msg no swatch or colour group `{n}` in `{_1}` `{_1}`에 견본 또는 색상 그룹 `{n}`이(가) 없습니다 +@msg no symbol named `{_1}` `{_1}` 심볼이 없습니다 +@msg no symbol named `{name}` `{name}` 심볼이 없습니다 +@msg no text objects selected 선택한 텍스트 개체가 없습니다 +@msg no toolbar named `{_1}` `{_1}` 도구 막대가 없습니다 +@msg no toolbar named `{name}` `{name}` 도구 막대가 없습니다 +@msg no tracing preset `{preset}` (see imageTrace.presets) 추적 사전 설정 `{preset}`이(가) 없습니다(imageTrace.presets 참조) +@msg no user library folder here (save with a path, or without one to get the data) 여기에는 사용자 라이브러리 폴더가 없습니다(경로를 지정해 저장하거나, 경로 없이 저장해 데이터를 받으세요) +@msg no variable named `{from}` `{from}` 변수가 없습니다 +@msg no variable named `{name}` `{name}` 변수가 없습니다 +@msg no variable named `{var}` `{var}` 변수가 없습니다 +@msg no variable named `{v}` `{v}` 변수가 없습니다 +@msg no view named `{name}` `{name}` 보기가 없습니다 +@msg node {_1} already has the text variable `{_2}` 노드 {_1}에 이미 텍스트 변수 `{_2}`이(가) 있습니다 +@msg node {_1} is not a graph 노드 {_1}은(는) 그래프가 아닙니다 +@msg node {_1} is not an image 노드 {_1}은(는) 이미지가 아닙니다 +@msg node {_1} is not type 노드 {_1}은(는) 문자가 아닙니다 +@msg not a DCS file DCS 파일이 아닙니다 +@msg not a PDF presets file (format `{_1}`) PDF 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a PDF presets file: {e} PDF 사전 설정 파일이 아닙니다: {e} +@msg not a RegExp object RegExp 개체가 아닙니다 +@msg not a Word document: its WordDocument stream doesn't start with a file information block Word 문서가 아닙니다: WordDocument 스트림이 파일 정보 블록으로 시작하지 않습니다 +@msg not a Word document: its compound file has no WordDocument stream Word 문서가 아닙니다: 복합 파일에 WordDocument 스트림이 없습니다 +@msg not a Word document: its main part is not a WordprocessingML document Word 문서가 아닙니다: 주 부분이 WordprocessingML 문서가 아닙니다 +@msg not a Word document: its package has no main document part Word 문서가 아닙니다: 패키지에 주 문서 부분이 없습니다 +@msg not a closed path 닫힌 패스가 아닙니다 +@msg not a flattener presets file (format `{_1}`) 병합 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a flattener presets file: {e} 병합 사전 설정 파일이 아닙니다: {e} +@msg not a layer or group 레이어나 그룹이 아닙니다 +@msg not a library file: {e} 라이브러리 파일이 아닙니다: {e} +@msg not a mesh envelope point 메시 엔벨로프 점이 아닙니다 +@msg not a perspective grid presets file (format `{_1}`) 원근 격자 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a perspective grid presets file: {e} 원근 격자 사전 설정 파일이 아닙니다: {e} +@msg not a print presets file (format `{_1}`) 인쇄 사전 설정 파일이 아닙니다(형식 `{_1}`) +@msg not a print presets file: {e} 인쇄 사전 설정 파일이 아닙니다: {e} +@msg not a regular expression 정규식이 아닙니다 +@msg not a stored or deflated central record 저장 방식이나 deflate 방식의 중앙 레코드가 아닙니다 +@msg not a valid SVGZ file: {e} 올바른 SVGZ 파일이 아닙니다: {e} +@msg not an RTF file: it doesn't start with {\rtf RTF 파일이 아닙니다: {\rtf(으)로 시작하지 않습니다 +@msg not an action set: the text doesn't start with /version and a number 액션 세트가 아닙니다: 텍스트가 /version과 숫자로 시작하지 않습니다 +@msg not an image VectorCraft reads (see document.formats) VectorCraft가 읽는 이미지가 아닙니다(document.formats 참조) +@msg not area type (text in a frame) outside perspective 원근 밖의 영역 문자(프레임 안의 텍스트)가 아닙니다 +@msg not editing an opacity mask 불투명 마스크를 편집하고 있지 않습니다 +@msg not type on a path outside perspective 원근 밖의 패스 위 문자가 아닙니다 +@msg nothing prints 인쇄할 것이 없습니다 +@msg nothing to bind 바인딩할 항목이 없습니다 +@msg nothing to change 변경할 항목이 없습니다 +@msg nothing to change: give dx, dy, hScale, vScale or rotation 변경할 항목이 없습니다: dx, dy, hScale, vScale 또는 rotation을 지정하세요 +@msg nothing to crop 자를 항목이 없습니다 +@msg nothing to export 내보낼 항목이 없습니다 +@msg nothing to export: the document has no visible art 내보낼 항목이 없습니다: 문서에 보이는 아트웍이 없습니다 +@msg nothing to mark 표시할 항목이 없습니다 +@msg nothing to print: every artboard picked is blank 인쇄할 것이 없습니다: 선택한 아트보드가 모두 비어 있습니다 +@msg nothing to print: the document has no art that prints 인쇄할 것이 없습니다: 문서에 인쇄되는 아트웍이 없습니다 +@msg nothing to repeat 반복할 항목이 없습니다 +@msg numbers out of order in {_1} quantifier {_1} 수량자의 숫자 순서가 잘못되었습니다 +@msg object is not a path 개체가 패스가 아닙니다 +@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object 개체 투명도는 강조 표시한 글자에 샘플링할 수 없습니다. 스포이드 옵션에서 투명도를 끄거나 개체 전체를 선택하세요 +@msg object {_1} can't be set as point type 개체 {_1}은(는) 포인트 문자로 설정할 수 없습니다 +@msg object {_1} has lost its file's contents: relink it 개체 {_1}이(가) 파일 내용을 잃었습니다: 다시 연결하세요 +@msg object {_1} has no path 개체 {_1}에 패스가 없습니다 +@msg object {_1} has no size 개체 {_1}에 크기가 없습니다 +@msg object {_1} is hidden or locked, or on a locked artboard 개체 {_1}이(가) 숨겨졌거나 잠겨 있거나 잠긴 아트보드에 있습니다 +@msg object {_1} is not a placed document 개체 {_1}은(는) 가져온 문서가 아닙니다 +@msg object {_1} is not an Intertwine group 개체 {_1}은(는) 교차 그룹이 아닙니다 +@msg object {_1} is not an image 개체 {_1}은(는) 이미지가 아닙니다 +@msg object {_1} is not an image or a linked file 개체 {_1}은(는) 이미지나 연결된 파일이 아닙니다 +@msg object {_1} is not an image or a placed document 개체 {_1}은(는) 이미지나 가져온 문서가 아닙니다 +@msg object {_1} is not in this Intertwine group 개체 {_1}은(는) 이 교차 그룹에 없습니다 +@msg object {_1} needs a 3D effect 개체 {_1}에는 3D 효과가 필요합니다 +@msg object {_1} needs a 3D effect at index {index} 개체 {_1}에는 인덱스 {index}에 3D 효과가 필요합니다 +@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic 개체 {_1}: 속이 빈 돌출 및 경사에는 캡이 없습니다: 그래픽을 매핑하려면 캡을 켜세요 +@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front 개체 {_1}: 깊이가 0이면 개체에 면이 하나뿐입니다: 그래픽을 앞면에 매핑하세요 +@msg object {_1}: {_2} 개체 {_1}: {_2} +@msg object {_1}: {e} 개체 {_1}: {e} +@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams 개체 {_1}: 인덱스 {index}의 {name}에는 조명 사전 설정이 없습니다. effect.setParams로 조명을 설정하세요 +@msg object {id} has anchors 0–{_1} for `corners`, not {past} 개체 {id}에서 `corners`에 쓸 수 있는 앵커는 0–{_1}입니다: {past} +@msg object {id} has {n} anchors, so no anchor {past} 개체 {id}에는 앵커가 {n}개이므로 앵커 {past}은(는) 없습니다 +@msg objects must be none, move or copy objects는 none, move 또는 copy여야 합니다 +@msg objects were left out 개체가 제외되었습니다 +@msg offset out of range 오프셋이 범위를 벗어났습니다 +@msg on must be true or false on은 true 또는 false여야 합니다 +@msg only groups, layers, type and images have a Contents (Characters) row 콘텐츠(글자) 행은 그룹, 레이어, 문자, 이미지에만 있습니다 +@msg only interior mesh lines can be deleted 안쪽 메시 선만 삭제할 수 있습니다 +@msg only solid colours can be added (not gradients or patterns) 단색만 추가할 수 있습니다(그레이디언트나 패턴은 안 됨) +@msg only solid-colour swatches can be spot colours 단색 견본만 별색이 될 수 있습니다 +@msg only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first 이 Affinity 파일은 포함된 미리 보기만 읽을 수 있었습니다: 파일 › 열기로 경고와 함께 확인하거나 먼저 Affinity에서 SVG나 PDF로 내보내세요 +@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning 이 Affinity 파일은 포함된 미리 보기만 읽을 수 있었습니다. 파일 › 열기로 경고와 함께 확인하세요 +@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon, 열 위치는 window.panel.float와 window.panel.dock에 씁니다 +@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon, 열 위치는 anchor, side, zone, before와 함께 쓸 수 없습니다 +@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon, 열 위치는 x, y, width, height와 함께 쓸 수 없습니다 +@msg opacity must be a number opacity는 숫자여야 합니다 +@msg open a document to look for its linked files 연결된 파일을 찾으려면 문서를 여세요 +@msg operand too large 피연산자가 너무 큽니다 +@msg options must be an object (see file.formatOptions) options는 개체여야 합니다(file.formatOptions 참조) +@msg order is a whole number from 0 to {others}, not `{v}` order는 0에서 {others} 사이의 정수입니다: `{v}` +@msg order must be leftToRight or rightToLeft order는 leftToRight 또는 rightToLeft여야 합니다 +@msg orientation must be portrait or landscape, not `{o}` orientation은 portrait 또는 landscape여야 합니다: `{o}` +@msg outlineText {v}: true or false outlineText {v}: true 또는 false여야 합니다 +@msg output intent profile: {e} 출력 의도 프로파일: {e} +@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination: “{name}”(이)라는 RGB 또는 CMYK 프로파일이 없습니다 +@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination: {_1} 파일은 CMYK인데 “{name}”은(는) RGB 프로파일입니다 +@msg output.destination: {e} output.destination: {e} +@msg output.inks: no ink is set to print output.inks: 인쇄하도록 설정된 잉크가 없습니다 +@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent: “{name}”(이)라는 프로파일이 없으며, {std} 출력 의도는 프로파일을 포함해야 합니다 +@msg output.outputIntent: no profile is called “{name}”: a {std} output intent embeds a profile, or names a registered printing condition (outputConditionId and registry) output.outputIntent: “{name}”(이)라는 프로파일이 없습니다: {std} 출력 의도는 프로파일을 포함하거나 등록된 인쇄 조건(outputConditionId와 registry)을 지정합니다 +@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent: {std} 파일은 CMYK로 인쇄하는데 “{name}”은(는) RGB 프로파일입니다 +@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent: “{name}”은(는) 회색 프로파일이어서 {std} 출력 의도로 쓸 수 없습니다 +@msg overlap: {o} pt is half a tile or more overlap: {o} pt는 타일의 절반 이상입니다 +@msg overlapping Shaper paths must be in the same layer or group 겹치는 셰이퍼 패스는 같은 레이어나 그룹에 있어야 합니다 +@msg overlapping edits 편집 범위가 겹칩니다 +@msg page art conflicts with image data for {key} 페이지 아트웍이 {key}의 이미지 데이터와 충돌합니다 +@msg page art conflicts with pattern {name} 페이지 아트웍이 패턴 {name}와(과) 충돌합니다 +@msg page art conflicts with swatch {name} 페이지 아트웍이 견본 {name}와(과) 충돌합니다 +@msg page art is missing image data for {key} 페이지 아트웍에 {key}의 이미지 데이터가 없습니다 +@msg page art is missing pattern {name} 페이지 아트웍에 패턴 {name}이(가) 없습니다 +@msg page art is missing swatch {name} 페이지 아트웍에 견본 {name}이(가) 없습니다 +@msg page must be a whole number from 1 page는 1 이상의 정수여야 합니다 +@msg page thumbnail: {e} 페이지 미리 보기 이미지: {e} +@msg page {page} of the PDF has no objects PDF의 {page}페이지에 개체가 없습니다 +@msg page {page}: `{file}` has {n} {artboards} {page}페이지: `{file}`에는 {artboards}이(가) {n}개 있습니다 +@msg page {page}: `{name}` has {n} {pages} {page}페이지: `{name}`에는 {pages}이(가) {n}개 있습니다 +@msg pages must be a range such as "2-3" or a page number, not {v} pages는 "2-3" 같은 범위 또는 페이지 번호여야 합니다: {v} +@msg pages: {e} pages: {e} +@msg panel position must be finite 패널 위치는 유한해야 합니다 +@msg parameters must be an object 매개변수는 개체여야 합니다 +@msg params must be an object params는 개체여야 합니다 +@msg passwords can have up to {MAX_PASSWORD} bytes 암호는 최대 {MAX_PASSWORD}바이트까지 쓸 수 있습니다 +@msg path chunks must share their boundary point 패스 조각은 경계 점을 공유해야 합니다 +@msg path has no open end 패스에 열린 끝이 없습니다 +@msg paths must be strings paths는 문자열이어야 합니다 +@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false를 쓰려면 편집 가능한 아트웍을 보존하도록 모든 아트보드를 포함해야 합니다 +@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false useArtboards가 false일 때 pdfCompatible: false를 쓰려면 편집 가능한 아트웍을 보존하도록 아트보드를 포함해야 합니다 +@msg pdfs `{m}`: single or multiple pdfs `{m}`: single 또는 multiple이어야 합니다 +@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom은 "artwork" 또는 "swatches"입니다: {v} +@msg pins and moved must be non-empty lists of the same length pins와 moved는 길이가 같고 비어 있지 않은 목록이어야 합니다 +@msg pins must be [[x,y]…] pins는 [[x,y]…]여야 합니다 +@msg pins must be finite pins는 유한해야 합니다 +@msg place is above, below or inside, not `{other}` place는 above, below 또는 inside입니다: `{other}` +@msg plane must be left|right|ground plane은 left|right|ground여야 합니다 +@msg plane must be left|right|ground|none plane은 left|right|ground|none이어야 합니다 +@msg point is outside the mesh 점이 메시 밖에 있습니다 +@msg point x must be a number 점의 x는 숫자여야 합니다 +@msg point y must be a number 점의 y는 숫자여야 합니다 +@msg points must be [t, left, right] with non-negative widths points는 너비가 음이 아닌 [t, left, right]여야 합니다 +@msg points must be [x,y] pairs points는 [x,y] 쌍이어야 합니다 +@msg points must be a non-empty [[x,y,pressure?]…] list points는 비어 있지 않은 [[x,y,pressure?]…] 목록이어야 합니다 +@msg points must be a non-empty list of finite [x,y] points는 유한한 [x,y]의 비어 있지 않은 목록이어야 합니다 +@msg points must be an array or null points는 배열 또는 null이어야 합니다 +@msg points must be finite points는 유한해야 합니다 +@msg points must be finite [x,y] pairs points는 유한한 [x,y] 쌍이어야 합니다 +@msg points must be finite canvas points points는 유한한 캔버스 점이어야 합니다 +@msg points must contain 1–4096 coordinates points에는 좌표가 1–4096개 있어야 합니다 +@msg position must be normal, superscript or subscript position은 normal, superscript 또는 subscript여야 합니다 +@msg position must be topLeft, topRight, bottomLeft or bottomRight position은 topLeft, topRight, bottomLeft 또는 bottomRight여야 합니다 +@msg positions must be between 0 and 100000 pt positions는 0에서 100000 pt 사이여야 합니다 +@msg ppi must be a positive number, not {ppi} ppi는 양수여야 합니다: {ppi} +@msg ppi must be between 1 and 2400 ppi는 1에서 2400 사이여야 합니다 +@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}`은(는) transforms|bounds|fileDimensions|fit|fill이 아닙니다 +@msg preserveShape must be clippingMask|transparency preserveShape는 clippingMask|transparency여야 합니다 +@msg preset `{id}`: mobile or density preset `{id}`: mobile 또는 density여야 합니다 +@msg preset must be standard, diffuse, topLeft or right preset은 standard, diffuse, topLeft 또는 right여야 합니다 +@msg preset must be the name of a print preset preset은 인쇄 사전 설정의 이름이어야 합니다 +@msg previewMode must be default, pixel or overprint previewMode는 default, pixel 또는 overprint여야 합니다 +@msg print settings: {e} 인쇄 설정: {e} +@msg procedures nest too deeply 프로시저가 너무 깊게 중첩되어 있습니다 +@msg procedures nested too deeply 프로시저가 너무 깊게 중첩되었습니다 +@msg quotes must be {double?, single?} quotes는 {double?, single?}여야 합니다 +@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k}은(는) 여는 따옴표와 닫는 따옴표여야 합니다(예: "“”") +@msg range out of order in character class 문자 클래스의 범위 순서가 잘못되었습니다 +@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi는 1–2400이어야 합니다(72 screen, 150 medium, 300 high) +@msg rating must be a whole number from 0 to {MAX_RATING} rating은 0에서 {MAX_RATING} 사이의 정수여야 합니다 +@msg rect and {k} contradict each other: give the area or the artboards rect와 {k}이(가) 서로 맞지 않습니다: 영역이나 아트보드 중 하나를 지정하세요 +@msg rect applies to raster formats, not {_1} rect는 래스터 형식에 적용되며 {_1}에는 적용되지 않습니다 +@msg rect is [x, y, width, height] in points rect는 포인트 단위의 [x, y, width, height]입니다 +@msg rect must be [x, y, width, height] rect는 [x, y, width, height]여야 합니다 +@msg rect must be [x, y, width, height] with a positive width and height rect는 너비와 높이가 양수인 [x, y, width, height]여야 합니다 +@msg rect needs finite numbers and a positive width and height rect에는 유한한 숫자와 양수인 너비와 높이가 필요합니다 +@msg redraw needs id redraw에는 id가 필요합니다 +@msg redraw within must be a finite distance of 0 or more redraw의 within은 0 이상의 유한한 거리여야 합니다 +@msg reduce of an empty array with no initial value 초기값 없이 빈 배열에 reduce를 호출했습니다 +@msg registration must be 0..8 registration은 0..8이어야 합니다 +@msg regular expression flag '{_1}' given twice 정규식 플래그 '{_1}'을(를) 두 번 지정했습니다 +@msg release a layer or a group 레이어나 그룹을 해제하세요 +@msg replace keeps the replaced object's place and transform: drop template, at and rect replace는 바꾼 개체의 위치와 변형을 유지합니다: template, at, rect를 빼세요 +@msg replace: select the one object to replace replace: 바꿀 개체 하나를 선택하세요 +@msg resolution must be a number or screen|medium|high resolution은 숫자 또는 screen|medium|high여야 합니다 +@msg resolution must be between 1 and 2400 ppi resolution은 1에서 2400 ppi 사이여야 합니다 +@msg rotation must be a number of degrees rotation은 도 단위의 숫자여야 합니다 +@msg rotation must be finite rotation은 유한해야 합니다 +@msg rows and cols must be 1..50 rows와 cols는 1..50이어야 합니다 +@msg rows and columns must be 1..500 rows와 columns는 1..500이어야 합니다 +@msg rows of objects whose appearances differ can't be reordered together 모양새가 서로 다른 개체의 행은 함께 순서를 바꿀 수 없습니다 +@msg sampled text would exceed the canvas (coordinates out of range) 샘플링한 텍스트가 캔버스를 벗어납니다(좌표 범위 초과) +@msg save the document first: Package collects a saved document 먼저 문서를 저장하세요: 패키지는 저장된 문서를 수집합니다 +@msg save the document first: copy puts the file in its folder 먼저 문서를 저장하세요: copy는 파일을 문서의 폴더에 넣습니다 +@msg savedPreset: {e} savedPreset: {e} +@msg scale `{s}`: a factor such as 2x, a width (100w), a height (100h) or a resolution (72ppi) scale `{s}`: 2x 같은 배율, 너비(100w), 높이(100h) 또는 해상도(72ppi)여야 합니다 +@msg scale must be a number above 0, at most 64 scale은 0보다 크고 64 이하인 숫자여야 합니다 +@msg scale must be non-zero scale은 0이 아니어야 합니다 +@msg scope must be selection or all (got {other}) scope는 selection 또는 all이어야 합니다({other} 입력됨) +@msg scribble is too long 스크리블이 너무 깁니다 +@msg segment does not exist 세그먼트가 없습니다 +@msg select a Shaper group 셰이퍼 그룹을 선택하세요 +@msg select a blend to set easing or color shift 이징이나 색상 이동을 설정할 블렌드를 선택하세요 +@msg select a compound shape 컴파운드 모양을 선택하세요 +@msg select a graph 그래프를 선택하세요 +@msg select a group or layer with visible, unlocked members 보이고 잠기지 않은 구성원이 있는 그룹이나 레이어를 선택하세요 +@msg select a layer 레이어를 선택하세요 +@msg select a text object 텍스트 개체를 선택하세요 +@msg select an SVG filter SVG 필터를 선택하세요 +@msg select an image (or an Image Trace group) 이미지(또는 이미지 추적 그룹)를 선택하세요 +@msg select an image, or pass id 이미지를 선택하거나 id를 전달하세요 +@msg select an object (or pass `id`) 개체를 선택하세요(또는 `id`를 전달하세요) +@msg select an object with a 3D effect 3D 효과가 있는 개체를 선택하세요 +@msg select an object with a 3D effect, or give its id in ids 3D 효과가 있는 개체를 선택하거나 ids에 그 id를 지정하세요 +@msg select an object with a freeform gradient 자유 형태 그레이디언트가 있는 개체를 선택하세요 +@msg select an object with an opacity mask 불투명 마스크가 있는 개체를 선택하세요 +@msg select area type 영역 문자를 선택하세요 +@msg select area type (text in a frame) 영역 문자(프레임 안의 텍스트)를 선택하세요 +@msg select area type and closed paths (at least one area type object) 영역 문자와 닫힌 패스를 선택하세요(영역 문자 개체가 하나 이상 있어야 함) +@msg select area type whose first line can fit its frame 첫 줄이 프레임에 들어가는 영역 문자를 선택하세요 +@msg select artwork to add its colours 색상을 추가할 아트웍을 선택하세요 +@msg select artwork to make a group of its colours 색상으로 그룹을 만들 아트웍을 선택하세요 +@msg select artwork to recolour, or give a colour `group` 색상을 재지정할 아트웍을 선택하거나 색상 `group`을 지정하세요 +@msg select exactly two path ends to join 연결할 패스 끝을 정확히 두 개 선택하세요 +@msg select members of a compound shape 컴파운드 모양의 구성원을 선택하세요 +@msg select objects or give ids 개체를 선택하거나 ids를 지정하세요 +@msg select objects to align to the guide 안내선에 정렬할 개체를 선택하세요 +@msg select one Intertwine group 교차 그룹 하나를 선택하세요 +@msg select one image or pass id 이미지 하나를 선택하거나 id를 전달하세요 +@msg select one layer, group or object to rename 이름을 바꿀 레이어, 그룹 또는 개체를 하나 선택하세요 +@msg select paths 패스를 선택하세요 +@msg select paths or compound paths 패스나 컴파운드 패스를 선택하세요 +@msg select rows or objects to collect 수집할 행이나 개체를 선택하세요 +@msg select something to export 내보낼 항목을 선택하세요 +@msg select something to export (or turn off selectedOnly) 내보낼 항목을 선택하세요(또는 selectedOnly를 끄세요) +@msg select text to redefine the style from 스타일을 재정의할 기준 텍스트를 선택하세요 +@msg select the art and, on top of it, the mask object 아트웍과 그 위에 있는 마스크 개체를 선택하세요 +@msg select the art for the design 디자인에 쓸 아트웍을 선택하세요 +@msg select the art for the pattern tile 패턴 타일에 쓸 아트웍을 선택하세요 +@msg select the art to add first 먼저 추가할 아트웍을 선택하세요 +@msg select the art to collect (or give its ids) 수집할 아트웍을 선택하세요(또는 ids를 지정하세요) +@msg select the art to weed 위딩할 아트웍을 선택하세요 +@msg select the banner, or mark every artboard 배너를 선택하거나 모든 아트보드에 표시하세요 +@msg select the content and a path on top 콘텐츠와 그 위의 패스를 선택하세요 +@msg select the images to relink, or pass ids 다시 연결할 이미지를 선택하거나 ids를 전달하세요 +@msg select the images, or pass ids 이미지를 선택하거나 ids를 전달하세요 +@msg select the linked images or placed documents, or pass ids 연결된 이미지나 가져온 문서를 선택하거나 ids를 전달하세요 +@msg select the object to copy from 복사해 올 개체를 선택하세요 +@msg select the objects (or give their ids) 개체를 선택하세요(또는 ids를 지정하세요) +@msg select the objects to attach to the path 패스에 부착할 개체를 선택하세요 +@msg select the objects to bind 바인딩할 개체를 선택하세요 +@msg select two or more objects to intertwine 교차할 개체를 두 개 이상 선택하세요 +@msg select two or more slices to combine 결합할 슬라이스를 두 개 이상 선택하세요 +@msg select type on a path 패스 위 문자를 선택하세요 +@msg select type on a path or area type 패스 위 문자 또는 영역 문자를 선택하세요 +@msg selection `{m}`: one or each selection `{m}`: one 또는 each여야 합니다 +@msg settings is an object of document.exportForScreens params settings는 document.exportForScreens 매개변수의 개체입니다 +@msg settings must be an object settings는 개체여야 합니다 +@msg show `{show}`: all, missing, modified or embedded show `{show}`: all, missing, modified 또는 embedded여야 합니다 +@msg show must be true or false show는 true 또는 false여야 합니다 +@msg side is left or right, not `{v}` side는 left 또는 right입니다: `{v}` +@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size는 "small", "medium", "large"이거나 0보다 크고 {_1} 이하인 포인트 값이어야 합니다 +@msg size must be positive size는 양수여야 합니다 +@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode는 "fixed" 또는 "pressure"입니다: `{other}` +@msg sizeMode is "fixed" or "pressure", not {other} sizeMode는 "fixed" 또는 "pressure"입니다: {other} +@msg sizeVariation must be 0 or more sizeVariation은 0 이상이어야 합니다 +@msg smallCaps must be true or false smallCaps는 true 또는 false여야 합니다 +@msg sort `{x}`: name, kind or status sort `{x}`: name, kind 또는 status여야 합니다 +@msg source paths are too large for Shaper 원본 패스가 셰이퍼에 쓰기에는 너무 큽니다 +@msg source text transform is collapsed or unrepresentable 원본 텍스트 변형이 축소되었거나 나타낼 수 없습니다 +@msg spot, mode and group apply to solid colours only spot, mode, group은 단색에만 적용됩니다 +@msg status `{s}`: linked, embedded, missing or modified status `{s}`: linked, embedded, missing 또는 modified여야 합니다 +@msg steps must be 1..1000 steps는 1..1000이어야 합니다 +@msg steps must be 1..{MAX_STEPS} steps는 1..{MAX_STEPS}이어야 합니다 +@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) 정지점 {i}: `plates`은(는) 0에서 1 사이의 숫자 네 개(C, M, Y, K)여야 합니다 +@msg stopped before the archive was finished 아카이브를 끝내기 전에 중지되었습니다 +@msg strokeMode must be within|along|across, got {m} strokeMode는 within|along|across여야 합니다({m} 입력됨) +@msg strokeOptions must be an object of stroke.set options strokeOptions는 stroke.set 옵션의 개체여야 합니다 +@msg strokeWidth must be a positive number of pixels strokeWidth는 픽셀 단위의 양수여야 합니다 +@msg subpath {si} is not an open subpath 하위 패스 {si}은(는) 열린 하위 패스가 아닙니다 +@msg subpaths required when joining the same path 같은 패스를 연결할 때는 subpaths가 필요합니다 +@msg swatchConflict must be "merge" or "add", not {v} swatchConflict는 "merge" 또는 "add"여야 합니다: {v} +@msg table must be true or false table은 true 또는 false여야 합니다 +@msg takes one name 이름 하나만 지정할 수 있습니다 +@msg target text transform is collapsed or unrepresentable 대상 텍스트 변형이 축소되었거나 나타낼 수 없습니다 +@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text는 {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?}여야 합니다 +@msg text transform is collapsed or unrepresentable 텍스트 변형이 축소되었거나 나타낼 수 없습니다 +@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces는 2에서 100 사이의 정수여야 합니다 +@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}`: "{_1}" 또는 "{_2}"을(를) 사용하세요 +@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage: 이미지 파일의 {path} 또는 {name, dataBase64}를 지정하거나 effect.list가 제공하는 텍스처를 지정하세요 +@msg textureImage: {e} textureImage: {e} +@msg that makes more than {MAX_PIECES} slices 그러면 슬라이스가 {MAX_PIECES}개보다 많아집니다 +@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer 그러면 조각(최대 100000개)이나 대시 단계가 너무 많아집니다: 대시 패턴을 단순하게 하거나 대시를 더 길게 하세요 +@msg that object is not in the overlap 그 개체는 겹친 부분에 없습니다 +@msg the Affinity document has no page with a size Affinity 문서에 크기가 있는 페이지가 없습니다 +@msg the CMYK image couldn't be written CMYK 이미지를 기록할 수 없습니다 +@msg the Layers panel is not shown 레이어 패널이 표시되어 있지 않습니다 +@msg the Layers panel's search, filter or Show Layers Only hides its row 레이어 패널의 검색, 필터 또는 레이어만 표시 때문에 해당 행이 숨겨져 있습니다 +@msg the Objects on Path group is hidden or locked 패스 위 개체 그룹이 숨겨졌거나 잠겨 있습니다 +@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected PDF 보안 설정이 암호 없이 저장되었습니다: openPassword 또는 permissionsPassword를 다시 지정하거나, 보호 없이 내보내려면 passwordRequired가 없는 security를 지정하세요 +@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) RTF 파일에 단락이나 서식 변경이 너무 많아 문자로 가져올 수 없습니다(최대 단락 {MAX_PARAGRAPHS}개, 런 {MAX_RUNS}개) +@msg the RTF file is too large to place as type (at most {_1} MB) RTF 파일이 너무 커서 문자로 가져올 수 없습니다(최대 {_1} MB) +@msg the SVG expands to more than {0} nested SVG images SVG를 확장하면 중첩된 SVG 이미지가 {0}개보다 많아집니다 +@msg the SVG expands to more than {0} objects SVG를 확장하면 개체가 {0}개보다 많아집니다 +@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes SVG 파일이 {MAX_IMPORT_BYTES}바이트보다 큽니다 +@msg the SVG has no drawable objects SVG에 그릴 수 있는 개체가 없습니다 +@msg the SVGZ file unpacks to more than {_1} MB SVGZ 파일의 압축을 풀면 {_1} MB를 넘습니다 +@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place Word 문서의 속성이 {MAX_PAGES}페이지보다 많아 가져올 수 없습니다 +@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place Word 문서에 문자 또는 단락 속성의 런이 {MAX_RUNS}개보다 많아 가져올 수 없습니다 +@msg the Word document has more than {_1} MB of formatting properties, too many to place Word 문서의 서식 속성이 {_1} MB보다 많아 가져올 수 없습니다 +@msg the Word document has too much text or formatting to place as type (at most {MAX_PARAGRAPHS} paragraphs, {MAX_RUNS} runs and {_1} MB of text) Word 문서에 텍스트나 서식이 너무 많아 문자로 가져올 수 없습니다(최대 단락 {MAX_PARAGRAPHS}개, 런 {MAX_RUNS}개, 텍스트 {_1} MB) +@msg the Word document has too much text to place as type (at most {_1} MB) Word 문서에 텍스트가 너무 많아 문자로 가져올 수 없습니다(최대 {_1} MB) +@msg the Word document is encrypted or protected with a password; save it without one to place it Word 문서가 암호화되었거나 암호로 보호되어 있습니다. 가져오려면 암호 없이 저장하세요 +@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) Word 문서가 너무 커서 가져올 수 없습니다({name} 스트림이 {_1} MB를 넘습니다) +@msg the Word document was last saved with fast save, which VectorCraft doesn't read; save it again with fast save turned off Word 문서가 마지막에 빠른 저장으로 저장되었는데, VectorCraft는 이 방식을 읽지 않습니다. 빠른 저장을 끄고 다시 저장하세요 +@msg the Word document was saved by Word 6.0 or Word 95, which VectorCraft doesn't read; save it as a Word 97-2003 document Word 문서가 Word 6.0 또는 Word 95에서 저장되었는데, VectorCraft는 이 형식을 읽지 않습니다. Word 97-2003 문서로 저장하세요 +@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place Word 문서의 텍스트가 {MAX_PIECES}개보다 많은 조각으로 이루어져 있어 가져올 수 없습니다 +@msg the XML operation needs an XML or XMLList value XML 연산에는 XML 또는 XMLList 값이 필요합니다 +@msg the XPath expression does not evaluate to a node-set XPath 식의 결과가 노드 집합이 아닙니다 +@msg the XPath expression ends early XPath 식이 중간에 끝났습니다 +@msg the XPath expression has an unmatched ) XPath 식에 짝이 없는 )가 있습니다 +@msg the XPath expression has an unmatched ] XPath 식에 짝이 없는 ]가 있습니다 +@msg the XPath expression has an unmatched closing bracket XPath 식에 짝이 없는 닫는 괄호가 있습니다 +@msg the XPath expression selects a namespace node, which is not an XML value XPath 식이 XML 값이 아닌 네임스페이스 노드를 선택합니다 +@msg the XPath expression selects the root node, which is not an XML value XPath 식이 XML 값이 아닌 루트 노드를 선택합니다 +@msg the XPath function {name} needs a node-set XPath 함수 {name}에는 노드 집합이 필요합니다 +@msg the XPath function {name} takes the wrong number of arguments XPath 함수 {name}의 인수 개수가 맞지 않습니다 +@msg the XPath name functions need a node-set XPath 이름 함수에는 노드 집합이 필요합니다 +@msg the XPath prefix {shown} is not bound to a namespace XPath 접두사 {shown}이(가) 네임스페이스에 바인딩되어 있지 않습니다 +@msg the XPath variable ${shown} is not bound XPath 변수 ${shown}이(가) 바인딩되어 있지 않습니다 +@msg the action set ids are not a list 액션 세트 id가 목록이 아닙니다 +@msg the active paint is not a gradient 활성 페인트가 그레이디언트가 아닙니다 +@msg the argument list is not an object 인수 목록이 개체가 아닙니다 +@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size 아트보드가 캔버스에 들어가지 않습니다: 아트보드 수나 간격, 크기를 줄이세요 +@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out 아트웍이 EPS에 쓰기에는 너무 큽니다: PostScript 256 MB 이후의 내용은 제외되었습니다 +@msg the banner has no width or height 배너에 너비나 높이가 없습니다 +@msg the bitmaps hold more pixels than one file may 비트맵의 픽셀 수가 파일 하나에 담을 수 있는 양보다 많습니다 +@msg the character's size can't be measured 글자의 크기를 잴 수 없습니다 +@msg the code runs past its last instruction 코드가 마지막 명령을 지나 실행됩니다 +@msg the compound shape is too complex to expand 컴파운드 모양이 너무 복잡해서 확장할 수 없습니다 +@msg the container of the listed objects is gone 나열한 개체의 컨테이너가 없어졌습니다 +@msg the copy `{user_id}` can't be read as a library 복사본 `{user_id}`을(를) 라이브러리로 읽을 수 없습니다 +@msg the copy would leave the canvas 복사본이 캔버스를 벗어납니다 +@msg the curve tolerance must be from 0.01 to 10 points 곡선 허용치는 0.01에서 10포인트 사이여야 합니다 +@msg the custom dictionary couldn't be saved to {_1}: {e} 사용자 사전을 {_1}에 저장할 수 없습니다: {e} +@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is 사용자 사전 {_1}을(를) 읽을 수 없어({why}) 그대로 둡니다 +@msg the cut cannot be made on this art: {e} 이 아트웍은 자를 수 없습니다: {e} +@msg the data isn't an EMF or WMF picture 데이터가 EMF 또는 WMF 그림이 아닙니다 +@msg the data isn't an EPS or PostScript picture 데이터가 EPS 또는 PostScript 그림이 아닙니다 +@msg the deferred paste is no longer available 지연된 붙여넣기를 더 이상 사용할 수 없습니다 +@msg the design is too big to draw at every data point of this graph 디자인이 너무 커서 이 그래프의 모든 데이터 점에 그릴 수 없습니다 +@msg the design is too big to draw in every column of this graph 디자인이 너무 커서 이 그래프의 모든 막대에 그릴 수 없습니다 +@msg the dialog it was opened over is no longer open 이 대화 상자가 열렸던 바탕 대화 상자가 더 이상 열려 있지 않습니다 +@msg the dictionary stack overflowed 사전 스택이 오버플로되었습니다 +@msg the document already has an SVG filter named {name} 문서에 {name}(이)라는 SVG 필터가 이미 있습니다 +@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it 문서에 {name}(이)라는 다른 SVG 필터가 있습니다: svgFilter.apply를 사용하거나, 편집하려면 `replace`와 함께 svgFilter.define을 사용하세요 +@msg the document has no SVG filter named {name} 문서에 {name}(이)라는 SVG 필터가 없습니다 +@msg the document has no SVG filter named {r} 문서에 {r}(이)라는 SVG 필터가 없습니다 +@msg the document has no color swatches 문서에 색상 견본이 없습니다 +@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 문서의 견본에 색상이 {_1}개 있습니다. document 감소는 최대 255개까지 쓸 수 있습니다 +@msg the editing data's hash has no place 편집 데이터의 해시를 넣을 자리가 없습니다 +@msg the entities the document declares expand to too much text 문서가 선언한 엔티티를 확장하면 텍스트가 너무 많아집니다 +@msg the envelope's content has no bounds 엔벨로프의 콘텐츠에 경계가 없습니다 +@msg the envelope's mesh is malformed 엔벨로프의 메시가 올바르지 않습니다 +@msg the file holds no `variableSets` element 파일에 `variableSets` 요소가 없습니다 +@msg the file is larger than {_1} MB 파일이 {_1} MB보다 큽니다 +@msg the file is too large 파일이 너무 큽니다 +@msg the file isn't valid SVG: {e} 파일이 올바른 SVG가 아닙니다: {e} +@msg the file names no plate 파일에 지정된 색판이 없습니다 +@msg the file was updated or encrypted already 파일이 이미 업데이트되었거나 암호화되었습니다 +@msg the first character of a line has no character before it to kern: it moves only up and down (dy) 줄의 첫 글자는 앞에 커닝할 글자가 없습니다: 위아래로만 이동합니다(dy) +@msg the font {_1} is not available 글꼴 {_1}을(를) 사용할 수 없습니다 +@msg the format has no file extension 이 형식에는 파일 확장자가 없습니다 +@msg the function's prototype is not an object 함수의 프로토타입이 개체가 아닙니다 +@msg the graphic can't be read: {e} 그래픽을 읽을 수 없습니다: {e} +@msg the graphic is empty 그래픽이 비어 있습니다 +@msg the graphic's data is damaged 그래픽의 데이터가 손상되었습니다 +@msg the graphic's document has no artboard 그래픽의 문서에 아트보드가 없습니다 +@msg the grommet inset is too large for the banner 가장자리에서 아일렛까지 거리가 배너에 비해 너무 큽니다 +@msg the guide is hidden or locked 안내선이 숨겨졌거나 잠겨 있습니다 +@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them `{name}`의 머리글 `{cell}`은(는) 변수 `{other}`(으)로 다시 읽힙니다: 둘 중 하나의 이름을 바꾸세요 +@msg the hem is too deep for the banner 단 접기가 배너에 비해 너무 깊습니다 +@msg the image `{path}` isn't followed: {r} 이미지 `{path}`을(를) 따라가지 않습니다: {r} +@msg the image has no embedded pixels 이미지에 포함된 픽셀이 없습니다 +@msg the image is empty 이미지가 비어 있습니다 +@msg the image is fully transparent 이미지가 완전히 투명합니다 +@msg the image is not on an artboard; pass rect 이미지가 아트보드 위에 없습니다. rect를 전달하세요 +@msg the image's pixels are missing 이미지의 픽셀이 없습니다 +@msg the image's pixels aren't in the document 이미지의 픽셀이 문서에 없습니다 +@msg the image's pixels can't be read: {e} 이미지의 픽셀을 읽을 수 없습니다: {e} +@msg the image's transparency doesn't fit it 이미지의 투명도가 이미지와 맞지 않습니다 +@msg the job would print more than {MAX_PAGES} pages 이 작업은 {MAX_PAGES}페이지보다 많이 인쇄하게 됩니다 +@msg the job would print more than {_1} pages 이 작업은 {_1}페이지보다 많이 인쇄하게 됩니다 +@msg the layers hold no art to mask 레이어에 마스크할 아트웍이 없습니다 +@msg the layout moved 레이아웃이 이동했습니다 +@msg the library has a group `{name}` already 라이브러리에 `{name}` 그룹이 이미 있습니다 +@msg the library has no group `{name}` 라이브러리에 `{name}` 그룹이 없습니다 +@msg the library has no item `{item}` 라이브러리에 `{item}` 항목이 없습니다 +@msg the library has no item `{other}` 라이브러리에 `{other}` 항목이 없습니다 +@msg the library holds {MAX_GROUPS} groups already 라이브러리에 이미 그룹이 {MAX_GROUPS}개 있습니다 +@msg the library holds {MAX_ITEMS} items of this kind already 라이브러리에 이 종류의 항목이 이미 {MAX_ITEMS}개 있습니다 +@msg the library's file is outside the read roots 라이브러리 파일이 읽기 루트 밖에 있습니다 +@msg the line is too long, or the paths it passes too many, to check 선이 너무 길거나 선이 지나는 패스가 너무 많아 확인할 수 없습니다 +@msg the method needs an XML value 이 메서드에는 XML 값이 필요합니다 +@msg the name is empty 이름이 비어 있습니다 +@msg the object has no fill, stroke or effect 개체에 칠, 선 또는 효과가 없습니다 +@msg the object has no graphic style (pass `name`) 개체에 그래픽 스타일이 없습니다(`name`을 전달하세요) +@msg the objects don't match the plan 개체가 계획과 맞지 않습니다 +@msg the open and permissions passwords must differ 열기 암호와 권한 암호는 서로 달라야 합니다 +@msg the operand stack overflowed 피연산자 스택이 오버플로되었습니다 +@msg the page has no area 페이지에 면적이 없습니다 +@msg the page's type in an unread object's box isn't in its page art 읽지 않은 개체의 상자에 있는 페이지 문자가 페이지 아트웍에 없습니다 +@msg the paint behind the proxy is not a gradient 프록시 뒤의 페인트가 그레이디언트가 아닙니다 +@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) 페인트가 자유 형태 그레이디언트가 아닙니다(paint.editGradient {kind}(으)로 적용하세요) +@msg the parameters or body do not form one function 매개변수나 본문이 하나의 함수를 이루지 않습니다 +@msg the parent must be a layer or sublayer 상위 항목은 레이어나 하위 레이어여야 합니다 +@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it 붙여넣은 SVG 필터 {name}은(는) 문서에 있는 같은 이름의 필터와 다르게 그려지며, 쓸 수 있는 빈 이름이 없습니다 +@msg the path can't be extended 패스를 연장할 수 없습니다 +@msg the path can't be inside one of the objects to attach 패스가 부착할 개체 안에 있으면 안 됩니다 +@msg the path has no segments to measure 패스에 잴 세그먼트가 없습니다 +@msg the path is empty 패스가 비어 있습니다 +@msg the path is too short to shorten that much 패스가 너무 짧아 그만큼 줄일 수 없습니다 +@msg the paths are too many or too tangled to find where they cross 패스가 너무 많거나 너무 얽혀 있어 교차하는 곳을 찾을 수 없습니다 +@msg the pen table holds more than 255 pens 펜 테이블에 펜이 255개보다 많습니다 +@msg the picture has no objects 그림에 개체가 없습니다 +@msg the plates can't be kept 색판을 보관할 수 없습니다 +@msg the position table does not match the instructions 위치 테이블이 명령과 맞지 않습니다 +@msg the program runs too long 프로그램 실행 시간이 너무 깁니다 +@msg the program uses too much memory 프로그램이 메모리를 너무 많이 사용합니다 +@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes 프롬프트의 텍스트가 {MAX_PROMPT_BYTES}바이트보다 깁니다 +@msg the radix must be between 2 and 36 기수는 2에서 36 사이여야 합니다 +@msg the rectangle must have a width and a height 사각형에는 너비와 높이가 있어야 합니다 +@msg the rectangle needs a size and must lie on the canvas 사각형에는 크기가 있어야 하고 캔버스 위에 있어야 합니다 +@msg the rectangle or line reaches too many paths to check 사각형이나 선이 닿는 패스가 너무 많아 확인할 수 없습니다 +@msg the right side of 'in' is not an object 'in'의 오른쪽이 개체가 아닙니다 +@msg the right side of 'instanceof' is not a function 'instanceof'의 오른쪽이 함수가 아닙니다 +@msg the selected art has no width 선택한 아트웍에 너비가 없습니다 +@msg the selected art is nested more than {MAX_DEPTH} levels deep 선택한 아트웍이 {MAX_DEPTH}단계보다 깊게 중첩되어 있습니다 +@msg the selected art is too large to measure 선택한 아트웍이 너무 커서 잴 수 없습니다 +@msg the selection draws nothing 선택 영역이 아무것도 그리지 않습니다 +@msg the selection has no area 선택 영역에 면적이 없습니다 +@msg the selection has no art to export 선택 영역에 내보낼 아트웍이 없습니다 +@msg the selection has no bounds 선택 영역에 경계가 없습니다 +@msg the selection has no graphic style 선택 영역에 그래픽 스타일이 없습니다 +@msg the selection has no outlines to weed around 선택 영역에 둘레를 위딩할 윤곽선이 없습니다 +@msg the slice would leave the canvas 슬라이스가 캔버스를 벗어납니다 +@msg the spacing makes more than {MAX_GROMMETS} grommets 이 간격으로는 아일렛이 {MAX_GROMMETS}개보다 많아집니다 +@msg the spot colour couldn't be added 별색을 추가할 수 없습니다 +@msg the subpath has no ends to extend (it is closed or a single point) 하위 패스에 연장할 끝이 없습니다(닫혀 있거나 점 하나입니다) +@msg the symbols are nested too deeply to expand their instances 심볼이 너무 깊게 중첩되어 있어 인스턴스를 확장할 수 없습니다 +@msg the text file is too large to place as type (at most {_1} MB) 텍스트 파일이 너무 커서 문자로 가져올 수 없습니다(최대 {_1} MB) +@msg the text is empty 텍스트가 비어 있습니다 +@msg the text is over {_1} MB 텍스트가 {_1} MB를 넘습니다 +@msg the top object must be a path 맨 위 개체는 패스여야 합니다 +@msg the trace was cancelled 추적이 취소되었습니다 +@msg the trailer has no size 트레일러에 크기가 없습니다 +@msg the type object is flattened to nothing 문자 개체를 병합하면 아무것도 남지 않습니다 +@msg the value bind was called on is not a function bind를 호출한 값이 함수가 아닙니다 +@msg the value is not a constructor 값이 생성자가 아닙니다 +@msg the value is not a function 값이 함수가 아닙니다 +@msg the value is not a valid XML name 값이 올바른 XML 이름이 아닙니다 +@msg the value is not an XML attribute name 값이 XML 속성 이름이 아닙니다 +@msg the value is not an XML name 값이 XML 이름이 아닙니다 +@msg the value of axis `{tag}` must be a number or null 축 `{tag}`의 값은 숫자 또는 null이어야 합니다 +@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets 변수 라이브러리가 데이터 세트를 {MAX_LIBRARY_DATASETS}개보다 많이 선언합니다 +@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities 변수 라이브러리가 엔티티를 {MAX_LIBRARY_ENTITIES}개보다 많이 선언합니다 +@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables 변수 라이브러리가 변수를 {MAX_LIBRARY_VARIABLES}개보다 많이 선언합니다 +@msg the variable library declares no variables 변수 라이브러리가 변수를 선언하지 않습니다 +@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes 변수 라이브러리가 {MAX_LIBRARY_BYTES}바이트보다 큽니다 +@msg the variable library is not UTF-8 변수 라이브러리가 UTF-8이 아닙니다 +@msg the variable library is not well-formed XML: {e} 변수 라이브러리가 올바른 형식의 XML이 아닙니다: {e} +@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep 변수 라이브러리에 요소가 {MAX_LIBRARY_NESTING}단계보다 깊게 중첩되어 있습니다 +@msg the variable library's document type declaration holds something other than entities of plain text 변수 라이브러리의 문서 형식 선언에 일반 텍스트 엔티티가 아닌 것이 있습니다 +@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes 변수 라이브러리의 엔티티를 확장하면 {MAX_LIBRARY_BYTES}바이트를 넘습니다 +@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines 위딩 선이 아트웍의 윤곽선과 너무 많이 교차해 잴 수 없습니다. 선을 줄이세요 +@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap 위딩 선이 너무 많은 윤곽선 세그먼트에 너무 가깝게 지나가 잴 수 없습니다. 선을 줄이거나 간격을 줄이세요 +@msg the weeding lines would be more than {max_pieces} pieces 위딩 선이 {max_pieces}개보다 많은 조각이 됩니다 +@msg the window has no dock 창에 도킹 영역이 없습니다 +@msg the written PDF has no catalog 기록한 PDF에 카탈로그가 없습니다 +@msg the written PDF has no cross-reference table 기록한 PDF에 상호 참조 테이블이 없습니다 +@msg the written PDF's metadata can't be edited 기록한 PDF의 메타데이터를 편집할 수 없습니다 +@msg the {_1} plate lies outside the file {_1} 색판이 파일 밖에 있습니다 +@msg there are no colour swatches to draw 그릴 색상 견본이 없습니다 +@msg there is already a design named `{name}` `{name}` 디자인이 이미 있습니다 +@msg there is already a design named `{to}` `{to}` 디자인이 이미 있습니다 +@msg there is already a toolbar named `{name}` `{name}` 도구 막대가 이미 있습니다 +@msg there is already a toolbar named `{to}` `{to}` 도구 막대가 이미 있습니다 +@msg there is no banner to mark 표시할 배너가 없습니다 +@msg there is no library: create one first 라이브러리가 없습니다: 먼저 하나 만드세요 +@msg these paths are too degenerate for Shaper 이 패스는 셰이퍼에 쓰기에는 지나치게 퇴화되어 있습니다 +@msg this is a binary DXF file, which can't be read: save it from the CAD app as an ASCII DXF and open that 바이너리 DXF 파일은 읽을 수 없습니다. CAD 앱에서 ASCII DXF로 저장한 후 여세요 +@msg this warp's points can't be edited: use Reset with Mesh first 이 왜곡의 점은 편집할 수 없습니다: 먼저 메시로 재설정을 사용하세요 +@msg thumbnail: no page {v} (the {what} has {count}) thumbnail: 페이지 {v}이(가) 없습니다({what}의 페이지 수: {count}) +@msg tileRange of {what}: {e} {what}의 tileRange: {e} +@msg title can't be empty title은 비워 둘 수 없습니다 +@msg to (the new name) is required to(새 이름)가 필요합니다 +@msg to must be fill or stroke to는 fill 또는 stroke여야 합니다 +@msg to must be smooth or corner to는 smooth 또는 corner여야 합니다 +@msg toExponential() digits must be between 0 and 20 toExponential()의 자릿수는 0에서 20 사이여야 합니다 +@msg toFixed() digits must be between 0 and 20 toFixed()의 자릿수는 0에서 20 사이여야 합니다 +@msg toISOString is not a function toISOString이 함수가 아닙니다 +@msg toPrecision() argument must be between 1 and 21 toPrecision()의 인수는 1에서 21 사이여야 합니다 +@msg toSource: a getter cannot be written as source toSource: getter는 소스로 기록할 수 없습니다 +@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource: 요소 외에 속성이 있거나, 열거할 수 없는 속성이 있거나, 프로토타입이 다른 배열은 소스로 기록할 수 없습니다 +@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource: 열거할 수 없는 속성이 있거나 프로토타입이 Object.prototype이 아닌 개체는 소스로 기록할 수 없습니다 +@msg toSource: the source is too long toSource: 소스가 너무 깁니다 +@msg toSource: the value contains itself toSource: 값이 자신을 포함합니다 +@msg toSource: the value has too many entries toSource: 값에 항목이 너무 많습니다 +@msg toSource: the value is nested too deep toSource: 값이 너무 깊게 중첩되어 있습니다 +@msg toString is not a function toString이 함수가 아닙니다 +@msg too large for a zip archive: package to a folder zip 아카이브에 넣기에는 너무 큽니다: 폴더로 패키지하세요 +@msg too many Shaper edits 셰이퍼 편집이 너무 많습니다 +@msg too many `points` (at most 100000) `points`가 너무 많습니다(최대 100000개) +@msg too many arguments 인수가 너무 많습니다 +@msg too many characters: at most {MAX_CHARS} 글자가 너무 많습니다: 최대 {MAX_CHARS}자 +@msg too many dots for this size: lower the frequency 이 크기에는 점이 너무 많습니다: 주파수를 낮추세요 +@msg too many filters over one file 한 파일에 필터가 너무 많습니다 +@msg too many overlapping paths for Shaper 셰이퍼에 쓰기에는 겹치는 패스가 너무 많습니다 +@msg too many quantifiers 수량자가 너무 많습니다 +@msg too many saved clips 저장된 클립이 너무 많습니다 +@msg too many saved graphics states 저장된 그래픽 상태가 너무 많습니다 +@msg toolbar (a toolbar's name) must be a string toolbar(도구 막대 이름)는 문자열이어야 합니다 +@msg toolbar name can't be empty 도구 막대 이름은 비워 둘 수 없습니다 +@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v}: "flatten"(기본값) 또는 "omit"이어야 합니다 +@msg turn on at least one side 한쪽 이상을 켜세요 +@msg two saved selections would be named `{_1}` 저장된 선택 영역 두 개의 이름이 `{_1}`이(가) 됩니다 +@msg type dimension cannot be represented with this text transform 이 텍스트 변형으로는 문자 치수를 나타낼 수 없습니다 +@msg type dimension must be finite 문자 치수는 유한해야 합니다 +@msg unexpected resources 예상하지 못한 리소스 +@msg unknown CMYK profile `{name}` 알 수 없는 CMYK 프로파일 `{name}` +@msg unknown File Info field `{k}` 알 수 없는 파일 정보 필드 `{k}` +@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; prefix - to turn off; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take an alternate's number as `tag=N` with N from 0 to {_1}, as `salt=2`) 알 수 없는 OpenType 기능 `{t}`(liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt. 끄려면 앞에 -를 붙이세요. swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt는 대체 글리프 번호를 `tag=N` 형식으로 받으며 N은 0에서 {_1} 사이입니다. 예: `salt=2`) +@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) featureChanges의 알 수 없는 OpenType 기능 `{t}`(`features`가 받는 태그를 사용하세요) +@msg unknown PDF preset `{name}` (presets: {_1}) 알 수 없는 PDF 프리셋 `{name}`(프리셋: {_1}) +@msg unknown Warp style 알 수 없는 왜곡 스타일 +@msg unknown alignToPath `{a}` 알 수 없는 alignToPath `{a}` +@msg unknown arrowhead {v} 알 수 없는 화살촉 {v} +@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) 알 수 없는 artboardLayout 키 `{k}`(layout, columns, spacing, rightToLeft) +@msg unknown attribute `{attribute}` 알 수 없는 속성 `{attribute}` +@msg unknown attribute `{k}` 알 수 없는 속성 `{k}` +@msg unknown axis `{other}` (vertical|horizontal|free) 알 수 없는 축 `{other}`(vertical|horizontal|free) +@msg unknown blend mode `{b}` 알 수 없는 혼합 모드 `{b}` +@msg unknown blend mode {b} 알 수 없는 혼합 모드 {b} +@msg unknown brush type `{o}` 알 수 없는 브러시 유형 `{o}` +@msg unknown category `{c}` 알 수 없는 범주 `{c}` +@msg unknown category `{c}` ({_1}) 알 수 없는 범주 `{c}`({_1}) +@msg unknown character `{c}` 알 수 없는 문자 `{c}` +@msg unknown character style number field 알 수 없는 문자 스타일 숫자 필드 +@msg unknown dither `{d}` (diffusion, pattern, noise or none) 알 수 없는 디더 `{d}`(diffusion, pattern, noise 또는 none) +@msg unknown effect `{e}` 알 수 없는 효과 `{e}` +@msg unknown end {e} 알 수 없는 끝 {e} +@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) 알 수 없는 flattenerPreset `{name}`(High Resolution, Medium Resolution, Low Resolution 또는 저장된 프리셋: flattener.presets.list 참조) +@msg unknown format `{f}` (vcswatches, gpl, ase or css) 알 수 없는 형식 `{f}`(vcswatches, gpl, ase 또는 css) +@msg unknown from {f} 알 수 없는 from {f} +@msg unknown graph type `{t}` 알 수 없는 그래프 유형 `{t}` +@msg unknown grid colours `{name}` (one of {_1}) 알 수 없는 격자 색상 `{name}`({_1} 중 하나) +@msg unknown gridType `{t}` 알 수 없는 gridType `{t}` +@msg unknown highlight `{v}` 알 수 없는 강조 `{v}` +@msg unknown intent `{i}` 알 수 없는 의도 `{i}` +@msg unknown issue kind {id} 알 수 없는 문제 종류 {id} +@msg unknown issue kind {v} 알 수 없는 문제 종류 {v} +@msg unknown knockout state {k} 알 수 없는 녹아웃 상태 {k} +@msg unknown language `{language}` (one of {_1}) 알 수 없는 언어 `{language}`({_1} 중 하나) +@msg unknown language `{l}` (one of {_1}) 알 수 없는 언어 `{l}`({_1} 중 하나) +@msg unknown method `{m}` ({_1}) 알 수 없는 방법 `{m}`({_1}) +@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) 알 수 없는 모드 `{other}`(gray, rgb, hsb, lab, cmyk 또는 web) +@msg unknown mode `{other}` (mono|cmyk) 알 수 없는 모드 `{other}`(mono|cmyk) +@msg unknown mode `{other}` (rgb|cmyk|gray|global) 알 수 없는 모드 `{other}`(rgb|cmyk|gray|global) +@msg unknown mode `{other}` (set, add, subtract) 알 수 없는 모드 `{other}`(set, add, subtract) +@msg unknown mode `{o}` 알 수 없는 모드 `{o}` +@msg unknown mode {m} 알 수 없는 모드 {m} +@msg unknown model `{other}` 알 수 없는 모델 `{other}` +@msg unknown model `{other}` (keep, new or document) 알 수 없는 모델 `{other}`(keep, new 또는 document) +@msg unknown op {o} 알 수 없는 연산 {o} +@msg unknown order `{o}`: toggle, front, forward, backward or back 알 수 없는 순서 `{o}`: toggle, front, forward, backward 또는 back +@msg unknown overprints `{v}` (preserve, simulate or discard) 알 수 없는 오버프린트 `{v}`(preserve, simulate 또는 discard) +@msg unknown panel 알 수 없는 패널 +@msg unknown panel `{panel}` 알 수 없는 패널 `{panel}` +@msg unknown parameter `{key}` 알 수 없는 매개변수 `{key}` +@msg unknown parameter `{within}settings.{format}`; {why} 알 수 없는 매개변수 `{within}settings.{format}`: {why} +@msg unknown parameter `{within}{key}`; {why} 알 수 없는 매개변수 `{within}{key}`: {why} +@msg unknown pieLegend `{v}` (none, standard or wedges) 알 수 없는 pieLegend `{v}`(none, standard 또는 wedges) +@msg unknown piePosition `{v}` (even, ratio or stacked) 알 수 없는 piePosition `{v}`(even, ratio 또는 stacked) +@msg unknown pieSort `{v}` (none, all or first) 알 수 없는 pieSort `{v}`(none, all 또는 first) +@msg unknown plate `{n}` 알 수 없는 색판 `{n}` +@msg unknown preference `{k}` 알 수 없는 환경 설정 `{k}` +@msg unknown profile {id} (see stroke.widthProfile.list) 알 수 없는 프로파일 {id}(stroke.widthProfile.list 참조) +@msg unknown proof target `{t}` 알 수 없는 교정 대상 `{t}` +@msg unknown quotes key `{k}` (double, single) 알 수 없는 따옴표 키 `{k}`(double, single) +@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) 알 수 없는 색상 축소 방식 `{reduction}`(selective, perceptual, adaptive, web, gray, blackWhite, uniform 또는 document) +@msg unknown resolution `{v}` 알 수 없는 해상도 `{v}` +@msg unknown rule `{rule}` ({_1}) 알 수 없는 규칙 `{rule}`({_1}) +@msg unknown rule `{r}` (see color.harmony) 알 수 없는 규칙 `{r}`(color.harmony 참조) +@msg unknown setting `{k}` 알 수 없는 설정 `{k}` +@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) 알 수 없는 모양 `{v}`(circle|ellipse|square|diamond|line) +@msg unknown spacing `{o}` (smooth|steps|distance) 알 수 없는 간격 `{o}`(smooth|steps|distance) +@msg unknown tileType `{t}` ({_1}) 알 수 없는 tileType `{t}`({_1}) +@msg unknown units 알 수 없는 단위 +@msg unknown units `{u}` 알 수 없는 단위 `{u}` +@msg unknown value axis `{v}` (left, right or both) 알 수 없는 값 축 `{v}`(left, right 또는 both) +@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) 알 수 없는 변형 `{v}`(tintsShades|warmCool|vividMuted) +@msg unknown warp style `{s}` 알 수 없는 왜곡 스타일 `{s}` +@msg unknown {key} `{v}` (none, short or full) 알 수 없는 {key} `{v}`(none, short 또는 full) +@msg unknown {key} profile `{n}` 알 수 없는 {key} 프로파일 `{n}` +@msg unknown {k} key `{key}` (size, position) 알 수 없는 {k} 키 `{key}`(size, position) +@msg unmatched ')' 짝이 맞지 않는 ')' +@msg unreadable object 읽을 수 없는 개체 +@msg unsafe destination file name 안전하지 않은 대상 파일 이름 +@msg unterminated character class 닫히지 않은 문자 클래스 +@msg unterminated group 닫히지 않은 그룹 +@msg use paths or compound paths 패스나 컴파운드 패스를 사용하세요 +@msg useArtboards must be true or false, not {v} useArtboards는 {v}이(가) 아니라 true 또는 false여야 합니다 +@msg value for `{var}` must match its variable kind `{var}`의 값은 변수 종류와 맞아야 합니다 +@msg variable `{_1}` is already defined 변수 `{_1}`이(가) 이미 정의되어 있습니다 +@msg variable `{name}` has no `trait` 변수 `{name}`에 `trait`이(가) 없습니다 +@msg variable `{name}` is already defined 변수 `{name}`이(가) 이미 정의되어 있습니다 +@msg variable `{name}` is declared twice 변수 `{name}`이(가) 두 번 선언되었습니다 +@msg variable `{to}` already exists 변수 `{to}`이(가) 이미 있습니다 +@msg view is required view가 필요합니다 +@msg view must be one of {_1} view는 {_1} 중 하나여야 합니다 +@msg warpStyle can only edit an applied Warp effect warpStyle은 적용된 왜곡 효과만 편집할 수 있습니다 +@msg width and height must be more than 0 and at most {limit} 너비와 높이는 0보다 크고 {limit} 이하여야 합니다 +@msg width factors must be finite 너비 계수는 유한해야 합니다 +@msg width must be positive width는 양수여야 합니다 +@msg width out of range 너비가 범위를 벗어났습니다 +@msg width/height must be > 0 너비/높이는 0보다 커야 합니다 +@msg with no hem and no grommets there is nothing to mark 단 접기와 아일렛이 모두 없으면 표시할 것이 없습니다 +@msg wordGap and lineGap need area type in the selection wordGap과 lineGap을 쓰려면 선택 항목에 영역 문자가 있어야 합니다 +@msg x and y must be finite numbers x와 y는 유한한 숫자여야 합니다 +@msg x and y must be given together x와 y는 함께 지정해야 합니다 +@msg xpath needs its variables as an object xpath는 변수를 개체로 받아야 합니다 +@msg xpath needs the expression as a string xpath는 식을 문자열로 받아야 합니다 +@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone에는 기준이 필요합니다: anchor로 패널 옆에 도킹하거나 side(Start 또는 End)로 새 도크 열에 도킹하세요 +@msg zoom must be positive zoom은 양수여야 합니다 +@msg {C}: no effect at index {list} {C}: 인덱스 {list}에 효과가 없습니다 +@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET}은(는) 보호되어 있어 그대로 유지됩니다 +@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET}은(는) 보호되어 있습니다: 설정을 다른 이름으로 저장하세요 +@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED}. 또한 설정과 캐시를 {_1}에서 {_2}(으)로 되돌려 옮길 수 없습니다: {e} +@msg {LABEL} produced an empty result {LABEL}의 결과가 비어 있습니다 +@msg {_1} MB of document, at most {_2} MB 문서 {_1} MB, 최대 {_2} MB +@msg {_1} MB of images, at most {_2} MB 이미지 {_1} MB, 최대 {_2} MB +@msg {_1} bytes of RGBA data for a {width} × {height} image {width} × {height} 이미지에 대한 RGBA 데이터 {_1}바이트 +@msg {_1} can't be read, or changed since it was read: relink or update it first {_1}을(를) 읽을 수 없거나 읽은 뒤에 바뀌었습니다: 먼저 다시 연결하거나 업데이트하세요 +@msg {_1} can't go into {into} {_1}은(는) {into}에 넣을 수 없습니다 +@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1}은(는) 인쇄를 저해상도로 제한할 수 없습니다: PDF 1.4 이상을 선택하거나 고해상도 인쇄를 허용하세요 +@msg {_1} files can't be opened yet {_1} 파일은 아직 열 수 없습니다 +@msg {_1} files can't be password-protected: remove the passwords or choose no standard {_1} 파일은 암호로 보호할 수 없습니다: 암호를 제거하거나 표준을 선택하지 마세요 +@msg {_1} files can't be {_2} {_1} 파일은 {_2}일 수 없습니다 +@msg {_1} files can't carry editing data: turn off preserveEditing {_1} 파일에는 편집 데이터를 담을 수 없습니다: preserveEditing을 끄세요 +@msg {_1} files can't have PDF layers: turn off createLayers {_1} 파일에는 PDF 레이어를 둘 수 없습니다: createLayers를 끄세요 +@msg {_1} files need a creation date, and this platform has no clock to give one {_1} 파일에는 작성 날짜가 필요하지만 이 플랫폼에는 날짜를 알려 줄 시계가 없습니다 +@msg {_1} is not a folder: {e} {_1}은(는) 폴더가 아닙니다: {e} +@msg {_1} isn't a path {_1}은(는) 패스가 아닙니다 +@msg {_1} options: {e} {_1} 옵션: {e} +@msg {_1} paints nothing to rasterize {_1}에는 래스터화할 칠이 없습니다 +@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) {_1} 암호에는 ASCII 문자를 {MAX_LEGACY_PASSWORD}자까지 쓸 수 있습니다(PDF 1.7 이상은 모든 문자를 받습니다) +@msg {_1} with linked images or an image map writes several files: use document.export 연결된 이미지나 이미지 맵이 있는 {_1}은(는) 여러 파일을 기록합니다: document.export를 사용하세요 +@msg {_1} writes one file per artboard here: use document.export, or name one artboard 여기서 {_1}은(는) 아트보드마다 파일을 하나씩 기록합니다: document.export를 사용하거나 아트보드 하나를 지정하세요 +@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2}픽셀은 {_3}에 너무 큽니다(한 변 최대 {max}픽셀): 비율을 낮추세요 +@msg {_1}: belongs to another account {_1}: 다른 계정의 소유입니다 +@msg {_1}: not a folder of its own {_1}: 자체 폴더가 아닙니다 +@msg {_1}: not a plain file {_1}: 일반 파일이 아닙니다 +@msg {_1}: {_2} and {_3} are in the way {_1}: {_2}와(과) {_3}이(가) 가로막고 있습니다 +@msg {code}: {0} {code}: {0} +@msg {dest}: {e} {dest}: {e} +@msg {dir}: {e} {dir}: {e} +@msg {doc} has no folder {doc}에는 폴더가 없습니다 +@msg {file} isn't linked {file}은(는) 연결되어 있지 않습니다 +@msg {file} names more than one linked JavaScript file {file}이(가) 연결된 JavaScript 파일을 둘 이상 지정합니다 +@msg {folder} is not a package folder {folder}은(는) 패키지 폴더가 아닙니다 +@msg {folder}: more than {max} files and folders {folder}: 파일과 폴더가 {max}개를 넘습니다 +@msg {folder}: {e} {folder}: {e} +@msg {g} is not a Live Paint group {g}은(는) 라이브 페인트 그룹이 아닙니다 +@msg {id} is not a slice {id}은(는) 슬라이스가 아닙니다 +@msg {key} must be 0–{MAX_ADD_AROUND} pt {key}은(는) 0–{MAX_ADD_AROUND} pt여야 합니다 +@msg {key} must be [x,y] {key}은(는) [x,y]여야 합니다 +@msg {key} must be a percentage {min}..100 {key}은(는) {min}..100 사이의 백분율이어야 합니다 +@msg {key} must be a positive number, not {n} {key}은(는) {n}이(가) 아니라 양수여야 합니다 +@msg {key} must be a positive number, not {v} {key}은(는) {v}이(가) 아니라 양수여야 합니다 +@msg {key} must be one of {_1} {key}은(는) {_1} 중 하나여야 합니다 +@msg {key} must be {_1}, got {s} {key}은(는) {_1}이어야 합니다({s} 입력됨) +@msg {key} must name a floating panel {key}은(는) 떠 있는 패널을 지정해야 합니다 +@msg {key} must name a panel in another group {key}은(는) 다른 그룹의 패널을 지정해야 합니다 +@msg {key} must name a panel that shows {key}은(는) 표시된 패널을 지정해야 합니다 +@msg {key} must name a registered panel {key}은(는) 등록된 패널을 지정해야 합니다 +@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) {kind} 파일에는 투명도가 없는데 이 파일에는 {what}이(가) 있게 됩니다: 먼저 병합하세요(object.flattenTransparency) +@msg {kind} images can't be opened (see document.formats) {kind} 이미지는 열 수 없습니다(document.formats 참조) +@msg {k} `{v}` is not one of the choices {k} `{v}`은(는) 선택 항목 중 하나가 아닙니다 +@msg {k} has control characters{_1} {k}에 제어 문자가 있습니다{_1} +@msg {k} is too long (at most {MAX_TEXT} characters) {k}이(가) 너무 깁니다(최대 {MAX_TEXT}자) +@msg {k} must be > 0 {k}은(는) 0보다 커야 합니다 +@msg {k} must be a number (pt) or a length such as "210 mm" {k}은(는) 숫자(pt) 또는 "210 mm" 같은 길이여야 합니다 +@msg {k} must be a number from {min} to {max} {k}은(는) {min}에서 {max} 사이의 숫자여야 합니다 +@msg {k} must be a number or "auto" {k}은(는) 숫자 또는 "auto"여야 합니다 +@msg {k} must be a string {k}은(는) 문자열이어야 합니다 +@msg {k} must be text {k}은(는) 텍스트여야 합니다 +@msg {k} must be true or false {k}은(는) true 또는 false여야 합니다 +@msg {k} must be {{size?, position?}} {k}은(는) {{size?, position?}}여야 합니다 +@msg {k}: a custom arrowhead's path needs an area to fill and at most {most} anchors and {most} subpaths, each anchor within {far} head units of its tip {k}: 사용자 정의 화살촉의 패스에는 칠할 영역이 있어야 하고 기준점은 {most}개 이하, 하위 패스는 {most}개 이하여야 하며, 각 기준점은 끝에서 화살촉 단위로 {far} 이내에 있어야 합니다 +@msg {k}: bad custom arrowhead path: {e} {k}: 잘못된 사용자 정의 화살촉 패스: {e} +@msg {k}: bad custom arrowhead: {e} {k}: 잘못된 사용자 정의 화살촉: {e} +@msg {k}: can't read `{s}` as a length {k}: `{s}`을(를) 길이로 읽을 수 없습니다 +@msg {k}: custom must be a name {k}: custom은 이름이어야 합니다 +@msg {label} must be a number at most {max} {label}은(는) {max} 이하의 숫자여야 합니다 +@msg {label} must be a number from -1296 to 1296 {label}은(는) -1296에서 1296 사이의 숫자여야 합니다 +@msg {label} must be a number from 1 to 10000 {label}은(는) 1에서 10000 사이의 숫자여야 합니다 +@msg {name} `{s}`: {ids} {name} `{s}`: {ids} +@msg {name} is built in and can't be deleted {name}은(는) 기본 제공 항목이라 삭제할 수 없습니다 +@msg {name} must be a number {name}은(는) 숫자여야 합니다 +@msg {name} must be {lo}–{hi}{unit} (got {v}) {name}은(는) {lo}–{hi}{unit}여야 합니다({v} 입력됨) +@msg {name}: CRC or size mismatch {name}: CRC 또는 크기가 일치하지 않습니다 +@msg {name}: only this Affinity file's embedded preview could be read; use File › Open to see it with its warning, or export SVG or PDF from Affinity before placing {name}: 이 Affinity 파일은 포함된 미리 보기만 읽을 수 있었습니다. 경고와 함께 보려면 파일 › 열기를 사용하거나, 가져오기 전에 Affinity에서 SVG 또는 PDF로 내보내세요 +@msg {name}: {e} {name}: {e} +@msg {native}; its embedded preview can't be read either ({e}) {native}. 포함된 미리 보기도 읽을 수 없습니다({e}) +@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: 복사는 색판이 파일 안에 있는 DCS 파일만 받습니다. 색판 파일이 있는 위치의 파일로 다시 연결하세요 +@msg {path}: larger than {_1} MB {path}: {_1} MB보다 큽니다 +@msg {saving} failed: {e} {saving} 실패: {e} +@msg {shown} is not an XPath 1.0 function {shown}은(는) XPath 1.0 함수가 아닙니다 +@msg {std} files are CMYK, and the file would have colours in {_1} {std} 파일은 CMYK인데 이 파일에는 {_1} 색상이 있게 됩니다 +@msg {std} files embed their fonts, and the file would have one that isn't {std} 파일은 글꼴을 포함하는데 이 파일에는 포함되지 않은 글꼴이 있게 됩니다 +@msg {target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway {target}은(는) 이 문서를 읽어 온 파일이며 읽을 때 빠진 내용이 있습니다({what}): 덮어쓰면 그 내용을 영구히 잃습니다. 다른 파일에 기록하거나, 그래도 바꾸려면 acknowledgeLoss: true를 전달하세요 +@msg {ty} brushes need selected art (or params.{k}) {ty} 브러시에는 선택한 아트웍(또는 params.{k})이 필요합니다 +@msg {v} is not an asset id {v}은(는) 에셋 id가 아닙니다 +@msg {v} names no layer or sublayer {v}에 해당하는 레이어나 하위 레이어가 없습니다 +@msg {what} can't be intertwined {what}은(는) 교차할 수 없습니다 +@msg {what} doesn't fit the paper with its bleed and marks {what}은(는) 도련과 표시를 포함하면 용지에 맞지 않습니다 +@msg {what} is not a function {what}은(는) 함수가 아닙니다 +@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what}은(는) 한 장에 {MAX_COPIES_PER_SHEET}개보다 많이 인쇄됩니다: 크기를 키우세요 +@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what}은(는) 타일 {MAX_TILES}개보다 많이 나뉘어 인쇄됩니다: 크기를 줄이세요 +@msg {what} {i} out of range {what} {i}이(가) 범위를 벗어났습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index cd3aa43966..476f707d22 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -157,6 +157,7 @@ @msg `select` must be true or false `select` moet true of false zijn @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` en `maxAnchors` moeten gehele getallen zijn @msg `{key}` must be a number not below 0 `{key}` moet een getal van minstens 0 zijn +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` moet uit vier getallen van minstens 0 bestaan: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` moet [x, y, width, height] zijn: vier eindige getallen, width en height niet negatief @msg `{min_key}` is greater than `{max_key}` `{min_key}` is groter dan `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` geef minstens een van `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 5f274cef09..db2f6449c6 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4623,6 +4623,7 @@ imageTrace Outline Kontur @msg `select` must be true or false `select` musi mieć wartość true lub false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` i `maxAnchors` muszą być liczbami całkowitymi @msg `{key}` must be a number not below 0 `{key}` musi być liczbą nie mniejszą niż 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` musi być czterema liczbami nie mniejszymi niż 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musi mieć postać [x, y, width, height]: cztery skończone liczby, szerokość i wysokość nieujemne @msg `{min_key}` is greater than `{max_key}` `{min_key}` jest większe niż `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` podaj co najmniej jeden z parametrów `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a9e56041f4..2d5047337a 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3953,6 +3953,7 @@ axis Both Ambos @msg `select` must be true or false `select` deve ser true ou false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devem ser números inteiros @msg `{key}` must be a number not below 0 `{key}` deve ser um número não inferior a 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` deve ser quatro números não inferiores a 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve ser [x, y, width, height]: quatro números finitos, com largura e altura não negativas @msg `{min_key}` is greater than `{max_key}` `{min_key}` é maior que `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` informe pelo menos um de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 4ea3511c36..f4220735a4 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4526,6 +4526,7 @@ axis Both Оба @msg `select` must be true or false `select` должно быть true или false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` и `maxAnchors` должны быть целыми числами @msg `{key}` must be a number not below 0 `{key}` должно быть числом не меньше 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` должно быть четырьмя числами не меньше 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` должно быть [x, y, width, height]: четыре конечных числа, ширина и высота не отрицательные @msg `{min_key}` is greater than `{max_key}` `{min_key}` больше, чем `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажите хотя бы один из параметров `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -7584,3 +7585,119 @@ shape Open Открытый Grommets Люверсы Grommet Inset Отступ люверсов от края Diameter Диаметр + +# Messages of the effects, 3D, image export, color profiles, swatch exchange files, color books and DXF import. +@msg 3D effects require finite coordinates within 100 million points 3D-эффектам нужны конечные координаты в пределах 100 миллионов пунктов +@msg 3D effects require one to ten lights 3D-эффектам нужно от одного до десяти источников света +@msg BMP depth {_1}: 1, 4, 8, 16, 24 or 32 bits per pixel Глубина цвета BMP {_1}: допустимо 1, 4, 8, 16, 24 или 32 бита на пиксель +@msg BMP encoding failed: {width} × {height} pixels doesn't fit the format or the pixel buffer Ошибка кодирования BMP: {width} × {height} пикселей не соответствует формату или буферу пикселей +@msg GIF encoding failed: the palette image is malformed Ошибка кодирования GIF: изображение с палитрой имеет неверную структуру +@msg GIF encoding failed: {e} Ошибка кодирования GIF: {e} +@msg JPEG colour profile: {e} Цветовой профиль JPEG: {e} +@msg JPEG encoding failed: pixel buffer doesn't match the image size Ошибка кодирования JPEG: буфер пикселей не соответствует размеру изображения +@msg JPEG encoding failed: {e} Ошибка кодирования JPEG: {e} +@msg PNG encoding failed: pixel buffer doesn't match the image size Ошибка кодирования PNG: буфер пикселей не соответствует размеру изображения +@msg PNG encoding failed: the palette image is malformed Ошибка кодирования PNG: изображение с палитрой имеет неверную структуру +@msg PNG encoding failed: {e} Ошибка кодирования PNG: {e} +@msg PNG encoding failed: {width} × {height} pixels is not a valid image size Ошибка кодирования PNG: {width} × {height} пикселей — недопустимый размер изображения +@msg PSD colour profile: {e} Цветовой профиль PSD: {e} +@msg PSD encoding failed: the pixel buffer doesn't match the image size Ошибка кодирования PSD: буфер пикселей не соответствует размеру изображения +@msg Revolve profile contains invalid coordinates Профиль вращения содержит недопустимые координаты +@msg Revolve profile is too complex Профиль вращения слишком сложен +@msg Revolve projection is not finite Проекция вращения не является конечной +@msg Revolve requires finite profile coordinates within 100 million points Для вращения нужны конечные координаты профиля в пределах 100 миллионов пунктов +@msg Revolve surface is too complex; reduce Segments or simplify the profile Поверхность вращения слишком сложна; уменьшите «Сегменты» или упростите профиль +@msg Revolve surface is too complex; simplify the profile Поверхность вращения слишком сложна; упростите профиль +@msg Revolve visibility cannot split the projected boundary Расчёт видимости вращения не может разделить спроецированную границу +@msg Revolve visibility creates too many fragments Расчёт видимости вращения создаёт слишком много фрагментов +@msg Revolve visibility creates too many vector anchors Расчёт видимости вращения создаёт слишком много векторных опорных точек +@msg Revolve visibility exceeds its geometry work limit Расчёт видимости вращения превышает свой предел геометрических вычислений +@msg Revolve visibility exceeds its spatial index limit Расчёт видимости вращения превышает свой предел пространственного индекса +@msg Revolve visibility has too many boundary crossings В расчёте видимости вращения слишком много пересечений границы +@msg Revolve visibility has too many input anchors В расчёте видимости вращения слишком много входных опорных точек +@msg Revolve visibility requires convex cap sectors Для расчёта видимости вращения нужны выпуклые секторы торцов +@msg Revolve visibility requires finite projected faces Для расчёта видимости вращения нужны конечные спроецированные грани +@msg SVG options: {e} Параметры SVG: {e} +@msg Save for Web settings: {e} Параметры «Сохранить для web»: {e} +@msg TIFF colour profile: {e} Цветовой профиль TIFF: {e} +@msg TIFF encoding failed: the pixel buffer doesn't match the image size Ошибка кодирования TIFF: буфер пикселей не соответствует размеру изображения +@msg TIFF encoding failed: {e} Ошибка кодирования TIFF: {e} +@msg Targa depth {_1}: 16, 24 or 32 bits per pixel Глубина цвета Targa {_1}: допустимо 16, 24 или 32 бита на пиксель +@msg Targa encoding failed: the pixel buffer doesn't match the image size Ошибка кодирования Targa: буфер пикселей не соответствует размеру изображения +@msg The 3D projection is not finite 3D-проекция не является конечной +@msg The 3D shadow is too complex; turn Shadows off or simplify the art 3D-тень слишком сложна; отключите «Тени» или упростите графику +@msg The shape contains invalid coordinates Фигура содержит недопустимые координаты +@msg Too many Revolve faces for visibility evaluation Слишком много граней вращения для расчёта видимости +@msg VectorCraft reads at most {ASE_MAX_ENTRIES} colors and groups from a swatch exchange file, and this library has {entries} VectorCraft читает из файла обмена образцами не более {ASE_MAX_ENTRIES} цветов и групп, а в этой библиотеке их {entries} +@msg WebP encoding failed: {e} Ошибка кодирования WebP: {e} +@msg [{j}] `{_1}`: {why} [{j}] `{_1}`: {why} +@msg `answers[{i}]` {e} `answers[{i}]` {e} +@msg `view` is smallThumb, mediumThumb, largeThumb, smallList or largeList `view` принимает значения smallThumb, mediumThumb, largeThumb, smallList или largeList +@msg a line can't run from a point to itself линия не может идти от точки к самой себе +@msg a line needs at least two points линии нужны хотя бы две точки +@msg a number cannot be read не удаётся прочитать число +@msg a swatch exchange block is too long блок файла обмена образцами слишком длинный +@msg a swatch exchange name is too long имя в файле обмена образцами слишком длинное +@msg a {sw} × {sh} image can't be resized to {width} × {height} pixels размер изображения {sw} × {sh} нельзя изменить до {width} × {height} пикселей +@msg a {width} × {height} image is too large изображение {width} × {height} слишком велико +@msg a {width} × {height} image: {e} изображение {width} × {height}: {e} +@msg an image could not be written: {e} не удалось записать изображение: {e} +@msg an image that couldn't be read was left out изображение, которое не удалось прочитать, пропущено +@msg can't open “{name}”: {e} не удаётся открыть «{name}»: {e} +@msg cannot align zip entries to {align} bytes не удаётся выровнять записи zip по {align} байт +@msg cannot open the automation {kind} root `{_1}`: {e} не удаётся открыть корневую папку автоматизации ({kind}) `{_1}`: {e} +@msg cannot read profile: {0} не удаётся прочитать профиль: {0} +@msg color book version {version} isn't supported (only 1) версия цветовой книги {version} не поддерживается (только 1) +@msg color books in color space {other} aren't supported (only RGB, CMYK and Lab) цветовые книги в цветовом пространстве {other} не поддерживаются (только RGB, CMYK и Lab) +@msg file name too long to zip: {_1} имя файла слишком длинное для zip: {_1} +@msg file name too long to zip: {name} имя файла слишком длинное для zip: {name} +@msg freeform line {i}: {e} произвольная линия {i}: {e} +@msg freeform point {i}: {e} произвольная точка {i}: {e} +@msg give `{key}` or `{other}`, not both укажите `{key}` или `{other}`, но не то и другое +@msg grayscale profiles can't be working spaces yet профили оттенков серого пока не могут быть рабочими пространствами +@msg input contains NaN or infinite coordinates входные данные содержат координаты NaN или бесконечные координаты +@msg input path could not be closed не удалось замкнуть входной контур +@msg invalid ICC profile: {0} недопустимый профиль ICC: {0} +@msg invalid scale {scale} недопустимый масштаб {scale} +@msg its localized names take more than {_1} KB его локализованные имена занимают более {_1} КБ +@msg its objects are nested too deep ({e}) его объекты вложены слишком глубоко ({e}) +@msg its tag table lists the `{_1}` tag more than once в его таблице тегов тег `{_1}` указан более одного раза +@msg not a graphic style library: {e} не библиотека графических стилей: {e} +@msg not a swatch library (.vcswatches or .gpl) не библиотека образцов (.vcswatches или .gpl) +@msg not a swatch library (format `{_1}`) не библиотека образцов (формат `{_1}`) +@msg not a swatch library: {e} не библиотека образцов: {e} +@msg not an action set file ({e}) не файл набора действий ({e}) +@msg profile colour space {other} is not RGB, CMYK or Gray цветовое пространство профиля {other} не является RGB, CMYK или оттенками серого +@msg regular expression too large регулярное выражение слишком велико +@msg script file is not UTF-8 text: {value} файл скрипта не является текстом UTF-8: {value} +@msg stop {i}: {e} точка перехода {i}: {e} +@msg swatch exchange version {major}.{minor} isn't supported (only 1.x) версия файла обмена образцами {major}.{minor} не поддерживается (только 1.x) +@msg the color book holds more than {ASE_MAX_ENTRIES} colors в цветовой книге больше {ASE_MAX_ENTRIES} цветов +@msg the color book is cut short цветовая книга обрезана +@msg the color book's names take more than {_1} MB имена цветовой книги занимают более {_1} МБ +@msg the image would be {w} × {h} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {_1} megapixels: lower the scale or resolution изображение имело бы размер {w} × {h} пикселей; растровый экспорт ограничен {MAX_RASTER_SIDE} пикселями по стороне и {_1} мегапикселями: уменьшите масштаб или разрешение +@msg the optimised image can't be read back: {e} оптимизированное изображение не удаётся прочитать обратно: {e} +@msg the pixel buffer doesn't match the image size буфер пикселей не соответствует размеру изображения +@msg the preview: {e} изображение для просмотра: {e} +@msg the request may not read the New Document profiles folder запросу не разрешено читать папку профилей нового документа +@msg the shapes are too degenerate to combine фигуры слишком вырождены для объединения +@msg the shapes cross themselves and each other too often to combine фигуры слишком часто пересекают себя и друг друга для объединения +@msg the swatch exchange file holds more than {ASE_MAX_ENTRIES} colors and groups в файле обмена образцами больше {ASE_MAX_ENTRIES} цветов и групп +@msg the swatch exchange file is cut short файл обмена образцами обрезан +@msg the {_1} plate can't be read: {e} не удаётся прочитать пластину {_1}: {e} +@msg this binary DXF file is damaged or cut short этот двоичный файл DXF повреждён или обрезан +@msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that это чертёж DWG, который невозможно прочитать: сохраните его из приложения CAD как DXF и откройте его +@msg too many classes слишком много классов +@msg too many groups слишком много групп +@msg too many layers for PSD (at most 32767) слишком много слоёв для PSD (не более 32767) +@msg too many swatch exchange blocks слишком много блоков в файле обмена образцами +@msg unknown colour profile: {0} неизвестный цветовой профиль: {0} +@msg {_1} clashes with a built-in profile name {_1} совпадает с именем встроенного профиля +@msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} пикселей — слишком большой размер для GIF (не более 65535 пикселей по стороне): уменьшите разрешение +@msg {_1}: no usable transform to/from sRGB {_1}: нет пригодного преобразования в sRGB или из sRGB +@msg {name} is not a CMYK profile {name} — не профиль CMYK +@msg {name} is not an RGB profile {name} — не профиль RGB +@msg {path}: could not be read {path}: не удалось прочитать +@msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} пикселей — слишком большой размер для JPEG (не более 65535 пикселей по стороне): уменьшите разрешение +@msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} пикселей — слишком большой размер для Targa (не более 65535 пикселей по стороне): уменьшите разрешение +@msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} пикселей — слишком большой размер для PSD (не более {MAX_SIDE} пикселей по стороне): уменьшите разрешение diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 9a0de40468..1649d0140e 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4397,6 +4397,7 @@ axis Both Båda @msg `select` must be true or false `select` måste vara true eller false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` och `maxAnchors` måste vara heltal @msg `{key}` must be a number not below 0 `{key}` måste vara ett tal som inte är under 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` måste vara fyra tal som inte är under 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` måste vara [x, y, width, height]: fyra ändliga tal, där bredd och höjd inte är negativa @msg `{min_key}` is greater than `{max_key}` `{min_key}` är större än `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` ange minst en av `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index a457467ef6..0fcf935e7e 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4246,6 +4246,7 @@ axis Both Her İkisi @msg `select` must be true or false `select` true veya false olmalıdır @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ve `maxAnchors` tam sayı olmalıdır @msg `{key}` must be a number not below 0 `{key}` 0'dan küçük olmayan bir sayı olmalıdır +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 0'dan küçük olmayan dört sayı olmalıdır: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` [x, y, width, height] olmalıdır: dört sonlu sayı, genişlik ve yükseklik negatif olmamalı @msg `{min_key}` is greater than `{max_key}` `{min_key}`, `{max_key}` değerinden büyük @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` şunlardan en az birini verin: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 18a118822f..168bd4ac84 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4525,6 +4525,7 @@ axis Both Обидві @msg `select` must be true or false `select` має бути true або false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` і `maxAnchors` мають бути цілими числами @msg `{key}` must be a number not below 0 `{key}` має бути числом, не меншим за 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` має бути чотирма числами, не меншими за 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` має бути [x, y, width, height]: чотири скінченні числа, ширина й висота невід’ємні @msg `{min_key}` is greater than `{max_key}` `{min_key}` більше за `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажіть принаймні один із параметрів `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index db36624af7..bfc6b7876a 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4574,6 +4574,7 @@ theme Appearance Giao diện @msg `select` must be true or false `select` phải là true hoặc false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` và `maxAnchors` phải là số nguyên @msg `{key}` must be a number not below 0 `{key}` phải là một số không nhỏ hơn 0 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` phải là bốn số không nhỏ hơn 0: [tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` phải là [x, y, width, height]: bốn số hữu hạn, chiều rộng và chiều cao không âm @msg `{min_key}` is greater than `{max_key}` `{min_key}` lớn hơn `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` hãy cung cấp ít nhất một trong `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 0b0639e602..0d760c21ed 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4694,6 +4694,7 @@ imageTrace Outline 轮廓 @msg `select` must be true or false `select` 必须是 true 或 false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必须是整数 @msg `{key}` must be a number not below 0 `{key}` 必须是不小于 0 的数字 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 必须是四个不小于 0 的数字:[tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必须是 [x, y, width, height]:四个有限数字,宽度和高度不能为负 @msg `{min_key}` is greater than `{max_key}` `{min_key}` 大于 `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 请至少提供以下其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @@ -9148,3 +9149,119 @@ breakText Glyphs 字形 @msg {what} can't be intertwined {what} 不能交织 @msg {what} doesn't fit the paper with its bleed and marks {what} 连同出血和标记在纸张上放不下 @msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} 会打印成超过 {MAX_TILES} 个拼贴:请将其缩小 + +# Messages of the effects, 3D, image export, color profiles, swatch exchange files, color books and DXF import. +@msg 3D effects require finite coordinates within 100 million points 3D 效果需要 1 亿点以内的有限坐标 +@msg 3D effects require one to ten lights 3D 效果需要一到十个光源 +@msg BMP depth {_1}: 1, 4, 8, 16, 24 or 32 bits per pixel BMP 位深度 {_1}:须为每像素 1、4、8、16、24 或 32 位 +@msg BMP encoding failed: {width} × {height} pixels doesn't fit the format or the pixel buffer BMP 编码失败:{width} × {height} 像素与该格式或像素缓冲区不符 +@msg GIF encoding failed: the palette image is malformed GIF 编码失败:调板图像格式错误 +@msg GIF encoding failed: {e} GIF 编码失败:{e} +@msg JPEG colour profile: {e} JPEG 颜色配置文件:{e} +@msg JPEG encoding failed: pixel buffer doesn't match the image size JPEG 编码失败:像素缓冲区与图像尺寸不符 +@msg JPEG encoding failed: {e} JPEG 编码失败:{e} +@msg PNG encoding failed: pixel buffer doesn't match the image size PNG 编码失败:像素缓冲区与图像尺寸不符 +@msg PNG encoding failed: the palette image is malformed PNG 编码失败:调板图像格式错误 +@msg PNG encoding failed: {e} PNG 编码失败:{e} +@msg PNG encoding failed: {width} × {height} pixels is not a valid image size PNG 编码失败:{width} × {height} 像素不是有效的图像尺寸 +@msg PSD colour profile: {e} PSD 颜色配置文件:{e} +@msg PSD encoding failed: the pixel buffer doesn't match the image size PSD 编码失败:像素缓冲区与图像尺寸不符 +@msg Revolve profile contains invalid coordinates 回转轮廓包含无效坐标 +@msg Revolve profile is too complex 回转轮廓过于复杂 +@msg Revolve projection is not finite 回转投影不是有限值 +@msg Revolve requires finite profile coordinates within 100 million points 回转需要 1 亿点以内的有限轮廓坐标 +@msg Revolve surface is too complex; reduce Segments or simplify the profile 回转表面过于复杂;请减少分段数或简化轮廓 +@msg Revolve surface is too complex; simplify the profile 回转表面过于复杂;请简化轮廓 +@msg Revolve visibility cannot split the projected boundary 回转可见性计算无法分割投影边界 +@msg Revolve visibility creates too many fragments 回转可见性计算产生的碎片过多 +@msg Revolve visibility creates too many vector anchors 回转可见性计算产生的矢量锚点过多 +@msg Revolve visibility exceeds its geometry work limit 回转可见性计算超出几何运算上限 +@msg Revolve visibility exceeds its spatial index limit 回转可见性计算超出空间索引上限 +@msg Revolve visibility has too many boundary crossings 回转可见性计算中的边界交叉过多 +@msg Revolve visibility has too many input anchors 回转可见性计算的输入锚点过多 +@msg Revolve visibility requires convex cap sectors 回转可见性计算需要凸形端盖扇区 +@msg Revolve visibility requires finite projected faces 回转可见性计算需要有限的投影面 +@msg SVG options: {e} SVG 选项:{e} +@msg Save for Web settings: {e} 存储为 Web 所用格式设置:{e} +@msg TIFF colour profile: {e} TIFF 颜色配置文件:{e} +@msg TIFF encoding failed: the pixel buffer doesn't match the image size TIFF 编码失败:像素缓冲区与图像尺寸不符 +@msg TIFF encoding failed: {e} TIFF 编码失败:{e} +@msg Targa depth {_1}: 16, 24 or 32 bits per pixel Targa 位深度 {_1}:须为每像素 16、24 或 32 位 +@msg Targa encoding failed: the pixel buffer doesn't match the image size Targa 编码失败:像素缓冲区与图像尺寸不符 +@msg The 3D projection is not finite 3D 投影不是有限值 +@msg The 3D shadow is too complex; turn Shadows off or simplify the art 3D 阴影过于复杂;请关闭阴影或简化图稿 +@msg The shape contains invalid coordinates 形状包含无效坐标 +@msg Too many Revolve faces for visibility evaluation 回转的面过多,无法进行可见性计算 +@msg VectorCraft reads at most {ASE_MAX_ENTRIES} colors and groups from a swatch exchange file, and this library has {entries} VectorCraft 最多从色板交换文件中读取 {ASE_MAX_ENTRIES} 个颜色和组,而此库有 {entries} 个 +@msg WebP encoding failed: {e} WebP 编码失败:{e} +@msg [{j}] `{_1}`: {why} [{j}] `{_1}`:{why} +@msg `answers[{i}]` {e} `answers[{i}]` {e} +@msg `view` is smallThumb, mediumThumb, largeThumb, smallList or largeList `view` 必须是 smallThumb、mediumThumb、largeThumb、smallList 或 largeList +@msg a line can't run from a point to itself 曲线不能从一个点连到该点自身 +@msg a line needs at least two points 曲线至少需要两个点 +@msg a number cannot be read 无法读取数字 +@msg a swatch exchange block is too long 色板交换文件中的某个块过长 +@msg a swatch exchange name is too long 色板交换文件中的某个名称过长 +@msg a {sw} × {sh} image can't be resized to {width} × {height} pixels 无法将 {sw} × {sh} 的图像调整为 {width} × {height} 像素 +@msg a {width} × {height} image is too large {width} × {height} 的图像过大 +@msg a {width} × {height} image: {e} {width} × {height} 的图像:{e} +@msg an image could not be written: {e} 无法写入某个图像:{e} +@msg an image that couldn't be read was left out 已略去一个无法读取的图像 +@msg can't open “{name}”: {e} 无法打开“{name}”:{e} +@msg cannot align zip entries to {align} bytes 无法将 zip 条目对齐到 {align} 字节 +@msg cannot open the automation {kind} root `{_1}`: {e} 无法打开自动化 {kind} 根目录 `{_1}`:{e} +@msg cannot read profile: {0} 无法读取配置文件:{0} +@msg color book version {version} isn't supported (only 1) 不支持色谱版本 {version} (仅支持 1) +@msg color books in color space {other} aren't supported (only RGB, CMYK and Lab) 不支持色彩空间为 {other} 的色谱 (仅支持 RGB、CMYK 和 Lab) +@msg file name too long to zip: {_1} 文件名过长,无法放入 zip:{_1} +@msg file name too long to zip: {name} 文件名过长,无法放入 zip:{name} +@msg freeform line {i}: {e} 自由曲线 {i}:{e} +@msg freeform point {i}: {e} 自由点 {i}:{e} +@msg give `{key}` or `{other}`, not both 请提供 `{key}` 或 `{other}`,不要同时提供两者 +@msg grayscale profiles can't be working spaces yet 灰度配置文件目前还不能用作工作空间 +@msg input contains NaN or infinite coordinates 输入包含 NaN 或无穷大坐标 +@msg input path could not be closed 无法闭合输入路径 +@msg invalid ICC profile: {0} ICC 配置文件无效:{0} +@msg invalid scale {scale} 缩放比例 {scale} 无效 +@msg its localized names take more than {_1} KB 其本地化名称超过 {_1} KB +@msg its objects are nested too deep ({e}) 其对象嵌套过深 ({e}) +@msg its tag table lists the `{_1}` tag more than once 其标签表多次列出 `{_1}` 标签 +@msg not a graphic style library: {e} 不是图形样式库:{e} +@msg not a swatch library (.vcswatches or .gpl) 不是色板库 (.vcswatches 或 .gpl) +@msg not a swatch library (format `{_1}`) 不是色板库 (格式为 `{_1}`) +@msg not a swatch library: {e} 不是色板库:{e} +@msg not an action set file ({e}) 不是动作组文件 ({e}) +@msg profile colour space {other} is not RGB, CMYK or Gray 配置文件的色彩空间 {other} 不是 RGB、CMYK 或灰度 +@msg regular expression too large 正则表达式过大 +@msg script file is not UTF-8 text: {value} 脚本文件不是 UTF-8 文本:{value} +@msg stop {i}: {e} 色标 {i}:{e} +@msg swatch exchange version {major}.{minor} isn't supported (only 1.x) 不支持色板交换文件版本 {major}.{minor} (仅支持 1.x) +@msg the color book holds more than {ASE_MAX_ENTRIES} colors 色谱包含的颜色超过 {ASE_MAX_ENTRIES} 种 +@msg the color book is cut short 色谱数据不完整 +@msg the color book's names take more than {_1} MB 色谱的名称超过 {_1} MB +@msg the image would be {w} × {h} pixels; raster exports are limited to {MAX_RASTER_SIDE} pixels per side and {_1} megapixels: lower the scale or resolution 图像将为 {w} × {h} 像素;栅格导出限制为每边 {MAX_RASTER_SIDE} 像素和 {_1} 百万像素:请降低缩放比例或分辨率 +@msg the optimised image can't be read back: {e} 无法回读优化后的图像:{e} +@msg the pixel buffer doesn't match the image size 像素缓冲区与图像尺寸不符 +@msg the preview: {e} 预览:{e} +@msg the request may not read the New Document profiles folder 此请求无权读取新建文档配置文件文件夹 +@msg the shapes are too degenerate to combine 这些形状过于退化,无法合并 +@msg the shapes cross themselves and each other too often to combine 这些形状的自身交叉和相互交叉过多,无法合并 +@msg the swatch exchange file holds more than {ASE_MAX_ENTRIES} colors and groups 色板交换文件包含的颜色和组超过 {ASE_MAX_ENTRIES} 个 +@msg the swatch exchange file is cut short 色板交换文件不完整 +@msg the {_1} plate can't be read: {e} 无法读取 {_1} 印版:{e} +@msg this binary DXF file is damaged or cut short 此二进制 DXF 文件已损坏或不完整 +@msg this is a DWG drawing, which can't be read: save it from the CAD app as a DXF and open that 这是 DWG 图纸,无法读取:请在 CAD 应用中将其保存为 DXF,然后打开该文件 +@msg too many classes 字符类过多 +@msg too many groups 分组过多 +@msg too many layers for PSD (at most 32767) 图层过多,PSD 无法容纳 (最多 32767 个) +@msg too many swatch exchange blocks 色板交换文件中的块过多 +@msg unknown colour profile: {0} 未知的颜色配置文件:{0} +@msg {_1} clashes with a built-in profile name {_1} 与内置配置文件名称冲突 +@msg {_1} × {_2} pixels is too large for GIF (at most 65535 pixels a side): lower the resolution {_1} × {_2} 像素对 GIF 而言过大 (每边最多 65535 像素):请降低分辨率 +@msg {_1}: no usable transform to/from sRGB {_1}:没有可用的与 sRGB 之间的转换 +@msg {name} is not a CMYK profile {name} 不是 CMYK 配置文件 +@msg {name} is not an RGB profile {name} 不是 RGB 配置文件 +@msg {path}: could not be read {path}:无法读取 +@msg {width} × {height} pixels is too large for JPEG (at most 65535 pixels a side): lower the resolution {width} × {height} 像素对 JPEG 而言过大 (每边最多 65535 像素):请降低分辨率 +@msg {width} × {height} pixels is too large for Targa (at most 65535 pixels a side): lower the resolution {width} × {height} 像素对 Targa 而言过大 (每边最多 65535 像素):请降低分辨率 +@msg {w} × {h} pixels is too large for PSD (at most {MAX_SIDE} pixels a side): lower the resolution {w} × {h} 像素对 PSD 而言过大 (每边最多 {MAX_SIDE} 像素):请降低分辨率 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index e72c0d1a0d..899520f58e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2408,6 +2408,7 @@ file-filter Templates 範本 @msg `select` must be true or false `select` 必須是 true 或 false @msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必須是整數 @msg `{key}` must be a number not below 0 `{key}` 必須是不小於 0 的數字 +@msg `{key}` must be four numbers not below 0: [tl, tr, br, bl] `{key}` 必須是四個不小於 0 的數字:[tl, tr, br, bl] @msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必須是 [x, y, width, height]:四個有限數字,寬度和高度不能為負 @msg `{min_key}` is greater than `{max_key}` `{min_key}` 大於 `{max_key}` @msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 請至少提供下列其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 924950c1cb..eec2a66831 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -171,14 +171,20 @@ mod tests { app.run("file.save", json!({"path": "/out/b.vectorcraft"})).unwrap(); assert_eq!((app.ui.status.as_str(), &app.ui.status_notes), ("Saved /out/b.vectorcraft", &None)); - // A PDF export of a knockout group and of type in a font that isn't installed. + // A PDF export of a knockout group whose soft masks would draw a brushed stroke, and of type + // in a font that isn't installed. app.run("text.create", json!({"x": 5, "y": 20, "text": "Ab", "font": "Nonexistent Sans Probe"})).unwrap(); - app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); + let lower = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap()["id"].clone(); + let upper = app.run("shape.rectangle", json!({"x": 25, "y": 15, "width": 30, "height": 20})).unwrap()["id"].clone(); + app.run("brush.apply", json!({"ids": [upper], "name": "3 pt. Round"})).unwrap(); + app.run("object.setProps", json!({"ids": [lower, upper], "opacity": 50, "knockoutShape": true})).unwrap(); + app.run("select.set", json!({"ids": [lower, upper]})).unwrap(); app.run("object.group", json!({})).unwrap(); app.run("transparency.set", json!({"knockout": "on"})).unwrap(); let r = export(&mut app, Some("pdf"), Some("/out/c.pdf".into()), &json!({})).unwrap(); let notes = notes_of(&r); assert_eq!(notes.len(), 2, "{r}"); + assert!(notes.iter().any(|n| n.contains("brushed strokes")), "{r}"); assert_eq!(app.ui.status, exported_with_notes("/out/c.pdf", notes.len(), ¬es[0])); assert_eq!(app.ui.status_notes, Some((app.ui.status.clone(), notes))); } diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index 1e6ca2c8d5..0cb47e6542 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -1219,20 +1219,40 @@ fn axes_cmd(app: &mut VectorcraftApp, axes: Value, phase: widgets::Live) { } /// The Control bar's type controls while type is selected or edited: the Character link (the -/// Character panel in a popover), the font family and style menus and the Font Size combo. -pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { - let Some((s, _)) = text_style(app) else { return }; - let link = ui - .link(egui::RichText::new(tl!("Character:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline()) - .on_hover_text(tl!("Character options")); +/// Character panel in a popover), the font family and style menus and the Font Size combo. They +/// show whole, with `reserve` left after them, or not at all: it returns false when type is selected +/// or edited and the row has no room for them. +pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui, reserve: f32) -> bool { + let Some((s, _)) = text_style(app) else { return true }; + if ui.available_width() < control_bar_width(ui) + reserve { + return false; + } + let link = ui.link(control_bar_link_text(ui)).on_hover_text(tl!("Character options")); widgets::popover(&link, link.clicked(), |ui| { ui.set_width(260.0); show_owned(app, ui, crate::font_menu::WheelOwner::ControlPopover(link.id)); }); - font_pickers(app, ui, &s, ("cb-font", "cb-font-style"), (150.0, 96.0)); - size_field(app, ui, "cb-font-size", 96.0); + let [family, style, size] = CONTROL_BAR_WIDTHS; + font_pickers(app, ui, &s, ("cb-font", "cb-font-style"), (family, style)); + size_field(app, ui, "cb-font-size", size); ui.add_space(4.0); crate::chrome::group_separator(ui); + true +} + +/// The Control bar's widths of the font family menu, the font style dropdown and the Font Size combo. +const CONTROL_BAR_WIDTHS: [f32; 3] = [150.0, 96.0, 96.0]; + +/// The text of the Control bar's Character link, as [`control_bar`] draws it. +fn control_bar_link_text(ui: &Ui) -> egui::RichText { + egui::RichText::new(tl!("Character:")).size(12.0).color(Tokens::get(ui.ctx()).text).underline() +} + +/// The room [`control_bar`]'s link, menus and Font Size combo take in the Control bar's row, with +/// the spacing after each. +fn control_bar_width(ui: &Ui) -> f32 { + let spacing = ui.spacing().item_spacing.x; + widgets::text_width(ui, control_bar_link_text(ui)) + CONTROL_BAR_WIDTHS.iter().sum::() + 4.0 * spacing } /// The Contextual Task Bar's widths of the font family menu, the font style dropdown and the diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index 894b8488f8..c667f01200 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -167,15 +167,19 @@ pub(crate) fn weight_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, weig /// "Stroke"): a click toggles the Stroke panel in a popover under it, and a drag scrubs the weight /// field after it ([`crate::scrub`]). pub(crate) fn link(app: &mut VectorcraftApp, ui: &mut Ui, label: &str) { - let t = Tokens::get(ui.ctx()); // A drag on it scrubs the weight field after it (#4088). - let resp = crate::scrub::link_label(ui, egui::RichText::new(label).size(12.0).color(t.text).underline()).on_hover_text(tl!("Stroke options")); + let resp = crate::scrub::link_label(ui, link_text(ui, label)).on_hover_text(tl!("Stroke options")); widgets::popover(&resp, resp.clicked(), |ui| { ui.set_width(260.0); show(app, ui); }); } +/// The text of the Stroke [`link`] labeled `label`, as it draws it and the Control bar measures it. +pub(crate) fn link_text(ui: &Ui, label: &str) -> egui::RichText { + egui::RichText::new(label).size(12.0).color(Tokens::get(ui.ctx()).text).underline() +} + pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let st = current_stroke(app); @@ -491,12 +495,15 @@ pub fn paint_profile(ui: &Ui, r: Rect, points: Option<&[(f64, f64, f64)]>, color ui.painter().with_clip_rect(r).add(egui::Shape::mesh(mesh)); } +/// The width of the [`profile_dropdown`] button. +pub(crate) const PROFILE_DROPDOWN_WIDTH: f32 = 100.0; + /// The Profile dropdown (Stroke panel, Control bar): the stroke's own silhouette on the button /// (a custom profile too), the built-in profiles and then the saved ones in the list. Returns the /// id or name picked. pub(crate) fn profile_dropdown(app: &VectorcraftApp, ui: &mut Ui, cur: Option<&WidthProfile>) -> Option { let t = Tokens::get(ui.ctx()); - let (r, resp) = ui.allocate_exact_size(vec2(100.0, 24.0), Sense::click()); + let (r, resp) = ui.allocate_exact_size(vec2(PROFILE_DROPDOWN_WIDTH, 24.0), Sense::click()); ui.painter().rect_filled(r, 2, t.input); ui.painter().rect_stroke(r, 2, Stroke::new(1.0, if resp.hovered() { t.text } else { t.input_border }), StrokeKind::Inside); let body = Rect::from_min_max(r.min + vec2(6.0, 5.0), pos2(r.right() - 20.0, r.bottom() - 5.0)); diff --git a/crates/ui-egui/src/tests/document_drag.rs b/crates/ui-egui/src/tests/document_drag.rs index f01314a667..cefe441020 100644 --- a/crates/ui-egui/src/tests/document_drag.rs +++ b/crates/ui-egui/src/tests/document_drag.rs @@ -291,6 +291,54 @@ fn hovering_back_to_source_resumes_move_instead_of_copying() { assert!((bounds.x0 - 150.0).abs() < 0.001 && (bounds.y0 - 150.0).abs() < 0.001, "{bounds:?}"); } +/// Back in the source tab, the move resumes even when coming to the front measured the source's +/// type again in its own composite fonts, which replaces the document without editing it. +#[test] +fn hovering_back_to_a_source_whose_type_is_measured_again_resumes_move() { + let font = |roman: u32| { + json!({ + "name": "CF Tab Drag", + "parts": {"kanji": {"font": "Source Sans 3"}, "roman": {"font": "JetBrains Mono", "size": roman}}, + }) + }; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width":400,"height":300,"name":"Source"})).unwrap(); + app.run("type.compositeFonts.set", json!({"fonts": [font(100)]})).unwrap(); + app.run("text.create", json!({"x": 200, "y": 250, "text": "Ab12", "size": 20, "font": "CF Tab Drag"})).unwrap(); + let id = vectorcraft_doc::NodeId(app.run("shape.rectangle", json!({"x":50,"y":50,"width":100,"height":100})).unwrap()["id"].as_u64().unwrap()); + app.run("file.new", json!({"width":400,"height":300,"name":"Target"})).unwrap(); + app.run("type.compositeFonts.set", json!({"fonts": [font(50)]})).unwrap(); + app.session.set_active(0); + let undo = app.session.active().unwrap().history.undo.len(); + app.select_tool("selection"); + app.ui.task_bar = false; + app.ui.view.rulers = false; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + frame(&mut app, &ctx, 0.0, vec![]); + let tabs = frame(&mut app, &ctx, 0.01, vec![]); + let grab = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(75.0, 80.0)); + frame(&mut app, &ctx, 0.05, vec![Event::PointerMoved(grab), button(grab, true)]); + frame(&mut app, &ctx, 0.1, vec![Event::PointerMoved(grab + egui::vec2(20.0, 10.0))]); + frame(&mut app, &ctx, 0.2, vec![Event::PointerMoved(tabs[1].center())]); + frame(&mut app, &ctx, 0.25, vec![]); + frame(&mut app, &ctx, 0.8, vec![]); + assert_eq!(app.session.active_index(), Some(1)); + let left = app.session.documents()[0].doc.clone(); + frame(&mut app, &ctx, 0.9, vec![Event::PointerMoved(tabs[0].center())]); + frame(&mut app, &ctx, 1.4, vec![]); + assert_eq!(app.session.active_index(), Some(0)); + assert!(!std::sync::Arc::ptr_eq(&app.session.documents()[0].doc, &left), "the source's type is measured again"); + let drop = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(175.0, 180.0)); + frame(&mut app, &ctx, 1.5, vec![Event::PointerMoved(drop)]); + frame(&mut app, &ctx, 1.6, vec![button(drop, false)]); + let st = app.session.active().unwrap(); + assert_eq!(st.selection.objects, vec![id]); + assert_eq!(st.history.undo.len(), undo + 1); + let bounds = st.doc.node(id).unwrap().geometric_bounds().unwrap(); + assert!((bounds.x0 - 150.0).abs() < 0.001 && (bounds.y0 - 150.0).abs() < 0.001, "{bounds:?}"); +} + /// Verify Alt-drag transfers restored originals across tabs without retaining preview duplicates. #[test] fn alt_drag_can_continue_across_multiple_tabs_without_copying_the_source_preview() { @@ -380,6 +428,58 @@ fn source_edits_target_closure_switches_and_focus_loss_cancel_stale_transfers() } } +/// Artwork held over the tab of a document with other assigned color profiles switches to it and +/// drops there: bringing those profiles to the front redraws the source without editing it. +#[test] +fn a_drop_on_a_document_with_other_assigned_profiles_copies_the_artwork() { + const CHILD: &str = "VECTORCRAFT_TEST_DRAG_ASSIGNED_PROFILES"; + if std::env::var_os(CHILD).is_none() { + // Color settings are process-wide: change them in a separate test process. + let output = std::process::Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "tests::document_drag::a_drop_on_a_document_with_other_assigned_profiles_copies_the_artwork", "--nocapture"]) + .env(CHILD, "1") + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"), + "isolated drag test failed:\n{stdout}\n{}", + String::from_utf8_lossy(&output.stderr), + ); + return; + } + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width":400,"height":300,"name":"Source"})).unwrap(); + app.run("shape.rectangle", json!({"x":50,"y":50,"width":100,"height":100})).unwrap(); + app.run("file.new", json!({"width":400,"height":300,"name":"Target"})).unwrap(); + app.run("edit.assignProfile", json!({"cmyk": vectorcraft_color::cms::DEVICE_CMYK})).unwrap(); + app.session.set_active(0); + let source = app.session.active().unwrap().doc.clone(); + app.select_tool("selection"); + app.ui.task_bar = false; + app.ui.view.rulers = false; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + frame(&mut app, &ctx, 0.0, vec![]); + let tabs = frame(&mut app, &ctx, 0.01, vec![]); + let grab = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(75.0, 80.0)); + frame(&mut app, &ctx, 0.05, vec![Event::PointerMoved(grab), button(grab, true)]); + frame(&mut app, &ctx, 0.1, vec![Event::PointerMoved(grab + egui::vec2(20.0, 10.0))]); + assert!(app.session.in_interaction()); + frame(&mut app, &ctx, 0.2, vec![Event::PointerMoved(tabs[1].center())]); + frame(&mut app, &ctx, 0.25, vec![]); + frame(&mut app, &ctx, 0.8, vec![]); + assert_eq!(app.session.active_index(), Some(1)); + let undo = app.session.active().unwrap().history.undo.len(); + let drop = canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(175.0, 180.0)); + frame(&mut app, &ctx, 0.9, vec![Event::PointerMoved(drop)]); + frame(&mut app, &ctx, 1.0, vec![button(drop, false)]); + let placed = app.session.active().unwrap().selection.objects.len(); + let steps = app.session.active().unwrap().history.undo.len(); + assert_eq!((placed, steps), (1, undo + 1), "the drop places the artwork"); + assert_eq!(app.session.documents()[0].doc, source); +} + /// Verify tab-strip relayout under a stationary pointer cannot trigger another document switch. #[test] fn scrolling_tabs_under_a_stationary_drag_does_not_cascade_to_other_documents() { diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index ff249bbed4..d4a51799ca 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -522,7 +522,9 @@ fn the_wheel_steps_the_families_in_properties_and_the_control_bar() { type Draw = fn(&mut VectorcraftApp, &mut egui::Ui); let places: [(&str, Draw); 2] = [ ("Properties", |app, ui| crate::panels::properties::type_sections(app, ui)), - ("Control bar", |app, ui| crate::panels::character::control_bar(app, ui)), + ("Control bar", |app, ui| { + crate::panels::character::control_bar(app, ui, 0.0); + }), ]; let at = |f: &str| FontDb::global().menu_family_list().iter().position(|x| x == f); for (place, draw) in places { diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 17d16c171f..9080695a8e 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -29,17 +29,17 @@ Chinese by `imachus`). Georgian and the original Gujarati catalog are recorded a | Language | Code | UI strings translated | Dialogs / tooltips / messages | Script support | Native review | Status | To `full` (h) | |---|---|---|---|---|---|---|---| | English | `en` | source, 100% | all | Latin | — | full | — | -| Simplified Chinese (Mandarin) | `zh-hans` | 5,102 rows, 791 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | all (3,439 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 40 missing messages for it) | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) | +| Simplified Chinese (Mandarin) | `zh-hans` | 5,102 rows, 791 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | all (3,554 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 55 missing messages for it, which open #7116, #7211, #7330, #7378 and #7405 translate) | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) | | Spanish | `es` | 4,009 / 4,020 (99.7%) | all (604 messages) | Latin | no | full | — (review) | | Hindi | `hi` | 8,782 rows (2026-11-13): 3,014 `@msg`, 47 `@msgplural`, 41 `@plural`; no gaps in the untranslated report | menus, dialogs, tooltips and scanned messages; contributor-approved loanword terminology; agent wording pass | UI and document text shape with HarfRust; installed desktop Devanagari fallback, or a `Deva` craft-fonts face; web font still needed | contributor terminology review; 2026-11-13 wording pass on messages and longer labels, with a glossary-term test; wider proofread welcome | complete catalog, machine-assisted draft | native review, layout and web font | | Gujarati | `gu` | 4,673 rows (2026-11-14): original 4,286 unchanged; all 387 additions contributor-reviewed and approved | menus and dialogs; status/error messages partial | installed Gujarati fonts on desktop; optional `Gujr` craft-fonts fallback; no Gujarati face in the pinned CI/release fonts or web startup list | original catalog reviewed by a native speaker; all 387 later additions approved by the contributor, with 11 wording corrections | contributor-reviewed catalog | remaining messages and a bundled Gujarati web font | | Arabic | `ar` | 0 | none | document text: bidi layout and Paragraph Direction; interface: no right-to-left layout, no Arabic shaping in egui | no | none | 25–40 | -| French | `fr` | 4,020 / 4,020 (100%) | all | Latin | no | full | — (review) | +| French | `fr` | 9,166 rows, 3,436 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 178 missing messages for it | all | Latin | no | full | — (review) | | Portuguese | `pt-br` | 9,260 rows, 3,445 of them `@msg` and 48 `@msgplural` (counted 2026-11-13): the untranslated report lists 39 missing messages for it; one/other plurals | all | Latin | contributor is a Brazilian Portuguese speaker; further native review welcome | full | — (review) | | Indonesian | `id` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one plural form | dialogs and tooltips all; 750 message rows (French has 796) | Latin | no | full | — (review) | | Japanese | `ja` | 4,004 / 4,020 (99.6%) | all | CJK UI font as above; Japanese IME; vertical type partial; Japanese crop marks and composition settings | no | full | — (review) | | German | `de` | 3,952 / 4,020 (98%; menus, literals and messages complete when counted) | all (3,433 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 48 missing messages for it) | Latin | contributor-written, not reviewed | full | — (review) | -| Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | all | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) | +| Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | all (3,438 `@msg` and 48 `@msgplural` rows, counted 2026-11-13: the untranslated report lists 39 missing messages for it) | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) | | Vietnamese | `vi` | 4,886 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form; 4,046 plain + 18 `@plural` + 28 contextual + 794 `@msg` | all | Latin with Vietnamese diacritics (stacked marks attach in precomposed, decomposed and keyboard forms, #4491; glyph tests pass) | contributor is a Vietnamese speaker; AI-assisted draft, further native review welcome | full | — (review) | **Historical 12-language score: ~82%** (equal weights: 100 + 85 + 100 + 0 + 0 + 100 + 100 + 100 + 100 + 98 + 100 + 100, over 12). diff --git a/docs/mcp.md b/docs/mcp.md index 1ad05ba3df..b4dfcea774 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1305,7 +1305,9 @@ Drive a tool like a mouse: Edit a gradient with the Gradient tool's annotator (the bar runs along the vector; stop chips sit 10 px under it, midpoint diamonds 7 px above): a click on the bar adds a stop, dragging a chip moves it (drag it off the bar to -delete it, hold Alt to copy it), dragging the end handle changes the vector. The selected stop +delete it, hold Alt to copy it), dragging the end handle changes the vector. Both ends of a vector drawn on the art, +the start of one a click applies, and the dragged start and end handles snap as drawn points do; with Shift, the end +or the handle keeps to 45° steps and snaps along them. The selected stop (`gradient.selectStop`) takes Delete and ←/→: ```json @@ -2158,7 +2160,7 @@ VectorCraft's own, and `vc.api.has("dollar")` is true where they are available: | `build`, `version` | VectorCraft's version | | `buildDate` | `null` | | `decimalPoint` | `","` for a locale whose language writes a decimal comma (such as `de`, `fr`, `ru`), otherwise `"."` | -| `engineName` | `"VectorCraft"` | +| `engineName` | the name `#targetengine` gave the engine the script runs in, or `"VectorCraft"` without that line | | `error` | the last `Error` object the run threw, caught or not, or `null` before one | | `flags` | `0`, or `512` while localization is on; only `0` and `512` can be set, and setting `512` turns `$.localize` on | | `gc()` | does nothing; the interpreter collects its own garbage | @@ -2166,7 +2168,7 @@ VectorCraft's own, and `vc.api.has("dollar")` is true where they are available: | `global` | the global object | | `hiresTimer` | the microseconds since the run last read it, or since the run started; `0` on the web | | `level`, `memCache` | `0`; setting `memCache` throws | -| `line`, `stack` | the line of the code that reads it, counted in the file that code is in (the script, a file it included or a file `$.evalFile` ran, in this run or an earlier run of its named engine); the running code, outermost first, one a line as `name@file:line` (`[global]` for the script's own code, `[anonymous]` for a function without a name; the file as `$.fileName` gives it, or the run's name for a script given as `source`), at most 64 lines. Code that `eval` or `Function` compiled keeps the line of the source they compiled, in the file `[eval]`; code whose place a named engine no longer keeps is at line `0` of `[unknown]` | +| `line`, `stack` | `$.line` is the line of the code that reads it, counted in the file that code is in (the script, a file it included or a file `$.evalFile` ran, in this run or an earlier run of its named engine). `$.stack` lists the running code, outermost first, one frame a line as `name@file:line`: `name` is `[global]` for code outside a function (in the script, in a file it included, or in code that `$.evalFile` or `eval` ran) and `[anonymous]` for a function without a name, and `file` and `line` are the file and line that code is in, so `file` is the file `$.fileName` gives in that code. It lists the innermost 64 frames; when there are more, a first line `…` stands for the ones left out. In code from a script given as `source`, `file` is that run's name, while `$.fileName` is empty. Code that `eval` or `Function` compiled keeps the line of the source they compiled, in the file `[eval]`, while `$.fileName` there is the file being run; code whose place a named engine no longer keeps is at line `0` of `[unknown]`, and `$.fileName` there is empty | | `locale` | the interface's language as a locale such as `de` or `pt_BR` (`en` when the interface follows the system); a script can set `ll` or `ll_RR`, and `null` or `""` sets the interface's again | | `localize` | `false`; while `true`, `String(object)` of a localization object gives its text for `$.locale` | | `os` | `Windows`, `macOS`, `Linux`, `FreeBSD`, `Unix` or `Web`, without a version | @@ -2510,8 +2512,12 @@ neutral, the default). In a knockout group each child hides what it covers of th pass the enclosing group's setting to their children, off groups never knock out. `knockoutShape: true` makes the object's opacity and opacity mask scale how much it knocks out. `transparency.togglePageKnockoutGroup` and `transparency.togglePageIsolatedBlending` (`{value?}`) treat the whole page as a knockout or isolated group; they are -saved with the document and undoable, and `transparency.info` reports them. PDF and SVG write knockout groups as -soft-masked groups with the same look (the PDF export reports a warning). +saved with the document and undoable, and `transparency.info` reports them. PDF writes a knockout group as a PDF +knockout group, which PDF import reads back as one. Where an element knocks out less than the area it paints +(`knockoutShape: true` with opacity or a mask, or translucent paint), PDF draws each element through one soft mask of +the overlapping elements above it, as SVG does for every knockout group of translucent art. A group whose masks would +hold more than 65,536 objects (PDF) or shapes (SVG), or that has more than 2,048 elements, is written as a PDF knockout +group or a plain SVG group instead, and the export reports a warning. Groups are not isolated unless Isolate Blending is on: a blend mode inside a group with opacity, a blend mode, an opacity mask, knockout or a clip reaches the art below the group, on screen and in PDF (a knockout group's elements @@ -4798,7 +4804,10 @@ type, with the image under it. A group's or layer's own fills and strokes become members are expanded too. A symbol instance whose topmost fill or topmost stroke is visible, has an opacity above 0 and has a geometry effect becomes a group of its art: that fill's effects reshape the filled shapes of the art and that stroke's effects reshape its stroked shapes, which keep their colors, as the canvas draws them (VectorCraft's own -rule); the instance's other fills and strokes are dropped. The result is `{ids}`; when an image in the art can't be decoded, the images made from +rule); the instance's other fills and strokes are dropped. A raster effect on that fill or stroke draws on the filled +or stroked shapes of the art, and expanding makes it an image as for other objects; with no geometry effect, the +instance stays an instance over a shadow or glow image, and a blur, feather or inner glow replaces it with the image. +The result is `{ids}`; when an image in the art can't be decoded, the images made from raster effects do not show it, and the result also carries the `warning` that Flatten Transparency gives. A path's strokes stay strokes, as the [Expand objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/expand-objects.html) help @@ -6596,8 +6605,8 @@ number, naming the key, and place nothing; null is the same as leaving the key o `edit.copy` keeps the copied objects with the document resources they use: image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used; never the built-in [Registration] swatch), the gradient swatches their gradients came from, graphic styles the objects are linked to, character and paragraph styles and -brushes. Width profiles live in the preferences, so they need no copying. Every `edit.paste*` command (also `edit.pasteWithoutFormatting`, which leaves -text styles behind) brings them into the active document in the same undo step: an identical resource is reused, +brushes, with the swatches, patterns and text styles a graph keeps for its series and labels. Width profiles live in the preferences, so they need no copying. Every `edit.paste*` command (also `edit.pasteWithoutFormatting`, which leaves +text styles behind, those of graph labels included) brings them into the active document in the same undo step: an identical resource is reused, a missing one added, and one of the same name that differs is added renamed ("Mark 2"; a copy an earlier paste added is reused), which the pasted objects follow. Linked colours show the document's swatch at their tint (a Lab spot colour as its Spot Colors option says). The result reports `{ids, added, merged, renamed: [{kind, from, @@ -10488,9 +10497,13 @@ give the same result there. A paragraph whose characters differ in line metrics scale, or an inline graphic taller than the text) is composed with Every-line in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line that reaches where the frame is -narrower, or beside a wrap object, gets the room there. Such a paragraph falls back to Single-line when the frame has -no room for all of it, or when the search for its breaks passes VectorCraft's own limits (1,000,000 lines tried and -2,000,000 baseline steps for one paragraph, 2,000,000 lines and 4,000,000 steps for all the paragraphs of a frame). +narrower, or beside a wrap object, gets the room there. When the frame has no room for all of such a paragraph, the +lines it holds are still composed with the whole paragraph: VectorCraft measures the lines past the frame's end as +wide as the span of the last line the frame holds, less their indents. Those lines overflow, or go on in the next +threaded frame, which composes them again at its own widths. Such a paragraph falls back to Single-line when the +frame has no room for its first line, or when the search for its breaks passes VectorCraft's own limits (1,000,000 +lines tried and 2,000,000 baseline steps for one paragraph, 2,000,000 lines and 4,000,000 steps for all the +paragraphs of a frame). The UI command `type.lineComposer` (Cmd+Alt+Shift+C, in remote mode `press_key {key: "C", mods: {cmd: true, alt: true, shift: true}}` or `invoke_menu`) sets `singleLine` when every paragraph of the selected type (or every paragraph the Type tool's selection touches) uses Every-line, else `everyLine`, and returns the composer it set. @@ -11051,6 +11064,33 @@ whole points and Escape cancels. A drag keeps a Contour's own corners and miter options `corners` and `miterLimit` (`tool.setOption`). A border with a gap is two Contours of one shape (the gap first, so it ends up in front), baked with `effect.expandAppearance`, then `object.pathfinder.minusFront`. +## 3D Box and shapes add-on + +The `addon.shapes.*` commands draw rotatable 3D boxes (`addon.shapes.box3d`) and rectangles, ellipses, polygons, stars and +spirals that keep their settings (`addon.shapes.rectangle`, `ellipse`, `polygon`, `star`, `spiral`), and +`addon.shapes.configure` changes those settings on the selected shapes (or `ids`). In a drawing command, an omitted or +null value takes the default in the table. In `configure`, an omitted or null value keeps each shape's setting. A value +outside its range is an error that names the parameter and the range, and the command then draws or changes nothing. +The ranges are VectorCraft's own: + +| Parameter | Commands | Range (default) | +|---|---|---| +| `width`, `height` | `rectangle`, `ellipse`, `box3d` | pt, 0 or more | +| `radius` | `polygon`, `spiral` | pt, 0 or more | +| `radius` (corners) | `rectangle`, `configure` | pt, 0 or more (0); `configure` fits a larger radius to half each rectangle's shorter side | +| `radii` | `rectangle`, `configure` | four numbers, 0 or more: `[tl, tr, br, bl]` | +| `radius1` | `star` | pt, 0 or more | +| `radius2` | `star` | 0.01 to 1 times `radius1` (half of it) | +| `sides`, `points` | `polygon`, `star`, `configure` (`sides`) | a whole number from 3 to 1000 (6 sides, 5 points) | +| `rounded`, `randomized` | `polygon`, `star`, `configure` | −10 to 10 (0) | +| `spokeRatio` | `configure` | 0.01 to 1 | +| `turns` | `spiral`, `configure` | 0.01 to 1024 (3) | +| `divergence` | `spiral`, `configure` | 0 to 1000 (1) | +| `innerRadius` | `spiral`, `configure` | 0 to 0.999 of the radius (0) | +| `arcMode` | `rectangle`, `ellipse`, `polygon`, `star`, `configure` | `whole`, `pie` (or `slice`), `chord`, `arc` (or `open`) (`whole`) | + +A `configure` call that changes no shape records no undo step and returns `{updated: 0}`. + ## Cutting Tools add-on Cutting Tools is a built-in add-on that prepares art for cutting. Its commands are `addon.cutting.bridges`, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b39f7697c3..44c80534e2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -626,6 +626,7 @@ since 2026-10-01. - **`.ai` files keep an image's Warp live:** an embedded image whose appearance is one Warp effect, with no opacity mask, is saved to the `.ai` editing data with an art style of that Warp: its bent pieces are its drawn look, and the image itself follows on `%_` lines, as for other styled objects. Opening an `.ai` file reads such an image as the image with its Warp live, its pixels, placement, name and opacity kept, when the live warp draws as the file's drawn look and the import has a comparison left; otherwise the drawn look stays, with the drawn-look note. The Warp effect applies to bitmap images ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html)), and native files keep live effects ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/supported-file-formats-in-illustrator.html)). Images with an opacity mask, and images with a Warp and another effect or with another geometry effect, are saved and opened as their drawn art. Existing `.ai` files with an embedded image on `%_` lines open differently: before, that image also came in beside its drawn look. Which images keep the Warp and the comparison are VectorCraft's own rules (eps `ai_editing::tests::an_image_with_a_warp_reads_back_with_its_warp_live`, `an_image_whose_warp_draws_otherwise_than_its_look_keeps_its_look`, `an_image_on_hidden_lines_does_not_come_in_beside_its_look`, `other_warped_images_are_written_as_their_drawn_art`, `import::ai::read::ops::tests::an_image_takes_a_style_of_one_warp_alone`). Scores and hours unchanged. - **`.ai` files keep a linked image's Warp live and its link:** a linked image whose appearance is one Warp effect, with no opacity mask, is saved to the `.ai` editing data with an art style of that Warp, as an embedded one is: its bent pieces are its drawn look, and the image itself follows on `%_` lines as a placed file with its path, its folder relative to the document's and its preview. Before, it was saved as its drawn art and its live Warp was lost. Opening the file reads it as the linked image with its Warp live, its link, pixels, placement, name, opacity, hide and lock kept, under the same drawn-look comparison; a hidden image with a Warp, embedded or linked, is now compared with its look drawn shown, and keeps its Warp too. A linked image with a Warp in an opacity mask's art keeps its Warp and is embedded, with the existing warning for linked images in masks. Linked files stay connected to their source and track it by relative path ([the help page](https://helpx.adobe.com/illustrator/desktop/add-and-import-files/manage-linked-and-embedded-files/links-panel-overview.html)); the placed-file form is the one the `.ai` reader reads (`crates/eps/src/import/ai/read/art.rs` `end_place`). eps `ai_editing::tests::a_linked_image_with_a_warp_reads_back_linked_with_its_warp_live`, `a_large_linked_image_with_a_warp_reads_back_from_its_preview`, `a_hidden_or_locked_image_with_a_warp_keeps_its_state`, `a_linked_image_with_a_warp_in_a_mask_is_embedded_with_its_warp_live`. Scores and hours unchanged. - **EPS writes a repeated image's samples once:** an image drawn more than once in an EPS file (the pieces of an envelope or a Warp effect, copies of one image, or one image in a symbol's instances) has its compressed samples written once in the setup, and each placement reads them from there: a reusable stream rewound for each placement at Language Level 3 ([PostScript Language Reference, 3rd ed., §3.13](https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf)), and an array of strings of at most 49,152 bytes fed to the image by a procedure at Level 2. An image drawn once is written where it is drawn, as before. The 256 MB limit on the page counts the samples once, so an envelope of a large photo no longer loses pieces to it. A colorized image is reused per ink (eps `tests::an_image_placed_twice_writes_its_samples_once_at_both_levels`, `tests::an_envelopes_image_pieces_draw_one_copy_of_its_samples`, `tests::each_ink_of_a_colorized_image_placed_twice_is_written_once`, `tests::samples_longer_than_a_string_are_reused_at_both_levels`, `tests::the_page_limit_counts_a_repeated_images_samples_once`). Scores and hours unchanged. +- **EPS writes a repeated freeform gradient's image once:** a freeform gradient whose image samples the same on more than one fill (copies of a shape moved with their points, or one shape in a symbol's instances) has its samples written once in the setup, as a repeated image's are, and a fill whose image differs (other colors, another size) gets its own. A freeform fill drawn once is written where it is drawn, as before, and a fill whose placement cannot be drawn, or that arrives when the page is already at the 256 MB limit, is no longer sampled first (eps `tests::a_freeform_gradients_image_drawn_twice_writes_its_samples_once`, `tests::freeform_gradients_sampled_differently_dont_share_an_image`, `tests::a_field_taken_back_at_the_page_limit_leaves_no_image_behind`). Scores and hours unchanged. - **PDF writes a warped image once:** with downsampling on, the pieces of an image bent by a Warp effect are resampled for the widest and the highest size any piece places the image at, so every piece paints one image XObject (a colorized image: one colorized image and its soft mask), instead of one image for each size a piece is placed at. Another placement of the same image is still resampled for its own size. Without downsampling, a colorized image and a CMYK image with a grayscale image mask on its grid are decoded and encoded once for all the sizes they are placed at (pdf `tests::compression::a_warped_image_is_written_once_for_all_its_pieces`, `tests::colorize::a_colorized_warped_image_is_written_once_for_all_its_pieces`, `export::images::tests::colorized_and_grid_masked_images_are_made_once_for_every_size_without_downsampling`). Scores and hours unchanged. - **Distort Appearance with Transform effect options:** with Envelope Options › Distort Appearance on, a path whose Transform effect has Scale Strokes & Effects or Transform Patterns on, or copies it with a gradient, is bent as the same per-copy art it draws and expands as outside the envelope: each copy's own stroke weight, dashes, pattern placement and gradient (effects `bake::tests`). Scores and hours unchanged. - **Fitted gradients turn with the Transform effect:** a gradient fitted to the object (not placed with the Gradient tool) turns, reflects and stretches with the object when its Transform effect rotates, reflects or scales it unevenly, as Object › Transform already does: the gradient is fixed to its fit on the geometry the effect takes, then follows the effect and the effects after it as a placed gradient does. Under moves and even scales it draws as a refit does, and a Transform effect that scales the object to nothing across leaves it fitted, with Scale Strokes & Effects or Transform Patterns on too, and on each copy. This holds on the canvas and in Object › Expand of the fill, for an object's own effects, a fill's or stroke's own, and a group's, where the gradients fitted to its paths, to its own fill, to the groups inside it and to its images' and symbol instances' own fills each fix to their own object's fit. Existing documents with such a Transform effect on fitted-gradient art draw differently: before, the gradient refit to the turned outline. The Gradient tool still shows a fitted gradient's vector at its unturned fit. How gradients follow the effect is VectorCraft's own rule; what the effect does to the object is described in [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html) (effects `bake::tests::a_fitted_gradient_turns_reflects_and_stretches_with_a_transform_effect`, `bake::tests::a_transform_effect_that_scales_to_nothing_leaves_a_fitted_gradient_fitted`, `group::tests::a_groups_transform_turns_the_gradients_fitted_to_its_members_and_its_own_fill`; render `objectfx::a_fitted_gradient_turns_and_reflects_with_its_transform_on_the_canvas`; engine `objexpand::a_fitted_gradient_turned_by_a_transform_expands_as_the_canvas_draws_it`). Scores and hours unchanged. @@ -646,6 +647,7 @@ since 2026-10-01. - **Control bar docks to the top or the bottom, or floats:** the Control bar's panel menu, at the bar's right end, has Dock to Top and Dock to Bottom, the edge in use checked and neither while the bar floats. Dragging the bar's gripper and releasing it within 24 points of the top (under the menu bar) or the bottom of the window docks it at that edge, with a band along that edge while it moves; releasing it farther from both edges floats the bar there, with its outline under the pointer while it moves, and a floating bar's gripper moves it or docks it again (`window.control.dock {edge: "top"|"bottom"|"float"}`), as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). Escape cancels a gripper drag. The 24-point zones, the floating bar's 800-point width and first position, a release outside the window leaving the bar where it was, a window too short for a floating bar docking it at the nearer edge, the floating bar staying inside a window that shrinks, and keeping the edge and the floating position with the workspace, with older workspaces docking the bar at their edge, are VectorCraft's own rules. Regression tests: `the_control_bar_panel_menu_docks_it_to_the_top_or_the_bottom`, `dragging_the_control_bar_gripper_docks_it_at_the_nearer_edge`, `dragging_the_control_bar_gripper_away_from_both_edges_floats_it`, `floating_control_bar_moves_and_redocks_from_each_edge`, `floating_control_bar_menu_docks_to_either_edge`, `control_bar_drag_escape_restores_placement`, `control_bar_drag_release_outside_window_keeps_previous_placement`, `floating_control_bar_stays_reachable_in_tiny_and_resized_windows`, `the_control_bar_dock_command_takes_top_bottom_or_float` and `menus_stop_above_a_control_bar_docked_at_the_bottom` in `crates/ui-egui/src/chrome/tests.rs`, `saved_workspaces_restore_the_control_bar_edge` and `saved_workspaces_restore_floating_control_bar_position` in `crates/ui-egui/src/workspaces.rs`. - **Control bar panel menu turns groups of options on or off:** the Control bar's panel menu lists the bar's groups of options (Snapping, Anchor Points, Tool Options, Object Options, Image Options, Fill and Stroke, Opacity, Graphic Style, Character, Paragraph, Vertical Alignment, Align and Distribute, Transform, Artboard Options and Document Options) above Dock to Top and Dock to Bottom, each checked while it is on; a click turns that group off or on (`window.control.options {group, visible?}`), and a group that is off stays out of the bar even while its tool or object is selected, as the help page describes ([Control panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/control-panel-overview.html)). The list of groups and what each holds, every group on to start, a group that is off leaving its room to the groups after it, and keeping the groups with the preferences and the workspace are VectorCraft's own rules. Regression tests: `the_control_bar_panel_menu_turns_each_group_on_or_off`, `the_control_bar_keeps_its_label_and_menu_with_every_group_off`, `the_control_bar_panel_menu_scrolls_in_a_short_window`, `the_control_bar_options_command_turns_a_group_on_or_off`, `groups_that_are_off_leave_their_room_to_the_later_groups`, `the_artboard_tool_s_control_bar_follows_its_two_groups` and `the_anchor_points_group_holds_convert_and_anchors` in `crates/ui-egui/src/chrome/tests.rs`, `the_image_options_group_holds_the_image_and_trace_controls` in `crates/ui-egui/src/tests/place.rs`, `the_character_and_paragraph_groups_hold_the_type_controls` in `crates/ui-egui/src/panels/paragraph/tests.rs`, `saved_workspaces_restore_the_control_bar_groups` in `crates/ui-egui/src/workspaces.rs`. - **Control bar separators stand only between groups:** the Control bar draws a separator between two groups of options only when a control follows it in the bar. When the groups after a separator are off in the panel menu, do not apply to the selection or the tool, or have no room in the window, the bar ends with the last control and no separator; two separators with nothing between them show as one. Each separator keeps its room, so the groups keep their places. This is VectorCraft's own rule. Regression test: `no_separator_is_left_at_the_end_of_the_control_bar` in `crates/ui-egui/src/chrome/tests.rs`. +- **Fill and Stroke and Opacity stay inside a narrow Control bar:** the Control bar leaves out a Fill and Stroke, Opacity or Graphic Style control it has no room for, whole, instead of drawing it past the panel menu button or under it. The bar draws its controls from left to right, each only when the room left still holds it, so a narrower control can still show after a wider one that was left out: the Opacity link with its field can show when the Stroke link with its weight field did not fit, the Style link with its chip can show when the Opacity link with its field did not, and with nothing selected Preferences can show when Document Setup did not fit. With something selected, the Fill and Stroke, Opacity, Graphic Style and Character controls show only while the Transform link still has room after them, so the bar keeps the link and leaves those controls out first. The Character controls show whole or not at all, and while they are left out the Paragraph, vertical alignment and Align controls after them are left out too. The Snapping menu, the selection's label, the Convert and Anchors groups, the isolation button, Document Setup and Preferences also show only while they fit, and a group separator only while it has room. This is VectorCraft's own rule. Regression tests: `the_control_bar_s_controls_stay_inside_the_bar_in_a_narrow_window`, `a_narrow_control_bar_shows_the_style_link_without_the_opacity_field`, `a_narrow_control_bar_shows_preferences_without_document_setup` and `the_control_bar_shows_the_inline_transform_fields_only_when_they_fit` in `crates/ui-egui/src/chrome/tests.rs`, `the_control_bar_trims_its_paragraph_section_after_the_controls_to_its_right` in `crates/ui-egui/src/panels/paragraph/tests.rs`. - **Pattern-only backquote drags:** With Selection move, bounding-box scale or rotate, or the Rotate, Reflect, Scale or Shear tool, backquote held at mouse release applies the drag matrix to selected pattern paint while object geometry stays in place. The key may be pressed after mouse press; if released before the mouse button, the drag commits as an ordinary transform. Objects without pattern paint stay still; stroke-only patterns and patterned objects in mixed selections take the same matrix. Regression tests: `backquote_move_uses_release_state_and_one_commit` and `backquote_box_scale_and_rotate_use_pattern_matrix_at_release` in `select/tests.rs`, `backquote_drag_previews_and_commits_pattern_only_for_each_transform_tool` in `xform/transform.rs`, `selection_backquote_drag_changes_pattern_fill_and_stroke_without_moving_mixed_art` and `selection_backquote_drag_changes_stroke_only_pattern` in `tests/pattern.rs`, `egui_backtick_drag_uses_key_state_at_release` in `canvas/tests.rs`, and `synthetic_pointer_backquote_keeps_solid_object_in_place` in `control/tests.rs`. Scores and hours unchanged. - **Existing-guide Escape cancellation (G1):** Escape clears a pending guide press or drag through Selection and Direct/Group Selection, cancelling a begun preview. Eight app-input regressions cover both axes, move/copy, temporary selection-tool borrowing from idle Pen, preselected-guide document/interaction-start selection and Undo/Redo/saved-state preservation, later on-canvas motion/release, Shift-press cancellation, and ordinary-release/Enter/idle controls. Historical focused checks on `8ab574b7` recorded five expected RED failures with three passing controls, then all eight GREEN tests and the Direct Selection Escape control passed. All nine also passed in current full CI. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. The original 1280×960 headless app capture from `b076025a` was reviewed for one vertical Selection guide after Escape; source review found its scene unchanged on `a0671777`. Scores and hours unchanged. - **Escape in more tool drags:** Escape during an Anchor Point drag, a Mesh tool drag, a Direct Selection drag of a mesh point, a corner widget or the brackets of type on a path, or a Shape Builder drag cancels it: the art goes back to how the press found it, no undo step is added, the release does nothing, and the tool stays chosen. Escape during a Measure tool drag brings back the measurement before it. Regression tests: `escape_cancels_*` in `draw2/anchor/tests.rs`, `meshedit.rs`, `direct/tests.rs`, `builder/tests.rs` and `xform/measure.rs`, and `escape_cancels_an_anchor_point_mesh_shape_builder_or_measure_drag` in `tests/toolescape.rs`. @@ -673,7 +675,7 @@ since 2026-10-01. - **Artboard tab-switch follow-up:** changing the active document through its tab aligns the Artboard tool with the displayed board before Properties Delete; clicking the already-active tab preserves multiselection and Move/Scale Artwork options. Three headless `chrome::tests::artboard_document_tab_*` regressions cover actual tab A → B → A clicks followed by Properties Delete, surviving boards and guides, preserved artwork/symbols and Undo/Redo; controls cover same-tab multiselection, Move/Scale Artwork options and other tools. Historical focused validation on `dbd2497b` recorded one expected RED failure with two passing controls, then all three GREEN tests passed. All three also passed in historical full CI on `a0671777`. A full CI retry on the five-unit combined source at `a0671777` passed all seven stages with 6,613 test outcomes on 2026-10-14. An earlier full-CI run on the identical source panicked in the randomized `ai_mutated_save_never_panics` test; the cause remains unresolved. This five-fix batch does not claim to repair that failure. - **Artboard clean active-tab close (2026-10-15):** with the Artboard tool active, closing a clean active tab restores the surviving document's displayed board before Properties Delete. Nine targeted checks passed: three existing tab regressions, one clean close/Delete regression and five existing unsaved-document controls. The close regression checks the correct surviving board, preserved artwork/symbols and object selection, affected guides, one Delete edit, saved state and Undo/Redo. Three reviewed original full-app headless captures show the active tab before closing, the surviving document's shown board after closing, and the correct survivor after Properties Delete, using the actual tab × and Delete controls. - **Colour emoji with bitmap glyphs:** `sbix` (Apple Color Emoji) and `CBDT` (Noto Color Emoji) glyphs draw as their largest strike's PNG, placed with the type and unpainted by its fill; PDF writes them in Type 3 glyphs or as images, SVG as images with outlined text, EPS and legacy `.ai` as images, and Create Outlines makes embedded images. Synthetic test fonts in `crates/text/src/test_fonts.rs`; tests `crates/text/src/tests/bitmaps.rs`, `crates/render/src/tests/bitmapglyphs.rs`, `crates/svg/tests/it/colour_glyphs.rs`, `pdf::tests::colour_bitmap_glyphs_export_as_images`, the `eps` `colour_bitmap_glyphs_*` tests and `tests_pathops::create_outlines_turns_bitmap_glyphs_into_images`. COLR and OpenType SVG fonts remain. -- **COLR colour fonts:** v0 and v1 glyphs draw as vector layers (solid colours, linear and radial gradients, clips, blend modes, the foreground colour from the run's fill) on the canvas, as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as Type 3 text in PDF, and as editable groups with Create Outlines; sweep gradients and Porter-Duff compositing draw the glyph as an image. Measured by `tests_colr`, `render::tests_colrglyphs`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colr_glyphs_*`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups`. Scores unchanged (estimated). +- **COLR colour fonts:** v0 and v1 glyphs draw as vector layers (solid colours, linear and radial gradients, including ones that repeat or reflect, clips, blend modes, the foreground colour from the run's fill) on the canvas, as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as Type 3 text in PDF, and as editable groups with Create Outlines; sweep gradients, Porter-Duff compositing and radial gradients that repeat or reflect and start at a circle draw the glyph as an image. Measured by `tests_colr`, `render::tests_colrglyphs`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colr_glyphs_*`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups`. Scores unchanged (estimated). - **OpenType SVG colour fonts:** each glyph's SVG document is read by the SVG importer (registered by the engine) into vector art in the run's colours, then drawn, exported and outlined as COLR glyphs are; PDF real text writes them as text that draws nothing and draws their art where they sit. Measured by `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`. Scores unchanged (estimated). - **Create Outlines keeps the type object's own appearance:** Type › Create Outlines and Object › Expand put the type object's own fills and strokes (the Appearance panel's items around its Characters row) on paths of the whole outline, below or above the glyph paths as that row places them, with one gradient across every glyph, so the outlines draw what the type drew; `object.expand.info` reports those strokes, pattern fills and gradient fills. Outlined characters keep their stroke and fill (the help page, ); a type object's appearance is separate from its characters' (the help page, ); where the object's items go is VectorCraft's own rule. Measured by `tests::pathops::create_outlines_keeps_the_objects_own_fills_and_strokes`, `tests::objexpand::create_outlines_draws_the_type_objects_own_paint`, `tests::objexpand::type_objects_own_stroke_pattern_and_gradient_are_reported`, `tests::objexpand::type_without_an_outline_reports_no_own_stroke_or_fill`, `tests::objexpand::a_stroke_under_an_unpainted_stroke_is_reported_and_outlined` and `tests::objexpand::expand_keeps_the_type_objects_own_paint`. Scores unchanged (estimated). - **Composite fonts that two documents define differently:** when two open documents each define a composite font (Type › Composite Fonts…) by the same name with different parts, each document draws and exports its type in its own definition: in a tiled or floating window drawn while the front document draws, when it is placed in a document that defines the name differently, and in an export made while another document is in front. Evidence: `tests::composite::nested_loans_of_one_name_use_each_documents_definition` and `threads_lending_one_name_at_once_use_their_own_definitions` (`crates/text`), `tests::fontchange::a_placed_document_draws_its_own_definition_of_a_name_the_page_defines` (`crates/render`). Scores unchanged (estimated). @@ -814,7 +816,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Scripts: an error `$.evalFile` throws for a file it cannot run is reported at that file's line and column only while no `catch` has taken it. Caught and thrown again later, in the script or in another file, it is reported where it was thrown again, and a new error whose message holds its text is reported where that error was thrown. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`. - Scripts: an uncaught error that one or more `finally` blocks pass on unchanged, in the script or in a function it called, is reported at the line and column where it was thrown, in `script.run`'s report and the Script Console. An error a `finally` block throws is reported at that throw, and one its `return`, `break` or `continue` discards is not reported. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`. - Scripts: an error a `finally` block passes on is still reported where it was thrown when the block's allocations collect garbage under a small heap limit, and a `finally` block that `return`, `break` or `continue` leaves drops the saved location of its error as it leaves, so a collection no longer counts that code as live. VectorCraft's own rule. `crates/js/src/host_tests.rs`. - - Scripts: a run reads its preprocessor lines before any of it runs: `#include` and `//@include` put another file in place of the line, looked for beside the including file, in the `#includepath` folders and in `JSINCLUDE`; `#script` names the run; `#target` and `#strict off` are taken, `#strict on` turns on strict write checks (below), `#targetengine` names the engine (below), and `#engine` is refused. `$.evalFile` runs a file with the same globals and returns its last value, and `$.fileName` and `$.includePath` give the current script's file and the include folders. A script run from a file reads these files only in its folder and below, without `..` names or links that lead out, and an error in one is reported at its own file, line and column ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/), [the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/); `crates/engine/src/tests/script_include.rs`). + - Scripts: a run reads its preprocessor lines before any of it runs: `#include` and `//@include` put another file in place of the line, looked for beside the including file, in the `#includepath` folders and in `JSINCLUDE`; `#script` names the run; `#target` and `#strict off` are taken, `#strict on` turns on strict write checks (below), `#targetengine` names the engine (below), and `#engine` is refused. `$.evalFile` runs a file with the same globals and returns its last value, `$.fileName` gives the file of the code that reads it (the file being run for code that `eval` or `Function` compiled), and `$.includePath` the include folders. A script run from a file reads these files only in its folder and below, without `..` names or links that lead out, and an error in one is reported at its own file, line and column ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/), [the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/); `crates/engine/src/tests/script_include.rs`). - Scripts: `#targetengine name` (and `//@targetengine name`) runs the script in a named engine that lasts for the session, and a later script that names it sees the globals and functions earlier ones left ([preprocessor directives](https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/)); `$.engineName` gives the name ([the `$` object](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: a script without the line and `#targetengine main` get a new engine each run, the line comes once in the script itself before its code, a session keeps 8 named engines with 64 MiB of heap each, an error or a stop keeps the globals the run set while the documents go back, kept host functions act for the run that calls them, `script.engines` lists the engines and `script.engine.drop` drops one, and the web build has none. `crates/engine/src/tests/script_engines.rs`. - Scripts: `$.fileName` is a read-only name of the script file in use ([the help page](https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/)). VectorCraft's own rules: it names the file of the code that reads it, so a function a file included with `#include` or run with `$.evalFile` defined, or one an earlier run of a named engine kept, gives its own file and not the calling file; code from a script given as `source`, and kept code whose place the engine dropped, gives an empty name, and code that `eval` or `Function` compiled gives the file being run. `crates/engine/src/tests/script_include.rs`, `crates/engine/src/tests/script_engines.rs`. - Scripts: a runtime error is shown at the line that was running ([the help page](https://extendscript.docsforadobe.dev/extendscript-toolkit/debugging-in-the-toolkit/)). VectorCraft's own rules: an error in code an earlier run of a named engine compiled, such as a function it defined, is reported at that code's file, line and column (the earlier run's script, a file it included or a file its `$.evalFile` ran), while the report and its console line keep the calling run's name; a script given as text is named by its run's name, and each engine keeps these places for 8 MiB of source maps, the oldest going first. An error in code whose place is gone, or in code that `eval` or `Function` compiled, is reported without a line and column, and the code still runs. `crates/engine/src/tests/script_engines.rs`. @@ -907,10 +909,11 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The counts that Package's status and question, the folder search of Missing Fonts and Find Linked Files, the modified and missing linked file prompts and the CSS Properties panel's notes show use the interface language's plural forms instead of a fixed "(s)" form. Evidence: `the_missing_links_note_uses_each_count_form` in `crates/ui-egui/src/dialogs/tests_package.rs`, `the_modified_links_question_uses_each_count_form` in `crates/ui-egui/src/dialogs/missing_links.rs`, `search_counts_use_each_count_form` in `crates/ui-egui/src/dialogs/missing_fonts/tests.rs` and `the_unnamed_objects_note_uses_each_count_form` in `crates/ui-egui/src/panels/tests_css_properties.rs`. The status bar's messages for relinked, replaced, updated and embedded images, broken links, linked files that can't be read or changed on disk, images that can't be decoded, added and unavailable fonts, copied CSS, downloads and exports with their notes name one item in the singular and more in the plural, in English and in each language's plural forms (`counted_status_messages_take_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`). The status bar's messages for documents opened or saved with notes and for discarded or restored recovered documents, and the Global Edit options' Results line, do the same (`open_save_and_recovery_messages_take_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`); the warnings for linked images and placed documents output as their previews and the errors for a page past a placed file's last page or artboard name their counts in the singular or the plural in English. - - The Simplified Chinese catalog (`crates/ui-egui/src/i18n/zh-hans.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,439 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report lists 40 Simplified Chinese messages: the composite font, justification order and Blob Brush connect end errors that open #7211, #7116 and #7330 translate, and two messages that no catalog has a row for yet. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. + - The Simplified Chinese catalog (`crates/ui-egui/src/i18n/zh-hans.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,554 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins, including the image export, color profile, color book, swatch exchange file, DXF import and Revolve errors. The report lists 55 Simplified Chinese messages: the composite font, SVG filter, read root, graph design, justification order and Blob Brush connect end errors that open #7211, #7378, #7405, #7116 and #7330 translate. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. - Edit › Undo and Edit › Redo, in the in-window and native menus, name the step in the interface language as the History panel lists it (`panels::history::step_name`), so when the catalog has no plain row for a step named after an effect, such as Feather, the menu shows the translation of the effect's menu row without its ellipsis (`undo_and_redo_name_the_step_as_the_history_panel_does`). - The History panel translates the undo step names the engine, the tools and the shell write as literals: the untranslated report (`untranslated_report`) lists any a complete catalog lacks, and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis. - The German catalog (`crates/ui-egui/src/i18n/de.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,433 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report lists 48 German messages; 38 of them are the composite font, select-a-path and justification order errors that open #7211, #7214 and #7116 translate. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. + - The Korean catalog (`crates/ui-egui/src/i18n/ko.tsv`) has an `@msg` row for the status and error messages the untranslated report (`untranslated_report`) finds in the sources, 3,438 `@msg` rows in all: the errors of commands, scripts, file import and export and plug-ins. The report lists 39 Korean messages; they are the composite font, select-a-path, justification order and `path.freehand` connect errors that open #7211, #7214, #7116 and #7330 translate. A message with two placeholders in a row (`Template::parse` in `crates/ui-egui/src/i18n/catalog.rs`) is still shown in English. - The History panel menu has Step Backward and Step Forward, which run Edit › Undo and Edit › Redo and are disabled when there is no step to undo or redo, and Set History Limit…, which opens Preferences at Performance, where History States is set. - The History panel menu's Clear History (`edit.clearHistory`) removes every step to undo and to redo and leaves the document as it is; it is not an undo step itself, and it is disabled when there is no step to clear. - The New Document from State button at the bottom of the History panel (`edit.newDocumentFromState`) makes a new document, which becomes active, from the state the panel has selected ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/undo-edits-and-manage-design-history.html)). VectorCraft's own rules: the new document takes the next Untitled-N name, is marked unsaved, has nothing selected and has no steps to undo or redo, and the source document and its steps stay as they are. @@ -1314,7 +1317,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Mouse wheel over the font family field (#5253):** In the Character and Properties panels and the Control bar, turning the mouse wheel over the closed font family field applies the next family to the selected type (the previous one when turned up), keeping the style when the family has it, else its closest. Each notch is one undo step; leaving the field or Escape keeps the family, and Undo takes it back. The steps use the font menu's order at the first turn and keep it until the pointer leaves the field or the text selection changes, so recent-font updates do not reshuffle later turns. That order follows the font filters every font menu shares (one chosen in the Glyphs panel's menu or a dialog's applies too) but ignores its search text, puts recent families first, and stops at the first and last family. A turn with a modifier key is left to the panel and the canvas. Behavior reference: the [Illustrator CS6 reference](https://help.adobe.com/archive/en/illustrator/cs6/illustrator_reference.pdf#page=10) describes editing hovered dropdown controls with the mouse wheel; [Find and apply fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-and-apply-fonts.html) names the family field's places. Applying it to the font field, the order, the ends and the undo steps are VectorCraft's own rules. Tests: `crates/ui-egui/src/tests/font_menu.rs`, `crates/ui-egui/src/font_menu/tests.rs`, `crates/ui-egui/tests/font_wheel.rs`. - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, Text Flow by rows or by columns (#4978), vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`), and an anchor of the path of type on a path outside perspective, the type reflowing along it (`text.reshapePath`, #3980). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. The Character panel's Variable Font sliders set each axis (see Fonts). Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Cmd+Shift+C (Align center) works on Windows and Linux too (#1940). A double-click on area type's bottom middle handle turns on Auto Size (#1940). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches back to the tool that was active before type editing began (kept while clicks move editing to other text), or to the Selection tool when there was none (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). Each reference shows an icon of the height it measures before its name, in the open list and in the closed field (#6304): a frame around a letter for Em Box, a bar from the baseline to the top of a capital for Cap Height and of a lowercase x for x-Height, and a frame inset in the em square for ICF Box. With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html). - The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. Such a paragraph is broken with the Single-line Composer when the frame has no room for all of it or when the search passes VectorCraft's own limits. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html). + The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. When the frame cannot hold all of such a paragraph, the lines it holds are still composed with the whole paragraph, the lines past the frame's end measured as wide as the last line the frame holds (VectorCraft's own rule); they overflow or go on in the next threaded frame, which composes them again. Such a paragraph is broken with the Single-line Composer when the frame has no room for its first line or when the search passes VectorCraft's own limits. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html), and for text that flows on between threaded objects, [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/create-text-threads-between-objects.html). A text wrap object's Offset may be negative as well as positive ([the help page](https://help.adobe.com/archive/en_US/illustrator/cs4/illustrator_cs4_help.pdf#page=307)). A negative Offset (to -1000 pt) shrinks the wrap object by that distance, measured from each edge itself and from the edges of its holes, so lines beside it, above it and below it run that far into it, and an object narrower than twice the distance no longer moves text aside. With Invert Wrap, a negative Offset keeps the text that far inside the object. These are VectorCraft's own rules for the shape of the shrunk object. Text Wrap Options, `object.textWrap.make` and `object.textWrap.options` and the MCP `text_wrap` tool take the negative value, and layout uses it (`crates/text/src/layout/region.rs` tests, `cmd::textwrap::tests::text_wrap_options_negative_offset_reflows_and_round_trips`). In area type of a shape other than a rectangle, each line's whole band, ascent to descent, stays inside the frame: where an edge pinches in within the band, the line ends at the narrowest point, and a line runs beside a hole that starts and ends within its band, not across it. In area type of any shape, a line also stays clear of a text wrap object that reaches into any part of its band, and inside an Invert Wrap object over its whole band (`crates/text/src/layout/region.rs` tests). In area type whose frame has several contours, a line runs across an edge two contours share only when their edges lie exactly on one line, so a gap between contours keeps lines from crossing it, even a gap one unit in the last place wide near the edge of the canvas; an edge divided by an anchor exactly on it still lets lines cross (`layout::region::separate_slanted_contours_keep_a_four_ulp_gap_near_canvas_limit`, `shared_slanted_edge_still_joins_near_canvas_limit`, `area_type_sets_no_line_across_a_four_ulp_gap_between_slanted_contours`). @@ -1523,7 +1526,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Swatches made from a shown color in a CMYK document:** the Color panel menu's Create New Swatch, an Alt-click on the Swatches panel's New Swatch button, the New Swatch dialog for a gradient, a Fill or Stroke proxy or the Gradient panel's thumbnail dropped on the Swatches panel (or Alt-dropped on a swatch of its kind), and the Gradient panel's Add to Swatches and Save to Swatches keep each color's model, as the Color Guide's and Color Themes' saved colors do: an RGB object's color gives an RGB swatch, and a gradient's RGB stops stay RGB. - **Sample Color from Screen:** an eyedropper button in the Color Picker and `ui.sampleScreenColor` (which can be given a key in Keyboard Shortcuts) pick a color anywhere on the screen with the system's sampler: `NSColorSampler` on macOS, the XDG desktop portal's [PickColor](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Screenshot.html) on Linux, and the EyeDropper API in browsers that have it. The color becomes the fill or the stroke as one undo step, or the Color Picker's color, converted from display sRGB into the working RGB space. On Linux, a session without a portal that has PickColor, a portal that does not return from the call within 25 seconds (VectorCraft's own limit) or leaves the bus before it answers, and a portal that ends the request another way each show a status message and end the wait. Windows, the BSDs and other browsers have no sampler yet; the button is gray there. - **Stroke visibility (#1253):** adding paint to a previously unpainted zero-width stroke restores the existing 1 pt default (`PaintDefaults::default`, `Appearance::set_stroke`), for the selected appearance item, the new-art defaults and Swap Fill/Stroke. Positive widths are kept; recoloring a painted zero-width stroke keeps its explicit zero. A real Eyedropper-to-new-shape sequence reproduces the zero-width inheritance. Five engine regressions cover creation, defaults, targeted items, preserved widths and one-step Undo/Redo (`crates/engine/tests/it/stroke_visibility.rs`). - - **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles snap and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh. + - **Gradients:** lossless, validated gradient params; gradients follow rotate, reflect, shear, non-uniform scale and distortions; the Gradient tool edits the fill or stroke of the active proxy, type included; an interactive on-canvas annotator (move, length/angle, rotate, add/move/delete/duplicate stops, midpoints, a stop popover, Delete and arrow keys). The Gradient panel has the Fill/Stroke proxy, a gradient-swatch dropdown with Save to Swatches, midpoint locations, Alt-drag to copy or swap stops, swatch drops on the ramp and a stop eyedropper; copied appearances and graphic styles place gradients on each target's own bounds; gradient handles, both ends of a vector drawn on the art and the start of a clicked one snap as other drawn points do (path segments, construction guides and View › Snap to Glyph's guides included) and honour Constrain Angle. Gradient stops stay linked to global and spot swatches. Freeform gradients (points and lines) are edited on the canvas; radial gradients have an extent ellipse, an aspect handle and an off-centre focal point (SVG `fx`/`fy`, PDF two-point radials). A stroke's gradient runs within, along or across it. Object ▸ Expand turns gradients into N objects or a gradient mesh. - The Gradient panel's Delete Stop sits at the right end of the ramp, level with it, so a panel too short to show the stop fields still shows it; it deletes the selected stop (never below two), as dragging a stop down off the ramp does (#5403). - Conic gradients, VectorCraft's own gradient type: the stops sweep once around the center from the vector, clockwise on the page (a negative aspect, or a reflection, turns the sweep the other way), with a Conic button in the Gradient panel's Type row and the Gradient tool's extent ellipse and aspect handle; they follow transforms, Expand into wedges or a mesh, open from `.af` files, and export to PDF as a function-based shading that PDF import reads back as a conic gradient; SVG, EPS and legacy `.ai` files get solid wedges, and CSS a `conic-gradient`. - Diamond gradients, VectorCraft's own gradient type: the stops run from the center out to a square with a corner at the end of the vector (the aspect stretches it into a rhombus), with a Diamond button in the Gradient panel's Type row and the Gradient tool's square and aspect handle; they follow transforms, Expand into nested squares or a mesh, and export to PDF, SVG, EPS and legacy `.ai` as four linear gradients, one for each quarter of the square (an image in PDF and SVG for translucent stops, a dithered fill, the Perceptual method other than spot color shadings in PDF, or a square with no area), and on type characters in SVG as a radial gradient; PDF import reads a function-based shading whose color is the same along each square about its origin as a diamond gradient. @@ -1569,6 +1572,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Outline Object on an image's own fill or stroke follows the outline of the image's opaque area, and the image's frame when the image has no transparency. Path effects apply to a fill or stroke added to a bitmap object ([archived reference manual, page 561](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=561)), and with Outline Object such a stroke follows the boundary of the art when the image has transparent areas and the image's frame when it has none ([CreativePro article on strokes for placed images](https://web.archive.org/web/20250815214529/https://creativepro.com/add-strokes-placed-images-illustrator/)). VectorCraft's own rules: the opaque area is the pixels at least 50% opaque, and its outline is traced as Image Trace's Black and White mode traces at threshold 128 and fitted with curves. Evidence: `tests::imageoutline` (vectorcraft-effects), `opaque::tests` (vectorcraft-trace), `tests::containers::an_images_stroke_with_outline_object_follows_its_opaque_area` and `tests::containers::an_images_outline_object_stroke_exports_selects_and_measures_along_its_opaque_area` (vectorcraft-engine), `tests::objectfx::an_images_outline_object_stroke_follows_new_pixels_under_the_same_key` (vectorcraft-render). A geometry effect on one of type's own fills or strokes reshapes the whole outline that the fill or stroke paints, on the canvas and in exports; PDF, SVG, EPS and the other vector exports write such type as paths. On type that holds inline graphics, those effects are drawn and exported only when the type has a geometry effect or perspective of its own. A geometry effect on a symbol instance's fill reshapes the filled shapes of the symbol's art and leaves their strokes as they are; one on the instance's stroke reshapes the stroked shapes and leaves their fills as they are. The shapes keep their colors. Only the instance's topmost fill and topmost stroke count, and only while they are visible with an opacity above 0. The canvas and Object › Expand Appearance use the same art, SVG and PDF export bake the same shapes, and Expand Appearance turns the instance into an editable group in one undo step. A group's own fills and strokes in the art take the effects too. An instance inside the art passes the effects on to its own art, and a dynamic symbol's instance reshapes the fills and strokes it paints on the art. How the effects map onto the symbol's art is VectorCraft's own rule. The help page describes effects on one fill or stroke, and the help page describes Expand Appearance (engine `expand::a_symbol_instance_fill_effect_reshapes_its_filled_art_and_expands`, `a_symbol_instance_stroke_effect_reshapes_its_stroked_art_and_expands`, `a_stroke_effect_reshapes_a_stroke_a_dynamic_instance_adds`, `a_fill_effect_reshapes_the_fill_of_a_group_in_the_art`, `only_the_topmost_visible_instance_fill_reshapes_the_art`, `a_nested_symbol_instance_takes_the_outer_fill_effect`, `export_bakes_a_symbol_instance_fill_effect`, `a_fill_effect_on_an_instance_of_a_symbol_that_holds_itself_ends`). Existing documents with a geometry effect on a symbol instance's fill or stroke draw differently: before, the effect drew nothing. + A raster effect (Drop Shadow, Outer Glow, Gaussian Blur and the other effects that paint pixels) on a symbol instance's topmost fill draws on each filled shape of the symbol's art, and one on its topmost stroke draws on each stroked shape, after any geometry effects listed before it, under the same rules as geometry effects: only a visible fill or stroke with an opacity above 0 counts, a group's own fills and strokes in the art take the effect, and an instance inside the art passes it on. Each shape gets its own shadow or glow, which is VectorCraft's own rule. The canvas, Use Preview Bounds and the drawn bounds, SVG and PDF export draw it, and Object › Expand Appearance is enabled for such an instance and makes the effect an image at the document's raster effects resolution, in one undo step (engine `expand::a_symbol_instance_fill_shadow_shadows_its_filled_art`, `a_symbol_instance_stroke_shadow_shadows_only_its_stroked_art`, `a_symbol_instance_fill_shadow_follows_the_fill_offset_before_it`, `a_nested_symbol_instance_takes_the_outer_fill_shadow`, `export_draws_a_symbol_instance_fill_shadow`). Existing documents with a raster effect on a symbol instance's fill or stroke draw differently: before, the effect drew nothing. The Appearance panel's inline effect editor labels each parameter as the effect dialogs' generic fields do, without the colon ("Max. Radius", "Channel 1", "Create New Fields by"). Roughen's and Zig Zag's Points, Offset Path's Joins, Inner Glow's Source and the menus of Radial Blur, Smart Blur, Sprayed Strokes, Glass, Mezzotint, Halftone Pattern, Grain, Texturizer and De-Interlace are dropdowns with the dialog's labels that send the dialog's values, and a plug-in's choice is a dropdown of the plug-in's values. Revolve's edge, Extrude & Bevel's bevel, every Warp style's orientation, Rasterize's Color Model, Background and Anti-aliasing and the channel of Levels and Curves are dropdowns too, with the labels their own dialogs show. `effect.apply` and `effect.setParams` refuse a Levels or Curves `channel` other than `rgb`, `red`, `green` or `blue` (or `r`, `g` and `b`, in any letter case). Object › Expand Appearance keeps each stroke of a path a stroke (weight, caps, joins, dashes, arrowheads, alignment and width profile) on a copy of the path with the geometry effects baked in, and Object › Expand… is dimmed, with a tooltip, while the selection has appearance attributes, so a stroked line with Zig Zag becomes a stroked zig-zag line and only Expand then outlines it, the two steps the [Expand objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/expand-objects.html) help page describes (#3089). Brushed strokes still become their brush art; `object.expand` still bakes and outlines in one step for agents. @@ -1577,6 +1581,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Transparency:** three-state Knockout Group (on, neutral, off), Opacity & Mask Define Knockout Shape, Page Isolated Blending and Page Knockout Group, on screen and in SVG/PDF. SVG export writes a knockout group of opaque art plainly, without masks, so shapes meeting edge to edge leave no seam; translucent, blended, masked or brushed art knocks out through masks. Blend modes match the standard formulas on screen, in SVG and in PDF, and non-isolated groups blend with the art below. Flatten Transparency with presets, a dialog and a Flattener Preview panel. Flattening for files without transparency (PDF/X-1a, PDF/X-3) in a CMYK document keeps the inks where it rasterizes, as under an image with see-through pixels: the images it makes hold ink amounts, not screen colours the file would separate again, so a C100 M6 Y41 K3 panel stays C100 M6 Y41 K3 there (#2160, `engine/src/cmd/fileio/tests_pdfx.rs`). On the canvas, the renderer no longer draws the picture of the art below back as an image each time it copies the backdrop of a non-isolated group that blends: it keeps that picture and draws the art that follows over it. At 2880 × 1800, 50 translucent groups with Multiply members take 65 ms a frame instead of 299 ms on 8 threads (`vectorcraft-cli bench`), and the frames have the same pixels. Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports. + PDF export writes a knockout group as one PDF knockout group (`/K true`, with `/I true` when the group isolates; ISO 32000-1, 11.4.6.3), so a knockout group of any size opens again in VectorCraft as a knockout group, and the form nesting of the file does not grow with the number of elements. When an element's opacity or mask defines its knockout shape, or it paints translucently, each element is drawn through one soft mask of the overlapping elements above it instead; past 65,536 objects in a group's masks (or 2,048 elements) the group is a PDF knockout group, with a warning that its translucent elements knock out fully. SVG export draws each element of a knockout group through one mask of the overlapping elements above it, and the masks refer to one copy of each element's shape, so the elements stay siblings and the file nests no deeper with more elements; past 65,536 shapes in a group's masks (or 2,048 elements) the group is written as a plain group, with a warning. Evidence: `pdf/src/tests/blend.rs`, `svg/tests/it/blend_export.rs`, `pdf/tests/gradient_forms.rs` (a gradient in a knockout group reads back in place). - **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; ten generated style libraries (including Game HUD, Status Effects, Painted and Sketched, whose seven styles each repaint a solid fill with one watercolor, dry brush, pastel, charcoal or ink effect, and Textures) save and load as `.vcstyles`. The built-in Textures graphic style library (`graphicStyle.addFromLibrary {library: "textures"}`) has eight styles that paint their fills and strokes with the texture effects: Fine Grain (Grain), Canvas Weave and Burlap Sack (Texturizer), Cracked Glaze (Craquelure), Mosaic Floor (Mosaic Tiles), Quilted Squares (Patchwork), Leaded Glass (Stained Glass) and Damp Paper (Water Paper). The help page names new graphic styles for textures ([What's new](https://helpx.adobe.com/illustrator/desktop/new-features/whats-new.html)) without naming them, so the styles, their names and their settings are VectorCraft's own (`tests::stylelib::textures_styles_apply_their_texture_effects_and_keep_their_params_through_save_and_open`). In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one. @@ -1780,6 +1785,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - File › Place and a file dropped on the canvas refuse a PNG, JPEG, GIF or WebP file whose pixels can't be read, such as one cut short by a download or a sync still running, and the status bar shows "Couldn't place : the image's pixels can't be read". Paste and Relink refuse such an image too, and Update Links, or Open when it reads the file again, counts it as missing. A JPEG cut short still places when its pixels can be decoded (`file.place`, `crates/engine/src/tests_place.rs`). - On the desktop, Copy and Cut make the system clipboard's formats (text, SVG, PDF, PNG) from a copy of the clipboard on the thread that writes background saves and exports, and write them when they are ready; while that thread has a save, an export, a print or a recovery copy to finish, they are made and written at once. Until they are written, Paste in VectorCraft takes the internal clipboard, and while a text field or the Type tool has the keyboard the app waits for them. Another Copy or Cut, or a copy of text in VectorCraft, drops them. - In the Linux Flatpak on GNOME, where the clipboard goes through the compositor's core Wayland protocol, one Copy offers the PNG (`image/png`), the SVG (`image/svg+xml`, with Include SVG Code on) and the text, and Paste takes another app's SVG, text or PNG in that order, a picture over blank text or text that only names it; the Paste menu item is enabled by the types offered (`clipboard::wayland` tests in `apps/vectorcraft/src/clipboard.rs`; not yet run in the Flatpak on a GNOME session). + - On that core Wayland clipboard, a paste from an app that leaves its pipe open and sends no data for 2 s fails and pastes nothing, and a paste of VectorCraft's Copy by an app that takes no data for 2 s is stopped and its pipe closed, so later pastes and copies are read and sent again; the 2 s limit is VectorCraft's own and starts again after each byte that moves (`clipboard::wayland::tests::a_read_from_an_app_that_stops_sending_fails_and_closes_the_pipe` and `a_send_to_an_app_that_stops_reading_fails_and_frees_its_thread`). - On the desktop with Enable Background Export on (Preferences › File Handling), File › Package… collects the linked files and fonts and writes the package from the document as it was at OK on the thread that writes background saves and exports, with "Packaging" in the status bar and the busy spinner until it is written; a save of unsaved changes that Package starts is written first. `file.package` over MCP and scripts replies as before. - On the desktop with Enable Background Export on, Export for Screens and the Asset Export panel's Export encode and write their files (into the folder, or one ZIP in it) from the document as it was at OK on the thread that writes background saves and exports, with "Exporting" in the status bar and the export progress dialog, which counts the files and whose Cancel stops before the next file. `document.exportForScreens` and `assets.export` over MCP and scripts reply as before. - With Enable Background Export on, an Export for Screens with no folder of its own also writes in the background into the folder its `exportPreset` keeps, with the same progress dialog and Cancel, and the status bar names that folder. When that folder is outside an MCP client's read roots, the reply, its errors and the status bar name it by its own name, as for an export made at once (`dialogs::tests_screens::an_export_into_a_presets_folder_writes_in_the_background`, `fileio::tests_screens::a_prepared_export_into_a_presets_folder_outside_the_read_root_leaves_it_out`). Behavior reference: the help page, https://helpx.adobe.com/illustrator/using/exporting-artwork.html (exports run in the background by default). @@ -2104,6 +2110,15 @@ stored data), `cmd::addons::pathtools::tests` in `vectorcraft-engine` (each comm anchor edits and transforms, the SVG export filter, the tool's gestures), `addons::pathtools::tests` in `vectorcraft-ui-egui` (submenu and panel) and `command_sweep`. +The 3D Box and shapes add-on's commands (`addon.shapes.*`, 2026-11-13) refuse a value outside its range with an error +that names the parameter and the range, and draw or change nothing: polygon sides and star points are whole numbers +from 3 to 1,000 ([the help page](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-live-shapes/live-shape-properties.html)), +sizes and corner radii are not negative, a star's inner radius is 0.01 to 1 times its outer radius, and rounding, +randomizing, spoke ratio, spiral turns, divergence and inner radius keep VectorCraft's own ranges; `arcMode` takes +only the names it lists. `addon.shapes.configure` checks every value before it changes any selected shape and records +no undo step when nothing changes. Tests: `cmd::addons::shapes::tests` in `vectorcraft-engine`. Commands: +`docs/mcp.md` › 3D Box and shapes add-on. + The add-on's Corner Tool (2026-10-31) edits the Live Corners every path already keeps (no second corner store): hover shows an anchor that can take a corner, a click cuts one with the tool's radius and kind (round, inverted, chamfer), a drag sizes it (Shift: whole values), a cut corner's marker selects it and loads its values, resizes it, and removes it diff --git a/docs/type-parity.md b/docs/type-parity.md index 308a395670..31a835772d 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -39,7 +39,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Full font-name matching (OpenType name ID 4) | done | #1258: loaded/cataloged faces and Missing Fonts searches; localized names and variable default faces; Windows font-service QA pending | | Variable fonts | done | named instances as styles; Character panel › Variable Font sets each axis (`fontAxes`); the font menu's Font Technology › Variable lists only variable font families, with the script, kind and ★ filters ([Find, apply, and adjust variable fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-apply-and-adjust-variable-fonts.html)); a family with a variable file beside static ones counts as variable. Evidence: `font_menu::tests::the_variable_filter_lists_only_variable_families`, `tests_axes::a_family_with_variation_axes_is_variable`, `tests_sysfonts::a_family_with_a_variable_file_beside_static_ones_is_variable` | | Fallback glyphs keep the text's weight and slant | done | #2491: a character from a fallback face is set in that family's face nearest the run's weight and slant; Bold Italic of a missing family is drawn Bold (Source Sans 3 has no Bold Italic) rather than Italic; a letter and its combining marks (a grapheme cluster) are drawn in one face, the first that has all of them, so marks the text's font lacks attach to their letter instead of drawing as missing glyphs (#4491). Vietnamese letters with two marks (Ể, Ứ, Ự) sit on their letter whether typed precomposed, decomposed or as a vowel plus a tone mark, built from GPOS mark-to-mark in fonts without the precomposed letters. Evidence: `tests::vietnamese` | -| Colour fonts (bitmap, COLR, OpenType SVG) | partial | bitmap glyphs (`sbix`, `CBDT`) done: the largest strike's PNG, transformed with the type, fill and stroke ignored; PDF Type 3 glyphs or images, SVG images with outlined text (live text leaves emoji to the viewer, their fonts not embedded), EPS and legacy `.ai` images, Create Outlines images; not `sbix` duplicate, flipped, JPEG or TIFF glyphs, or `CBDT` BGRA or mask data, or ruby; in envelopes they warp as images do, and a Warp effect moves them without bending them. COLR (v0 and v1) done: a glyph's layers as vector art (solid colours, linear and radial gradients, clip glyphs and boxes, blend modes, transforms, palette 0, the foreground colour from the run's fill, variable fonts at the face's instance), transformed with the type; drawn on the canvas, written as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as text in PDF (Type 3 glyphs), and Create Outlines makes groups of editable paths. Paints documents have no form for draw the glyph as an image (256 px per em): sweep gradients, Porter-Duff compositing, radial gradients from a circle that isn't the centre, repeating or reflecting gradients. Not ruby. COLR and SVG glyphs also go with type that geometry effects reshape (on the canvas and in exports), into Expand Appearance and EMF/WMF export, as their art, and bitmap glyphs as images. OpenType SVG done: a glyph's SVG document (its element `glyph`) is read by the SVG importer into vector art in font units, `currentColor` in the run's fill (and the legacy `context-fill` and `context-stroke` in its fill and stroke), a `viewBox` mapped onto the em square, and then drawn, exported and outlined as COLR glyphs are; in PDF real text such glyphs are text that draws nothing, with their art drawn where they sit. Images in glyph documents are in the art, holding their pixels until a document takes them in; text, scripts and embedded content are left out, as the table rules them out. Not CPAL palette variables. Evidence: `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::svg_glyph_images_draw_and_outline`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`, `tests_colourfonts::expand_appearance_keeps_the_colour_glyphs_of_type_with_effects`, `render::tests_colrglyphs::colour_glyphs_go_with_type_that_a_geometry_effect_reshapes`, `tests_bitmaps`, `tests_colr`, `render::tests_bitmapglyphs`, `render::tests_colrglyphs`, `pdf::tests::colour_bitmap_glyphs_export_as_images`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colour_bitmap_glyphs_*` and `colr_glyphs_*`, `tests_pathops::create_outlines_turns_bitmap_glyphs_into_images`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups` | +| Colour fonts (bitmap, COLR, OpenType SVG) | partial | bitmap glyphs (`sbix`, `CBDT`) done: the largest strike's PNG, transformed with the type, fill and stroke ignored; PDF Type 3 glyphs or images, SVG images with outlined text (live text leaves emoji to the viewer, their fonts not embedded), EPS and legacy `.ai` images, Create Outlines images; not `sbix` duplicate, flipped, JPEG or TIFF glyphs, or `CBDT` BGRA or mask data, or ruby; in envelopes they warp as images do, and a Warp effect moves them without bending them. COLR (v0 and v1) done: a glyph's layers as vector art (solid colours, linear and radial gradients that pad, repeat or reflect (the [COLR specification](https://learn.microsoft.com/en-us/typography/opentype/spec/colr)'s extend modes), clip glyphs and boxes, blend modes, transforms, palette 0, the foreground colour from the run's fill, variable fonts at the face's instance), transformed with the type; drawn on the canvas, written as paths in SVG, EPS, legacy `.ai` and PDF with outlined text, as text in PDF (Type 3 glyphs), and Create Outlines makes groups of editable paths. Paints documents have no form for draw the glyph as an image (256 px per em): sweep gradients, Porter-Duff compositing, radial gradients from a circle that isn't the centre, radial gradients that repeat or reflect and start at a circle, gradients that repeat or reflect with a stop whose color isn't in the palette. Not ruby. COLR and SVG glyphs also go with type that geometry effects reshape (on the canvas and in exports), into Expand Appearance and EMF/WMF export, as their art, and bitmap glyphs as images. OpenType SVG done: a glyph's SVG document (its element `glyph`) is read by the SVG importer into vector art in font units, `currentColor` in the run's fill (and the legacy `context-fill` and `context-stroke` in its fill and stroke), a `viewBox` mapped onto the em square, and then drawn, exported and outlined as COLR glyphs are; in PDF real text such glyphs are text that draws nothing, with their art drawn where they sit. Images in glyph documents are in the art, holding their pixels until a document takes them in; text, scripts and embedded content are left out, as the table rules them out. Not CPAL palette variables. Evidence: `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::svg_glyph_images_draw_and_outline`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`, `tests_colourfonts::expand_appearance_keeps_the_colour_glyphs_of_type_with_effects`, `render::tests_colrglyphs::colour_glyphs_go_with_type_that_a_geometry_effect_reshapes`, `tests_bitmaps`, `tests_colr`, `render::tests_bitmapglyphs`, `render::tests_colrglyphs`, `pdf::tests::colour_bitmap_glyphs_export_as_images`, `pdf::tests::colr_glyphs_export_as_vector_art`, `svg/tests/it/colour_glyphs.rs`, `eps` `colour_bitmap_glyphs_*` and `colr_glyphs_*`, `tests_pathops::create_outlines_turns_bitmap_glyphs_into_images`, `tests_pathops::create_outlines_turns_colr_glyphs_into_layer_groups` | | Every-line and Single-line composers | done | tuned by eye, never compared side by side | | Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, with one setting for all the frames of a thread, in point and area type of left-to-right paragraphs, and at the brackets of type on a path whose first paragraph runs left to right; right-to-left paragraphs of point and area type, and type on a path whose first paragraph runs right to left, do not hang | | System Layout | done | Character panel menu › System Layout, `type.systemLayout`: the whole document, off by default and for documents from before it, saved only when on. On, every text object advances by whole points (at least one when a character takes space). New point, area and path type, threaded frames, placed text, graph and dimension labels and pasted plain text inherit it. Evidence: `tests::system_layout_rounds_positive_advances_to_whole_points`, `typecmd::area_tests::system_layout_sets_the_document_and_every_text_object`, `panels::character::tests::system_layout_menu_item_toggles_the_document` |