diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66a15fa..5cd3f09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: macos: name: macOS universal needs: version - runs-on: macos-release # org larger runner (M2 Pro), release.yml@release only; capped by the actions_macos_xl budget + runs-on: macos-15-xlarge # M2 Pro larger runner; use only in release.yml (actions_macos_xl budget caps spend) environment: release env: DESIGNCRAFT_VERSION: ${{ needs.version.outputs.version }} diff --git a/Cargo.lock b/Cargo.lock index ed2bd47..222b525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1016,9 +1016,9 @@ dependencies = [ name = "designcraft-images" version = "0.2.1" dependencies = [ + "designcraft-color", "designcraft-fonts", "image", - "psd", "resvg", "tiff", ] @@ -3405,15 +3405,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "psd" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a25f9b8cfffd65d911baf31a033239f7a0facb755faa2481575b70db5dc9195" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "pxfm" version = "0.1.30" diff --git a/Cargo.toml b/Cargo.toml index 37ea3c8..715ab8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,10 +52,9 @@ vello_common = { version = "0.2", default-features = false } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] } # CMYK TIFF samples for PDF export (`image` decodes them to RGB); the version `image` uses. tiff = "0.11" -# SVG (same usvg as krilla-svg) and Photoshop composites. +# SVG (same usvg as krilla-svg). resvg = { version = "0.47", default-features = false, features = ["text"] } krilla-svg = "0.8" -psd = "0.3" # Boolean operations on Bézier paths (Pathfinder). flo_curves = "0.8" # QR codes (Object › Generate QR Code); MIT. diff --git a/ROADMAP.md b/ROADMAP.md index d871905..83fb692 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -154,7 +154,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Make / Release Compound Path (⌘8; nested paths become holes), Create Outlines (⇧⌘O; one path per text colour), Scissors tool (C) with `path.split`. - Transform Again / Individually / Sequence Again (⌥⌘3; a moved copy repeats like Step and Repeat), Clear Transformations; Layers: Merge, Delete Unused, Hide/Lock Others, Show/Unlock All; Break Link to Style (paragraph and character). - Floating panels: tear a panel off the dock (its float button, or drag its header away) into a movable, resizable window; Dock puts it back; positions persist; `window.floatPanel` / `window.dockPanel`. -- Place SVG (vector in PDF export via krilla-svg, resvg on screen, text in the bundled fonts), Photoshop (.psd composite), BMP, and Illustrator files saved with PDF compatibility (.ai). New `designcraft-images` crate (L2) for sniffing, sizes and decoding. EPS is still missing. +- Place SVG (vector in PDF export via krilla-svg, resvg on screen, text in the bundled fonts), Photoshop (.psd/.psb composite in its colour mode — Bitmap, Grayscale, Duotone, Indexed, RGB, CMYK, Lab, 8/16-bit — with merged transparency; extra alpha and spot channels don't change the image), BMP, and Illustrator files saved with PDF compatibility (.ai). New `designcraft-images` crate (L2) for sniffing, sizes and decoding. EPS is still missing. - View › Display Performance: Fast (grey boxes, no effects, ⌥⇧⌘Z), Typical (72 ppi proxies, ⌥⌘Z), High Quality (⌥⌘H, our default — the renderer is fast enough). - OpenType menu (Character panel): discretionary ligatures, fractions, ordinal, swash, titling, contextual alternates, slashed zero, the four figure styles and stylistic sets 1–20; `type.openType`; mapped to IDML's OTF attributes. - Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`. diff --git a/crates/images/Cargo.toml b/crates/images/Cargo.toml index b7be5df..de878d5 100644 --- a/crates/images/Cargo.toml +++ b/crates/images/Cargo.toml @@ -9,8 +9,8 @@ description = "DesignCraft placed-graphic decoding: raster formats (PNG, JPEG, G workspace = true [dependencies] +designcraft-color = { workspace = true } designcraft-fonts = { workspace = true } image = { workspace = true } tiff = { workspace = true } resvg = { workspace = true } -psd = { workspace = true } diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index 48e32f7..b770140 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -1,5 +1,5 @@ //! Placed graphics: format sniffing, sizes and decoding for the renderer and PDF export. -//! Rasters go through `image` (PNG, JPEG, GIF, WebP, TIFF, BMP) or `psd` (Photoshop's merged +//! Rasters go through `image` (PNG, JPEG, GIF, WebP, TIFF, BMP) or [`psd`] (Photoshop's merged //! composite); SVG is parsed with usvg (text set in the bundled fonts) and rasterised with resvg //! for the screen — PDF export draws the same tree as vectors. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -9,6 +9,7 @@ use std::sync::{Arc, OnceLock}; pub use resvg::usvg; mod eps; +mod psd; pub use eps::{bounding_box as eps_bounding_box, eps_proxy, is_eps}; /// CSS pixels (SVG user units) → points. @@ -99,8 +100,7 @@ pub fn pixel_size(bytes: &[u8]) -> Option<(u32, u32)> { /// Straight (non-premultiplied) RGBA8 of a raster. pub fn decode_rgba(bytes: &[u8]) -> Option { if is_psd(bytes) { - let p = psd::Psd::from_bytes(bytes).ok()?; - return image::RgbaImage::from_raw(p.width(), p.height(), p.rgba()); + return psd::decode(bytes); } Some(image::load_from_memory(bytes).ok()?.to_rgba8()) } diff --git a/crates/images/src/psd.rs b/crates/images/src/psd.rs new file mode 100644 index 0000000..7da9e3a --- /dev/null +++ b/crates/images/src/psd.rs @@ -0,0 +1,854 @@ +//! Photoshop documents (.psd, and .psb large documents): the merged composite in the image data +//! section, decoded by its colour mode per the public file format specification. Extra alpha and +//! spot channels don't change the placed image; when the layer count is negative, the first extra +//! channel is the composite's merged transparency. + +use designcraft_color::Color; +use designcraft_color::cms::{Lab, lab::lab_to_srgb}; + +/// The largest decoded image, in RGBA bytes (the `image` crate's default allocation limit). +const MAX_RGBA_BYTES: usize = 512 << 20; + +/// Tagged blocks whose length is 8 bytes in a PSB. +const LONG_KEYS: [&[u8; 4]; 13] = + [b"LMsk", b"Lr16", b"Lr32", b"Layr", b"Mt16", b"Mt32", b"Mtrn", b"Alph", b"FMsk", b"lnk2", b"FEid", b"FXid", b"PxSD"]; + +/// Big-endian reads; each one fails at the end of the data. +struct Reader<'a> { + b: &'a [u8], +} + +impl<'a> Reader<'a> { + fn take(&mut self, n: usize) -> Option<&'a [u8]> { + let (head, rest) = self.b.split_at_checked(n)?; + self.b = rest; + Some(head) + } + fn u16(&mut self) -> Option { + Some(u16::from_be_bytes(self.take(2)?.try_into().ok()?)) + } + fn u32(&mut self) -> Option { + Some(u32::from_be_bytes(self.take(4)?.try_into().ok()?)) + } + /// A length-prefixed block: 4-byte length, or 8 bytes when `long`. + fn block(&mut self, long: bool) -> Option<&'a [u8]> { + let n = if long { u64::from_be_bytes(self.take(8)?.try_into().ok()?) } else { u64::from(self.u32()?) }; + self.take(usize::try_from(n).ok()?) + } +} + +#[derive(Clone, Copy, PartialEq)] +enum Mode { + Bitmap, + /// Grayscale, and Duotone (its data is the gray channel; the inks are in the colour mode data). + Gray, + Indexed, + Rgb, + Cmyk, + Lab, +} + +impl Mode { + fn parse(v: u16) -> Option { + Some(match v { + 0 => Mode::Bitmap, + 1 | 8 => Mode::Gray, + 2 => Mode::Indexed, + 3 => Mode::Rgb, + 4 => Mode::Cmyk, + 9 => Mode::Lab, + _ => return None, + }) + } + fn colour_channels(self) -> usize { + match self { + Mode::Bitmap | Mode::Gray | Mode::Indexed => 1, + Mode::Rgb | Mode::Lab => 3, + Mode::Cmyk => 4, + } + } + /// The stored channel values of white: a composite with transparency is matted against it. + fn white(self) -> Option<[f32; 4]> { + match self { + Mode::Gray | Mode::Rgb | Mode::Cmyk => Some([255.0; 4]), + Mode::Lab => Some([255.0, 128.0, 128.0, 0.0]), + Mode::Bitmap | Mode::Indexed => None, + } + } +} + +/// The merged composite as straight RGBA8, `None` when the file is damaged or unsupported (32-bit, +/// Multichannel, ZIP-compressed composite). +pub(crate) fn decode(bytes: &[u8]) -> Option { + let mut r = Reader { b: bytes }; + if r.take(4)? != b"8BPS" { + return None; + } + let psb = match r.u16()? { + 1 => false, + 2 => true, + _ => return None, + }; + r.take(6)?; + let channels = usize::from(r.u16()?); + let (h, w) = (r.u32()?, r.u32()?); + let depth = r.u16()?; + let mode = Mode::parse(r.u16()?)?; + let max_side = if psb { 300_000 } else { 30_000 }; + let depth_ok = match mode { + Mode::Bitmap => depth == 1, + Mode::Indexed => depth == 8, + _ => depth == 8 || depth == 16, + }; + let colours = mode.colour_channels(); + if !(1..=56).contains(&channels) || channels < colours || !(1..=max_side).contains(&w) || !(1..=max_side).contains(&h) || !depth_ok { + return None; + } + let (wu, hu) = (usize::try_from(w).ok()?, usize::try_from(h).ok()?); + let pixels = wu.checked_mul(hu)?; + let (stride, len) = (wu.checked_mul(4)?, pixels.checked_mul(4)?); + if len > MAX_RGBA_BYTES { + return None; + } + + let colour_mode_data = r.block(false)?; + r.block(false)?; // image resources + let layers = r.block(psb)?; + let transparency = channels > colours && layer_count(layers, psb).is_some_and(|n| n < 0); + let row_bytes = wu.checked_mul(usize::from(depth))?.div_ceil(8); + let data = ImageData::parse(r, psb, channels, row_bytes, hu)?; + let palette = if mode == Mode::Indexed { Some(palette(colour_mode_data)?) } else { None }; + // Header sizes are untrusted: allocate only once the data to fill the image is present. + data.holds(colours + usize::from(transparency))?; + + let mut rgba = vec![0u8; len]; + for c in 0..colours { + let mut rows = rgba.chunks_exact_mut(stride); + data.channel(c, |src| { + if let Some(dst) = rows.next() { + fill(dst, c, 4, src, depth); + } + })?; + } + let mut alpha = Vec::new(); + if transparency { + alpha = vec![0u8; pixels]; + let mut rows = alpha.chunks_exact_mut(wu); + data.channel(colours, |src| { + if let Some(dst) = rows.next() { + fill(dst, 0, 1, src, depth); + } + })?; + } + + let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; + let byte = |v: f32| v.round().clamp(0.0, 255.0) as u8; + let white = mode.white(); + for (i, px) in rgba.chunks_exact_mut(4).enumerate() { + let Ok(px) = <&mut [u8; 4]>::try_from(px) else { continue }; + let a = alpha.get(i).copied().unwrap_or(255); + let mut v = px.map(f32::from); + // The composite is matted against white where it's partly transparent: un-matte it. + if let Some(white) = white + && a > 0 + && a < 255 + { + for (v, w) in v.iter_mut().zip(white).take(colours) { + *v = (w + (*v - w) * 255.0 / f32::from(a)).round().clamp(0.0, 255.0); + } + } + let [v0, v1, v2, v3] = v; + *px = match mode { + Mode::Bitmap | Mode::Gray => [byte(v0), byte(v0), byte(v0), a], + Mode::Indexed => { + let Some([pr, pg, pb]) = &palette else { return None }; + let i = usize::from(px[0]); + [pr[i], pg[i], pb[i], a] + } + Mode::Rgb => [byte(v0), byte(v1), byte(v2), a], + Mode::Cmyk => { + // Stored inverted: 255 is no ink. + let ink = |v: f32| 1.0 - v / 255.0; + let [r, g, b] = Color::cmyk(ink(v0), ink(v1), ink(v2), ink(v3)).to_rgb_uncalibrated(); + [q(r), q(g), q(b), a] + } + Mode::Lab => { + let [r, g, b] = lab_to_srgb(Lab::new(v0 * 100.0 / 255.0, v1 - 128.0, v2 - 128.0)); + [q(r), q(g), q(b), a] + } + }; + } + image::RgbaImage::from_raw(w, h, rgba) +} + +/// The layer count from the layer and mask section. 16- and 32-bit documents keep their layer +/// info in an `Lr16` / `Lr32` tagged block after the global layer mask info. +fn layer_count(section: &[u8], psb: bool) -> Option { + let count = |info: &[u8]| Some(i16::from_be_bytes(info.get(..2)?.try_into().ok()?)); + let mut r = Reader { b: section }; + let info = r.block(psb)?; + if !info.is_empty() { + return count(info); + } + r.block(false)?; // global layer mask info + while !r.b.is_empty() { + let sig = r.take(4)?; + if sig != b"8BIM" && sig != b"8B64" { + return None; + } + let key = r.take(4)?; + let data = r.block(psb && LONG_KEYS.iter().any(|k| k.as_slice() == key))?; + if matches!(key, b"Lr16" | b"Lr32" | b"Layr") { + return count(data); + } + // Padding to 2 or 4 bytes (a signature never starts with 0). + while r.b.first() == Some(&0) { + r.take(1)?; + } + } + None +} + +/// An indexed-colour palette: 256 reds, then greens, then blues. +fn palette(data: &[u8]) -> Option<[[u8; 256]; 3]> { + let part = |i: usize| data.get(i * 256..(i + 1) * 256)?.try_into().ok(); + Some([part(0)?, part(1)?, part(2)?]) +} + +/// The composite's channels: raw planes, or PackBits-compressed rows. +struct ImageData<'a> { + /// Each row's compressed length, channel after channel (2 bytes each, 4 in a PSB); empty for raw. + counts: &'a [u8], + count_size: usize, + data: &'a [u8], + row_bytes: usize, + height: usize, +} + +impl<'a> ImageData<'a> { + fn parse(mut r: Reader<'a>, psb: bool, channels: usize, row_bytes: usize, height: usize) -> Option { + let count_size = if psb { 4 } else { 2 }; + let counts = match r.u16()? { + 0 => &[][..], + 1 => r.take(channels.checked_mul(height)?.checked_mul(count_size)?)?, + // ZIP appears only in layers, never in the composite. + _ => return None, + }; + Some(ImageData { counts, count_size, data: r.b, row_bytes, height }) + } + + /// `Some` when the data can hold the first `channels` channels: whole raw planes, or row + /// counts that fit in the data and are each long enough to unpack to a full row (PackBits + /// writes at most 128 bytes per 2 input bytes). + fn holds(&self, channels: usize) -> Option<()> { + let rows = channels.checked_mul(self.height)?; + if self.counts.is_empty() { + let need = self.row_bytes.checked_mul(rows)?; + return (need <= self.data.len()).then_some(()); + } + let min_row = self.row_bytes.div_ceil(128).checked_mul(2)?; + let mut total = 0usize; + for c in self.counts.chunks_exact(self.count_size).take(rows) { + let n = c.iter().fold(0usize, |n, &b| n << 8 | usize::from(b)); + if n < min_row { + return None; + } + total = total.checked_add(n)?; + } + (total <= self.data.len()).then_some(()) + } + + /// Channel `ch`'s rows, uncompressed, to `row` from top to bottom. + fn channel(&self, ch: usize, mut row: impl FnMut(&[u8])) -> Option<()> { + if self.row_bytes == 0 { + return None; + } + if self.counts.is_empty() { + let plane = self.row_bytes.checked_mul(self.height)?; + let start = ch.checked_mul(plane)?; + self.data.get(start..start.checked_add(plane)?)?.chunks_exact(self.row_bytes).for_each(row); + return Some(()); + } + let mut counts = self.counts.chunks_exact(self.count_size).map(|c| c.iter().fold(0usize, |n, &b| n << 8 | usize::from(b))); + // This channel starts after every earlier channel's rows. + let mut at = counts.by_ref().take(ch.checked_mul(self.height)?).try_fold(0usize, |at, n| at.checked_add(n))?; + for _ in 0..self.height { + let n = counts.next()?; + let end = at.checked_add(n)?; + row(&unpack_bits(self.data.get(at..end)?, self.row_bytes)?); + at = end; + } + Some(()) + } +} + +/// One PackBits row; `None` unless it unpacks to exactly `len` bytes. +fn unpack_bits(mut src: &[u8], len: usize) -> Option> { + let mut out = Vec::with_capacity(len); + while let Some((&head, rest)) = src.split_first() { + src = rest; + match head as i8 { + -128 => {} + n @ 0.. => { + let (literal, rest) = src.split_at_checked(n as usize + 1)?; + out.extend_from_slice(literal); + src = rest; + } + n => { + let (&v, rest) = src.split_first()?; + out.resize(out.len() + (1 - n as isize) as usize, v); + src = rest; + } + } + if out.len() > len { + return None; + } + } + (out.len() == len).then_some(out) +} + +/// A row of `depth`-bit samples as 8-bit values, into every `step`-th byte of `dst` from `slot`. +/// Bitmap mode stores black as 1. +fn fill(dst: &mut [u8], slot: usize, step: usize, src: &[u8], depth: u16) { + let dst = dst.iter_mut().skip(slot).step_by(step); + match depth { + 1 => dst.zip(src.iter().flat_map(|&b| (0..8).rev().map(move |i| if (b >> i) & 1 == 1 { 0 } else { 255 }))).for_each(|(d, s)| *d = s), + 16 => dst + .zip(src.chunks_exact(2).map(|c| c.iter().fold(0u32, |n, &b| n << 8 | u32::from(b)))) + .for_each(|(d, s)| *d = ((s * 255 + 32_767) / 65_535) as u8), + _ => dst.zip(src).for_each(|(d, &s)| *d = s), + } +} + +#[cfg(test)] +mod tests { + use crate::decode_rgba; + + const W: u32 = 12; + const H: u32 = 8; + + /// A `W`×`H` 8-bit plane: `bg` everywhere, `val` in the square `[x0, x1)×[y0, y1)`. + fn plane(bg: u8, square: Option<(u32, u32, u32, u32, u8)>) -> Vec { + let mut p = vec![bg; (W * H) as usize]; + if let Some((x0, y0, x1, y1, val)) = square { + for y in y0..y1 { + for x in x0..x1 { + p[(y * W + x) as usize] = val; + } + } + } + p + } + /// Mid grey with a dark square in the middle. + fn gray() -> Vec { + plane(150, Some((4, 2, 8, 6, 60))) + } + /// An alpha channel (a saved selection): a square of 255 top-left. + fn sel() -> Vec { + plane(0, Some((1, 1, 4, 4, 255))) + } + /// A spot channel: a square of 255 bottom-right. + fn spot() -> Vec { + plane(0, Some((8, 5, 11, 7, 255))) + } + + enum Layers { + /// An empty layer and mask section. + None, + /// Layer info with this layer count and one layer record. + Count(i16), + /// Empty layer info; the layers in an `Lr16` tagged block (16-bit documents). + Lr16(i16), + } + + /// A Photoshop file for the tests, written per the public file format spec. + struct Psd { + /// 1 = PSD, 2 = PSB. + version: u16, + mode: u16, + depth: u16, + w: u32, + h: u32, + /// Composite channels: rows of big-endian samples. + channels: Vec>, + palette: Vec, + layers: Layers, + rle: bool, + } + + impl Psd { + fn new(mode: u16, channels: Vec>) -> Self { + Psd { version: 1, mode, depth: 8, w: W, h: H, channels, palette: Vec::new(), layers: Layers::None, rle: false } + } + fn rle(self) -> Self { + Psd { rle: true, ..self } + } + fn layers(self, layers: Layers) -> Self { + Psd { layers, ..self } + } + fn psb(&self) -> bool { + self.version == 2 + } + /// A section length: 4 bytes, 8 in PSB. + fn len(&self, n: usize) -> Vec { + if self.psb() { (n as u64).to_be_bytes().to_vec() } else { (n as u32).to_be_bytes().to_vec() } + } + fn row_bytes(&self) -> usize { + (self.w as usize * self.depth as usize).div_ceil(8) + } + /// Compression + channel data, as in the image data section. + fn channel_data(&self, planes: &[Vec], rle: bool) -> Vec { + let mut b = Vec::new(); + if !rle { + b.extend(0u16.to_be_bytes()); + planes.iter().for_each(|p| b.extend(p)); + return b; + } + b.extend(1u16.to_be_bytes()); + let rows: Vec> = planes.iter().flat_map(|p| p.chunks(self.row_bytes()).map(packbits)).collect(); + for r in &rows { + if self.psb() { b.extend((r.len() as u32).to_be_bytes()) } else { b.extend((r.len() as u16).to_be_bytes()) } + } + rows.iter().for_each(|r| b.extend(r)); + b + } + /// Layer info: the count, one full-canvas layer (transparency + first channel), its data. + fn layer_info(&self, count: i16) -> Vec { + let plane_len = self.row_bytes() * self.h as usize; + let mut b = Vec::new(); + b.extend(count.to_be_bytes()); + for v in [0, 0, self.h as i32, self.w as i32] { + b.extend(v.to_be_bytes()); + } + b.extend(2u16.to_be_bytes()); + for id in [-1i16, 0] { + b.extend(id.to_be_bytes()); + b.extend(self.len(2 + plane_len)); + } + b.extend(b"8BIMnorm"); + b.extend([255, 0, 0, 0]); + let mut extra = Vec::new(); + extra.extend(0u32.to_be_bytes()); // layer mask data + extra.extend(0u32.to_be_bytes()); // blending ranges + extra.extend([7, b'L', b'a', b'y', b'e', b'r', b' ', b'1']); // name, padded to 4 + b.extend((extra.len() as u32).to_be_bytes()); + b.extend(extra); + let first = self.channels.first().cloned().unwrap_or_default(); + b.extend(self.channel_data(&[vec![255; plane_len]], false)); + b.extend(self.channel_data(&[first], false)); + if b.len() % 2 == 1 { + b.push(0); + } + b + } + fn layer_and_mask(&self) -> Vec { + let mut s = Vec::new(); + match self.layers { + Layers::None => return s, + Layers::Count(n) => { + let info = self.layer_info(n); + s.extend(self.len(info.len())); + s.extend(info); + s.extend(0u32.to_be_bytes()); // global layer mask info + } + Layers::Lr16(n) => { + s.extend(self.len(0)); + s.extend(0u32.to_be_bytes()); + // An unrelated tagged block first (4-byte length, padded to 4). + s.extend(b"8BIMPatt"); + s.extend(3u32.to_be_bytes()); + s.extend([1, 2, 3, 0]); + let mut info = self.layer_info(n); + while !info.len().is_multiple_of(4) { + info.push(0); + } + s.extend(b"8BIMLr16"); + s.extend(self.len(info.len())); + s.extend(info); + } + } + s + } + fn write(&self) -> Vec { + let mut b = Vec::new(); + b.extend(b"8BPS"); + b.extend(self.version.to_be_bytes()); + b.extend([0u8; 6]); + b.extend((self.channels.len() as u16).to_be_bytes()); + b.extend(self.h.to_be_bytes()); + b.extend(self.w.to_be_bytes()); + b.extend(self.depth.to_be_bytes()); + b.extend(self.mode.to_be_bytes()); + b.extend((self.palette.len() as u32).to_be_bytes()); + b.extend(&self.palette); + // Image resources: ResolutionInfo (1005), 72 ppi. + let mut res = Vec::new(); + res.extend(b"8BIM"); + res.extend(1005u16.to_be_bytes()); + res.extend([0, 0]); + res.extend(16u32.to_be_bytes()); + for v in [72u32 << 16, 0x0001_0001, 72 << 16, 0x0001_0001] { + res.extend(v.to_be_bytes()); + } + b.extend((res.len() as u32).to_be_bytes()); + b.extend(res); + let lm = self.layer_and_mask(); + b.extend(self.len(lm.len())); + b.extend(lm); + b.extend(self.channel_data(&self.channels, self.rle)); + b + } + } + + /// PackBits: repeat runs of 2+ bytes, literals otherwise. + fn packbits(row: &[u8]) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < row.len() { + let run = row[i..].iter().take(128).take_while(|&&v| v == row[i]).count(); + if run >= 2 { + out.push((1 - run as i16) as i8 as u8); + out.push(row[i]); + i += run; + continue; + } + let start = i; + while i < row.len() && i - start < 128 && !(i + 1 < row.len() && row[i + 1] == row[i]) { + i += 1; + } + out.push((i - start - 1) as u8); + out.extend(&row[start..i]); + } + out + } + + fn pixels(b: &[u8]) -> Vec<[u8; 4]> { + let img = decode_rgba(b).expect("decodes"); + assert_eq!(img.dimensions(), (W, H)); + img.pixels().map(|p| p.0).collect() + } + + fn at(x: u32, y: u32) -> usize { + (y * W + x) as usize + } + + #[test] + fn grayscale_extra_channels_leave_the_gray_composite() { + let extras = [sel(), spot(), sel(), plane(255, None)]; + for n in 0..=extras.len() { + for rle in [false, true] { + for layered in [false, true] { + let mut ch = vec![gray()]; + ch.extend(extras[..n].iter().cloned()); + let mut psd = Psd::new(1, ch); + psd.rle = rle; + if layered { + psd.layers = Layers::Count(1); + } + let want: Vec<[u8; 4]> = gray().iter().map(|&g| [g, g, g, 255]).collect(); + assert_eq!(pixels(&psd.write()), want, "gray + {n} extra channels, rle {rle}, layered {layered}"); + } + } + } + } + + #[test] + fn negative_layer_count_makes_the_first_extra_channel_the_transparency() { + let hole = plane(255, Some((2, 3, 5, 6, 0))); + for rle in [false, true] { + for extras in [vec![], vec![spot()], vec![spot(), sel(), sel()]] { + let mut ch = vec![gray(), hole.clone()]; + ch.extend(extras.iter().cloned()); + let mut psd = Psd::new(1, ch).layers(Layers::Count(-1)); + psd.rle = rle; + let px = pixels(&psd.write()); + for (i, p) in px.iter().enumerate() { + if hole[i] == 0 { + assert_eq!(p[3], 0, "pixel {i} is in the hole"); + } else { + let g = gray()[i]; + assert_eq!(*p, [g, g, g, 255], "pixel {i}, rle {rle}, {} extras", extras.len()); + } + } + } + } + } + + #[test] + fn rgb_with_a_spot_channel_is_opaque_rgb() { + let (r, g, b) = (plane(200, None), plane(10, Some((0, 0, 6, 8, 90))), plane(30, None)); + for rle in [false, true] { + let mut psd = Psd::new(3, vec![r.clone(), g.clone(), b.clone(), spot()]); + psd.rle = rle; + let px = pixels(&psd.write()); + assert_eq!(px[at(0, 0)], [200, 90, 30, 255]); + assert_eq!(px[at(9, 6)], [200, 10, 30, 255], "the spot square doesn't hide the image"); + assert!(px.iter().all(|p| p[3] == 255)); + } + } + + #[test] + fn rgb_with_merged_transparency_is_rgba_unmatted_from_white() { + // Straight red at alpha 128, stored matted over white: 0 → 255 − 128 = 127. + let r = plane(255, None); + let gb = plane(0, Some((0, 0, 1, 1, 127))); + let a = plane(255, Some((0, 0, 1, 1, 128))); + let psd = Psd::new(3, vec![r, gb.clone(), gb, a, sel()]).layers(Layers::Count(-1)).rle(); + let px = pixels(&psd.write()); + assert_eq!(px[at(0, 0)], [255, 0, 0, 128]); + assert_eq!(px[at(5, 5)], [255, 0, 0, 255]); + assert_eq!(px[at(2, 2)][3], 255, "the alpha selection after the transparency is ignored"); + } + + #[test] + fn cmyk_converts_like_other_cmyk_images() { + // Stored inverted: 255 = no ink. Cyan 100 % on the left, 50 % black on the right. + let c = plane(255, Some((0, 0, 6, 8, 0))); + let m = plane(255, None); + let y = plane(255, None); + let k = plane(255, Some((6, 0, 12, 8, 127))); + let rgb8 = |c: f32, m: f32, y: f32, k: f32| { + let [r, g, b] = designcraft_color::Color::cmyk(c, m, y, k).to_rgb_uncalibrated(); + [r, g, b].map(|v| (v * 255.0).round() as u8) + }; + for rle in [false, true] { + // An extra alpha channel (5 channels) doesn't become the alpha. + let mut psd = Psd::new(4, vec![c.clone(), m.clone(), y.clone(), k.clone(), sel()]); + psd.rle = rle; + let px = pixels(&psd.write()); + let [r, g, b] = rgb8(1.0, 0.0, 0.0, 0.0); + assert_eq!(px[at(0, 0)], [r, g, b, 255]); + let [r, g, b] = rgb8(0.0, 0.0, 0.0, 128.0 / 255.0); + assert_eq!(px[at(11, 7)], [r, g, b, 255]); + assert!(px.iter().all(|p| p[3] == 255)); + } + } + + /// 8-bit plane → 16-bit big-endian samples (v × 257). + fn wide(p: &[u8]) -> Vec { + p.iter().flat_map(|&v| (v as u16 * 257).to_be_bytes()).collect() + } + + #[test] + fn sixteen_bit_grayscale() { + for rle in [false, true] { + let mut psd = Psd::new(1, vec![wide(&gray()), wide(&sel()), wide(&spot())]); + psd.depth = 16; + psd.rle = rle; + let want: Vec<[u8; 4]> = gray().iter().map(|&g| [g, g, g, 255]).collect(); + assert_eq!(pixels(&psd.write()), want); + } + } + + #[test] + fn sixteen_bit_transparency_from_the_lr16_block() { + let hole = plane(255, Some((2, 3, 5, 6, 0))); + let mut psd = Psd::new(1, vec![wide(&gray()), wide(&hole), wide(&spot())]).layers(Layers::Lr16(-1)); + psd.depth = 16; + let px = pixels(&psd.write()); + assert_eq!(px[at(3, 4)][3], 0); + assert_eq!(px[at(9, 6)], [150, 150, 150, 255]); + } + + #[test] + fn psb_with_rle_and_transparency() { + let hole = plane(255, Some((2, 3, 5, 6, 0))); + let mut psd = Psd::new(1, vec![gray(), hole, sel()]).layers(Layers::Count(-1)).rle(); + psd.version = 2; + let px = pixels(&psd.write()); + assert_eq!(px[at(3, 4)][3], 0); + assert_eq!(px[at(2, 2)], [150, 150, 150, 255]); + assert_eq!(px[at(5, 3)], [60, 60, 60, 255]); + } + + #[test] + fn indexed_uses_the_palette() { + let mut palette = vec![0u8; 768]; + palette[3] = 10; // index 3: R + palette[256 + 3] = 20; // G + palette[512 + 3] = 30; // B + palette[7] = 255; // index 7: red + let mut psd = Psd::new(2, vec![plane(3, Some((0, 0, 1, 1, 7)))]); + psd.palette = palette; + let px = pixels(&psd.write()); + assert_eq!(px[at(0, 0)], [255, 0, 0, 255]); + assert_eq!(px[at(1, 0)], [10, 20, 30, 255]); + } + + #[test] + fn bitmap_set_bits_are_black() { + // 12 px wide: 2 bytes per row. The first pixel of each row is black. + let mut psd = Psd::new(0, vec![[0x80u8, 0x00].repeat(H as usize)]); + psd.depth = 1; + let px = pixels(&psd.write()); + assert_eq!(px[at(0, 3)], [0, 0, 0, 255]); + assert_eq!(px[at(1, 3)], [255, 255, 255, 255]); + assert_eq!(px[at(11, 3)], [255, 255, 255, 255]); + } + + #[test] + fn lab_white_and_black() { + let l = plane(255, Some((0, 0, 1, 1, 0))); + let ab = plane(128, None); + let psd = Psd::new(9, vec![l, ab.clone(), ab, sel()]); + let px = pixels(&psd.write()); + assert_eq!(px[at(0, 0)], [0, 0, 0, 255]); + for v in &px[at(5, 5)][..3] { + assert!(*v >= 254, "L 100 is white: {:?}", px[at(5, 5)]); + } + } + + fn valid_files() -> Vec> { + let hole = plane(255, Some((2, 3, 5, 6, 0))); + let mut wide16 = Psd::new(1, vec![wide(&gray()), wide(&hole)]).layers(Layers::Lr16(-1)).rle(); + wide16.depth = 16; + let mut psb = Psd::new(3, vec![gray(), gray(), gray(), hole.clone()]).layers(Layers::Count(-1)).rle(); + psb.version = 2; + vec![ + Psd::new(1, vec![gray(), sel(), spot()]).write(), + Psd::new(1, vec![gray(), hole.clone(), spot()]).layers(Layers::Count(-1)).rle().write(), + Psd::new(4, vec![gray(), gray(), gray(), gray()]).rle().write(), + wide16.write(), + psb.write(), + ] + } + + #[test] + fn truncated_files_never_panic() { + for file in valid_files() { + let full = decode_rgba(&file).expect("the full file decodes"); + for n in 0..file.len() { + if let Some(img) = decode_rgba(&file[..n]) { + assert_eq!(img, full, "a truncated file decodes only when the composite survived"); + } + } + } + } + + #[test] + fn corrupted_files_never_panic() { + // A small LCG: deterministic byte flips across every valid file. + let mut seed = 0x2545_f491_u32; + let mut next = || { + seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + seed >> 8 + }; + for file in valid_files() { + for _ in 0..2000 { + let mut b = file.clone(); + for _ in 0..1 + next() % 4 { + let i = 4 + next() as usize % (b.len() - 4); + b[i] = next() as u8; + } + let _ = decode_rgba(&b); + } + } + } + + #[test] + fn hostile_headers_are_rejected() { + let ok = Psd::new(1, vec![gray()]).write(); + let set = |at: usize, v: &[u8]| { + let mut b = ok.clone(); + b[at..at + v.len()].copy_from_slice(v); + b + }; + assert!(decode_rgba(&ok).is_some()); + assert!(decode_rgba(&set(4, &3u16.to_be_bytes())).is_none(), "version 3"); + assert!(decode_rgba(&set(12, &0u16.to_be_bytes())).is_none(), "no channels"); + assert!(decode_rgba(&set(12, &57u16.to_be_bytes())).is_none(), "57 channels"); + assert!(decode_rgba(&set(14, &0u32.to_be_bytes())).is_none(), "zero height"); + assert!(decode_rgba(&set(18, &0u32.to_be_bytes())).is_none(), "zero width"); + assert!(decode_rgba(&set(18, &30_001u32.to_be_bytes())).is_none(), "wider than a PSD can be"); + assert!(decode_rgba(&set(14, &u32::MAX.to_be_bytes())).is_none(), "absurd height"); + assert!(decode_rgba(&set(22, &32u16.to_be_bytes())).is_none(), "32-bit"); + assert!(decode_rgba(&set(22, &7u16.to_be_bytes())).is_none(), "7-bit"); + assert!(decode_rgba(&set(24, &7u16.to_be_bytes())).is_none(), "multichannel"); + // Huge but within the side limit: refused before allocating. + let mut big = set(14, &30_000u32.to_be_bytes()); + big[18..22].copy_from_slice(&30_000u32.to_be_bytes()); + assert!(decode_rgba(&big).is_none(), "too many pixels"); + // RGB with only 2 channels; indexed without a palette. + assert!(decode_rgba(&Psd::new(3, vec![gray(), gray()]).write()).is_none()); + assert!(decode_rgba(&Psd::new(2, vec![gray()]).write()).is_none()); + } + + #[test] + fn bad_compression_is_rejected() { + let ok = Psd::new(1, vec![gray()]).rle().write(); + assert!(decode_rgba(&ok).is_some()); + let comp = ok.len() - Psd::new(1, vec![gray()]).rle().channel_data(&[gray()], true).len(); + for method in [2u16, 3, 4] { + let mut b = ok.clone(); + b[comp..comp + 2].copy_from_slice(&method.to_be_bytes()); + assert!(decode_rgba(&b).is_none(), "compression {method}"); + } + // A row count past the end of the data. + let mut b = ok.clone(); + b[comp + 2..comp + 4].copy_from_slice(&u16::MAX.to_be_bytes()); + assert!(decode_rgba(&b).is_none()); + // A row that unpacks to the wrong width: a literal of 1 byte instead of a run of 12. + let mut rows = Vec::new(); + for _ in 0..H { + rows.push(vec![0u8, 150]); + } + let mut b = ok[..comp].to_vec(); + b.extend(1u16.to_be_bytes()); + rows.iter().for_each(|r| b.extend((r.len() as u16).to_be_bytes())); + rows.iter().for_each(|r| b.extend(r)); + assert!(decode_rgba(&b).is_none()); + } + + #[test] + fn packbits_round_trips() { + let row = [1u8, 1, 1, 2, 3, 4, 4, 5, 6, 6, 6, 6, 7]; + assert_eq!(super::unpack_bits(&packbits(&row), row.len()), Some(row.to_vec())); + // -128 is a no-op. + assert_eq!(super::unpack_bits(&[0x80, 0x01, 9, 8], 2), Some(vec![9, 8])); + assert_eq!(super::unpack_bits(&[0x05, 1, 2], 6), None, "literal past the end of the data"); + assert_eq!(super::unpack_bits(&[0xFE, 7], 2), None, "run longer than the row"); + } + + #[test] + fn huge_header_without_data_is_rejected_before_allocating() { + // A gray 11000×11000 image (≈ 460 MiB as RGBA, under the cap) with almost no image data. + let (w, h) = (11_000u32, 11_000u32); + let mut head = Vec::new(); + head.extend(b"8BPS"); + head.extend(1u16.to_be_bytes()); + head.extend([0u8; 6]); + head.extend(1u16.to_be_bytes()); // channels + head.extend(h.to_be_bytes()); + head.extend(w.to_be_bytes()); + head.extend(8u16.to_be_bytes()); + head.extend(1u16.to_be_bytes()); // grayscale + head.extend([0u8; 12]); // empty colour mode data, image resources, layers + let holds = |b: &[u8]| { + let r = super::Reader { b: &b[head.len()..] }; + super::ImageData::parse(r, false, 1, w as usize, h as usize).expect("parses").holds(1) + }; + // Raw: a few bytes instead of 11000 × 11000. + let mut raw = head.clone(); + raw.extend(0u16.to_be_bytes()); + raw.extend([0u8; 16]); + assert!(holds(&raw).is_none()); + assert!(super::decode(&raw).is_none()); + // RLE: every row count present and plausible, but the rows themselves missing. + let mut rle = head.clone(); + rle.extend(1u16.to_be_bytes()); + for _ in 0..h { + rle.extend(174u16.to_be_bytes()); // 2 × ⌈11000 / 128⌉: the shortest full row + } + assert!(holds(&rle).is_none()); + assert!(super::decode(&rle).is_none()); + // RLE row counts too short to unpack to a full row, even with the data present. + let mut short = head.clone(); + short.extend(1u16.to_be_bytes()); + for _ in 0..h { + short.extend(2u16.to_be_bytes()); + } + short.extend(vec![0u8; 2 * h as usize]); + assert!(holds(&short).is_none()); + assert!(super::decode(&short).is_none()); + } +} diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 43979ee..b105483 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -2,10 +2,19 @@ //! menu items and panel names. Untranslated strings stay English. The translations are our own. mod ar; +mod pt_br; /// Supported interface languages: (code, name in that language). -pub const LANGUAGES: &[(&str, &str)] = - &[("", "English"), ("de", "Deutsch"), ("fr", "Français"), ("es", "Español"), ("ja", "日本語"), ("zh", "简体中文"), ("ar", "العربية")]; +pub const LANGUAGES: &[(&str, &str)] = &[ + ("", "English"), + ("de", "Deutsch"), + ("fr", "Français"), + ("es", "Español"), + ("ja", "日本語"), + ("zh", "简体中文"), + ("ar", "العربية"), + ("pt-br", "Português (Brasil)"), +]; /// English → [German, French, Spanish, Japanese, Simplified Chinese]. const TABLE: &[(&str, [&str; 5])] = &[ @@ -2674,6 +2683,9 @@ pub fn tr<'a>(lang: &str, s: &'a str) -> &'a str { if lang == "ar" { return ar::TABLE.iter().find(|(en, _)| *en == s).map_or(s, |(_, t)| *t); } + if lang == "pt-br" { + return pt_br::TABLE.iter().find(|(en, _)| *en == s).map_or(s, |(_, t)| *t); + } let Some(c) = column(lang) else { return s }; match TABLE.iter().find(|(en, _)| *en == s) { Some((_, t)) => t[c], @@ -2709,11 +2721,18 @@ mod tests { assert_eq!(tr("", "File"), "File"); assert_eq!(tr("ar", "File"), "ملف"); assert_eq!(tr("ar", "Unknown label"), "Unknown label"); + assert_eq!(tr("pt-br", "File"), "Arquivo"); + assert_eq!(tr("pt-br", "New Document…"), "Novo documento…"); + assert_eq!(tr("pt-br", "Unknown label"), "Unknown label"); assert!(is_rtl("ar")); for (i, (en, translation)) in ar::TABLE.iter().enumerate() { assert!(ar::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}"); assert!(!translation.is_empty(), "{en}"); } + for (i, (en, translation)) in pt_br::TABLE.iter().enumerate() { + assert!(pt_br::TABLE[..i].iter().all(|(key, _)| key != en), "duplicate {en}"); + assert!(!translation.is_empty(), "{en}"); + } // Every row is unique and complete. for (i, (en, t)) in TABLE.iter().enumerate() { assert!(TABLE[..i].iter().all(|(e, _)| e != en), "duplicate {en}"); diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs new file mode 100644 index 0000000..fa7973a --- /dev/null +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -0,0 +1,1024 @@ +//! Original Brazilian Portuguese interface translations, written from the English source +//! using ordinary Brazilian Portuguese computing vocabulary. +pub(super) const TABLE: &[(&str, &str)] = &[ + ("File", "Arquivo"), + ("Edit", "Editar"), + ("Layout", "Layout"), + ("Type", "Tipo"), + ("Object", "Objeto"), + ("Table", "Tabela"), + ("View", "Exibir"), + ("Window", "Janela"), + ("Help", "Ajuda"), + ("New", "Novo"), + ("New Document…", "Novo documento…"), + ("New Document", "Novo documento"), + ("Open…", "Abrir…"), + ("Close", "Fechar"), + ("Save", "Salvar"), + ("Save As…", "Salvar como…"), + ("Save a Copy…", "Salvar uma cópia…"), + ("Revert", "Reverter"), + ("Place…", "Inserir…"), + ("Export PDF…", "Exportar PDF…"), + ("Print…", "Imprimir…"), + ("Print", "Imprimir"), + ("Document Setup…", "Configuração do documento…"), + ("Document Setup", "Configuração do documento"), + ("Package…", "Empacotar…"), + ("Undo", "Desfazer"), + ("Redo", "Refazer"), + ("Cut", "Recortar"), + ("Copy", "Copiar"), + ("Paste", "Colar"), + ("Clear", "Limpar"), + ("Delete", "Excluir"), + ("Duplicate", "Duplicar"), + ("Select All", "Selecionar tudo"), + ("Deselect All", "Desmarcar tudo"), + ("Find/Change…", "Localizar/alterar…"), + ("Find/Change", "Localizar/alterar"), + ("Check Spelling…", "Verificar ortografia…"), + ("Preferences…", "Preferências…"), + ("Preferences", "Preferências"), + ("Keyboard Shortcuts…", "Atalhos de teclado…"), + ("Keyboard Shortcuts", "Atalhos de teclado"), + ("Interface Language", "Idioma da interface"), + ("Pages", "Páginas"), + ("Page", "Página"), + ("Margins and Columns…", "Margens e colunas…"), + ("Create Guides…", "Criar guias…"), + ("Numbering & Section Options…", "Opções de numeração e de seção…"), + ("Go to Page…", "Ir para página…"), + ("Go to Page", "Ir para página"), + ("Next Page", "Próxima página"), + ("Previous Page", "Página anterior"), + ("First Page", "Primeira página"), + ("Last Page", "Última página"), + ("Font", "Fonte"), + ("Size", "Tamanho"), + ("Character", "Caractere"), + ("Paragraph", "Parágrafo"), + ("Paragraph Styles", "Estilos de parágrafo"), + ("Character Styles", "Estilos de caractere"), + ("Paragraph Style Options", "Opções de estilo de parágrafo"), + ("Object Styles", "Estilos de objeto"), + ("Glyphs", "Glifos"), + ("Find/Replace Font…", "Localizar/substituir fonte…"), + ("Find/Replace Font", "Localizar/substituir fonte"), + ("Story Editor", "Editor de artigo"), + ("Edit in Story Editor", "Editar no editor de artigo"), + ("Insert Special Character", "Inserir caractere especial"), + ("Insert White Space", "Inserir espaço"), + ("Insert Break Character", "Inserir caractere de quebra"), + ("Change Case", "Alterar maiúsculas/minúsculas"), + ("Text Frame Options…", "Opções de quadro de texto…"), + ("Text Frame Options", "Opções de quadro de texto"), + ("Footnote Options", "Opções de notas de rodapé"), + ("New Cross-Reference", "Nova referência cruzada"), + ("Arrange", "Organizar"), + ("Transform", "Transformar"), + ("Group", "Agrupar"), + ("Ungroup", "Desagrupar"), + ("Lock", "Bloquear"), + ("Unlock All on Spread", "Desbloquear tudo na página dupla"), + ("Bring to Front", "Trazer para frente"), + ("Send to Back", "Enviar para trás"), + ("Bring Forward", "Avançar"), + ("Send Backward", "Recuar"), + ("Fitting", "Ajuste"), + ("Fit Frame to Content", "Ajustar quadro ao conteúdo"), + ("Fit Content to Frame", "Ajustar conteúdo ao quadro"), + ("Fill Frame Proportionally", "Preencher quadro proporcionalmente"), + ("Fit Content Proportionally", "Ajustar conteúdo proporcionalmente"), + ("Center Content", "Centralizar conteúdo"), + ("Frame Fitting Options", "Opções de ajuste de quadro"), + ("Generate QR Code", "Gerar código QR"), + ("Create Table", "Criar tabela"), + ("Insert Table…", "Inserir tabela…"), + ("Insert", "Inserir"), + ("Rows", "Linhas"), + ("Columns", "Colunas"), + ("Row", "Linha"), + ("Column", "Coluna"), + ("Merge Cells", "Mesclar células"), + ("Split Cell Horizontally", "Dividir célula horizontalmente"), + ("Split Cell Vertically", "Dividir célula verticalmente"), + ("Table Options", "Opções de tabela"), + ("Cell Options", "Opções de célula"), + ("Header Rows", "Linhas de cabeçalho"), + ("Footer Rows", "Linhas de rodapé"), + ("Zoom In", "Ampliar"), + ("Zoom Out", "Reduzir"), + ("Fit Page in Window", "Ajustar página na janela"), + ("Fit Spread in Window", "Ajustar página dupla na janela"), + ("Actual Size", "Tamanho real"), + ("Screen Mode", "Modo de tela"), + ("Normal", "Normal"), + ("Preview", "Visualização"), + ("Rulers", "Réguas"), + ("Grids & Guides", "Grades e guias"), + ("Show Guides", "Mostrar guias"), + ("Hide Guides", "Ocultar guias"), + ("Show Rulers", "Mostrar réguas"), + ("Hide Rulers", "Ocultar réguas"), + ("Extras", "Extras"), + ("Workspace", "Área de trabalho"), + ("New Workspace", "Nova área de trabalho"), + ("Properties", "Propriedades"), + ("Layers", "Camadas"), + ("Swatches", "Amostras"), + ("Colour", "Cor"), + ("Color", "Cor"), + ("Stroke", "Traçado"), + ("Fill", "Preenchimento"), + ("Gradient", "Gradiente"), + ("Transparency", "Transparência"), + ("Effects", "Efeitos"), + ("Links", "Links"), + ("Notes", "Notas"), + ("Conditions", "Condições"), + ("Library", "Biblioteca"), + ("Text Wrap", "Quebra de texto"), + ("Align", "Alinhar"), + ("Pathfinder", "Localizador de caminhos"), + ("Transform Again", "Transformar novamente"), + ("Interactive", "Interativo"), + ("Buttons and Forms", "Botões e formulários"), + ("Hyperlinks", "Hiperlinks"), + ("Bookmarks", "Indicadores"), + ("Show All Menu Items", "Mostrar todos os itens de menu"), + ("About DesignCraft", "Sobre o DesignCraft"), + ("Welcome to DesignCraft", "Bem-vindo ao DesignCraft"), + ("Page layout for print and screen — fast, open and scriptable.", "Layout de página para impressão e tela — rápido, aberto e scriptável."), + (" New file ", " Novo arquivo "), + (" Open ", " Abrir "), + (" Open sample magazine ", " Abrir revista de exemplo "), + ("Start a new document", "Iniciar um novo documento"), + ("No document open", "Nenhum documento aberto"), + ("Join the ArtCraft community", "Junte-se à comunidade ArtCraft"), + ( + "Get help, share your layouts and shape what we build next.", + "Obtenha ajuda, compartilhe seus layouts e ajude a moldar o que construímos em seguida.", + ), + ("Join our Discord", "Junte-se ao nosso Discord"), + ("Community", "Comunidade"), + ("Join the community", "Junte-se à comunidade"), + ("discord.gg/artcraft — help, feedback and show-and-tell", "discord.gg/artcraft — ajuda, feedback e demonstrações"), + (" OK ", " OK "), + ("Float panel", "Painel flutuante"), + ("Dock panel", "Painel encaixado"), + ("OK", "OK"), + ("Cancel", "Cancelar"), + ("Apply", "Aplicar"), + ("Done", "Concluir"), + ("Reset", "Redefinir"), + ("Add", "Adicionar"), + ("Remove", "Remover"), + ("Name", "Nome"), + ("Width", "Largura"), + ("Height", "Altura"), + ("Portrait", "Retrato"), + ("Landscape", "Paisagem"), + ("Orientation", "Orientação"), + ("Units", "Unidades"), + ("Preset", "Predefinição"), + ("Facing Pages", "Páginas frente a frente"), + ("Primary Text Frame", "Quadro de texto principal"), + ("Number of Pages", "Número de páginas"), + ("Start Page #", "Iniciar página nº"), + ("Page Size", "Tamanho da página"), + ("Margins", "Margens"), + ("Bleed", "Sangria"), + ("Slug", "Área de informações"), + ("Top", "Superior"), + ("Bottom", "Inferior"), + ("Left", "Esquerda"), + ("Right", "Direita"), + ("Inside", "Interna"), + ("Outside", "Externa"), + ("Gutter", "Medianiz"), + ("Binding", "Encadernação"), + ("Left to Right", "Da esquerda para a direita"), + ("Right to Left", "Da direita para a esquerda"), + ("No Selection", "Nenhuma seleção"), + ("Rectangle", "Retângulo"), + ("Ellipse", "Elipse"), + ("Polygon", "Polígono"), + ("Polygon Settings", "Configurações do polígono"), + ("Text", "Texto"), + ("Image", "Imagem"), + ("Line", "Linha"), + ("Position", "Posição"), + ("Appearance", "Aparência"), + ("Opacity", "Opacidade"), + ("Corner", "Canto"), + ("Rotation", "Rotação"), + ("Rulers & Grids", "Réguas e grades"), + ("Guides", "Guias"), + ("Flex Layout", "Layout flexível"), + ("Auto Size", "Tamanho automático"), + ("Tracking", "Espaçamento entre caracteres"), + ("Leading", "Entrelinha"), + ("Kerning", "Kerning"), + ("Language", "Idioma"), + ("Hyphenation", "Hifenização"), + ("Alignment", "Alinhamento"), + ("Indent", "Recuo"), + ("Space Before", "Espaço antes"), + ("Space After", "Espaço depois"), + ("New Layer", "Nova camada"), + ("New Swatch", "Nova amostra"), + ("Object Layer Options", "Opções de camada do objeto"), + ("Colour Picker", "Seletor de cores"), + ("Color Picker", "Seletor de cores"), + ("Color Settings", "Configurações de cores"), + ("User Dictionary", "Dicionário do usuário"), + ("Menu Customization", "Personalização de menus"), + ("Import Options", "Opções de importação"), + ("Place PDF", "Inserir PDF"), + ("Find", "Localizar"), + ("Change", "Alterar"), + ("Find What", "Localizar"), + ("Change To", "Substituir por"), + ("Find Next", "Localizar próximo"), + ("Change All", "Alterar tudo"), + ("Search", "Pesquisar"), + ("Document", "Documento"), + ("Selection", "Seleção"), + ("All Documents", "Todos os documentos"), + ("Direction", "Direção"), + ("Horizontal", "Horizontal"), + ("Vertical", "Vertical"), + ("On", "Ativado"), + ("Off", "Desativado"), + ("None", "Nenhum"), + ("[None]", "[Nenhum]"), + ("Place with Import Options…", "Inserir com opções de importação…"), + ("Print Booklet…", "Imprimir folheto…"), + ("Export", "Exportar"), + ("Export Page as PNG…", "Exportar página como PNG…"), + ("Export IDML…", "Exportar IDML…"), + ("Export EPUB…", "Exportar EPUB…"), + ("Export EPUB (Fixed Layout)…", "Exportar EPUB (layout fixo)…"), + ("Export PDF (Interactive)…", "Exportar PDF (interativo)…"), + ("Export HTML…", "Exportar HTML…"), + ("Export XML…", "Exportar XML…"), + ("Export Text…", "Exportar texto…"), + ("Quick Actions", "Ações rápidas"), + ("Import File", "Importar arquivo"), + ("Insert Page", "Inserir página"), + ("Edit Page", "Editar página"), + ("Adjust Layout", "Ajustar layout"), + ("Smart Guides", "Guias inteligentes"), + ("Lock Guides", "Bloquear guias"), + ("Show Document Grid", "Mostrar grade do documento"), + ("Show Baseline Grid", "Mostrar grade de linha de base"), + ("Options", "Opções"), + ("More Options", "Mais opções"), + ("Dialog", "Dialog"), + ("General", "Geral"), + ("Basic Character Formats", "Formatos básicos de caracteres"), + ("Indents and Spacing", "Recuos e espaçamento"), + ("Justification", "Justificação"), + ("Drop Caps and Nested Styles", "Capitulares e estilos aninhados"), + ("GREP Style", "Estilo GREP"), + ("Character Color", "Cor do caractere"), + ("Export Tagging", "Marcação de exportação"), + ("Numbering and Formatting", "Numeração e formatação"), + ("Spacing Options", "Opções de espaçamento"), + ("First Baseline", "Primeira linha de base"), + ("Rule Above", "Linha acima"), + ("Numbering", "Numeração"), + ("Footnote Reference Number in Text", "Número da referência da nota de rodapé no texto"), + ("Footnote Formatting", "Formatação de notas de rodapé"), + ("Span Footnotes Across Columns", "Estender notas de rodapé pelas colunas"), + ("Rule On", "Exibir linha"), + ("Ascent", "Ascendente"), + ("Cap Height", "Altura da caixa alta"), + ("x Height", "Altura do x"), + ("Fixed", "Fixo"), + ("Never (continuous)", "Nunca (contínuo)"), + ("Spread", "Página dupla"), + ("Section", "Seção"), + ("Footnote Reference", "Referência da nota de rodapé"), + ("Footnote Text", "Texto da nota de rodapé"), + ("Both", "Ambos"), + ("Apply Superscript", "Aplicar sobrescrito"), + ("Apply Subscript", "Aplicar subscrito"), + ("Apply Normal", "Aplicar normal"), + ("OpenType Superscript", "Sobrescrito OpenType"), + ("through", "até"), + ("up to", "até"), + ("line", "linha"), + ("lines", "linhas"), + ("sentences", "frases"), + ("words", "palavras"), + ("characters", "caracteres"), + ("letters", "letras"), + ("digits", "dígitos"), + ("tab", "tabulação"), + ("forcedLineBreak", "Quebra de linha forçada"), + ("emSpace", "Espaço em"), + ("enSpace", "Espaço en"), + ("chars", "caracteres"), + ("[Automatic]", "[Automático]"), + ("Size:", "Tamanho:"), + ("Tracking:", "Espaçamento entre caracteres:"), + ("Left Indent:", "Recuo esquerdo:"), + ("Right Indent:", "Recuo direito:"), + ("First Line Indent:", "Recuo da primeira linha:"), + ("Space Before:", "Espaço antes:"), + ("Space After:", "Espaço depois:"), + ("Words with at Least:", "Palavras com pelo menos:"), + ("After First:", "Após a primeira:"), + ("Before Last:", "Antes da última:"), + ("Hyphen Limit:", "Limite de hifens:"), + ("Word Spacing:", "Espaçamento de palavras:"), + ("Letter Spacing:", "Espaçamento de letras:"), + ("Glyph Scaling:", "Escala de glifos:"), + ("Insert Kashidas:", "Inserir cachidas:"), + ("In justified Arabic text", "Em texto árabe justificado"), + ("Minimum Space Before First Footnote:", "Espaço mínimo antes da primeira nota de rodapé:"), + ("Space Between Footnotes:", "Espaço entre notas de rodapé:"), + ("Offset:", "Deslocamento:"), + ("Min:", "Mín.:"), + ("Weight:", "Peso:"), + ("Color:", "Cor:"), + ("Width:", "Largura:"), + ("Style:", "Estilo:"), + ("Start at:", "Iniciar em:"), + ("Restart Numbering Every:", "Reiniciar numeração a cada:"), + ("Show Prefix/Suffix in:", "Mostrar prefixo/sufixo em:"), + ("Prefix:", "Prefixo:"), + ("Suffix:", "Sufixo:"), + ("Position:", "Posição:"), + ("Character Style:", "Estilo de caractere:"), + ("Paragraph Style:", "Estilo de parágrafo:"), + ("Separator:", "Separador:"), + ("EPUB and HTML", "EPUB e HTML"), + ("Tag:", "Marca:"), + ("Class:", "Classe:"), + ("Nested Styles", "Estilos aninhados"), + ("Nested Line Styles", "Estilos de linha aninhados"), + ("New Nested Style", "Novo estilo aninhado"), + ("New Line Style", "Novo estilo de linha"), + ("GREP Styles", "Estilos GREP"), + ("Apply Style:", "Aplicar estilo:"), + ("To Text:", "Ao texto:"), + ("New GREP Style", "Novo estilo GREP"), + ("Font Family:", "Família da fonte:"), + ("Font Style:", "Estilo da fonte:"), + ("Leading:", "Entrelinha:"), + ("Alignment:", "Alinhamento:"), + ("Hyphenate", "Hifenizar"), + ("Minimum", "Mínimo"), + ("Desired", "Desejado"), + ("Maximum", "Máximo"), + ("Composer:", "Compositor:"), + ("Paragraph Composer", "Compositor de parágrafos"), + ("Single-line Composer", "Compositor de linha única"), + ("Style Name:", "Nome do estilo:"), + ("Based On:", "Baseado em:"), + ("for", "para"), + ("Hyphenation Exceptions", "Exceções de hifenização"), + ("Binding:", "Encadernação:"), + ("Dynamic Spelling", "Verificação ortográfica dinâmica"), + ("UI Scaling", "Escala da interface"), + ("Ruby:", "Ruby:"), + ("Set over the selected text; empty removes it.", "Aplicada sobre o texto selecionado; vazio a remove."), + ("Document Footnote Options…", "Opções de notas de rodapé do documento…"), + ("Insert Footnote", "Inserir nota de rodapé"), + ("Go to Footnote Reference", "Ir para a referência da nota de rodapé"), + ("Footnotes", "Notas de rodapé"), + ("Paragraph Style Options…", "Opções de estilo de parágrafo…"), + ("New Paragraph Style…", "Novo estilo de parágrafo…"), + ("New Character Style…", "Novo estilo de caractere…"), + ("[Basic Paragraph]", "[Parágrafo básico]"), + ("[No Paragraph Style]", "[Sem estilo de parágrafo]"), + ("Align left", "Alinhar à esquerda"), + ("Align center", "Alinhar ao centro"), + ("Align right", "Alinhar à direita"), + ("Justify with last line aligned left", "Justificar com última linha alinhada à esquerda"), + ("Justify with last line aligned center", "Justificar com última linha centralizada"), + ("Justify with last line aligned right", "Justificar com última linha alinhada à direita"), + ("Justify all lines", "Justificar todas as linhas"), + ("Align towards spine", "Alinhar em direção à lombada"), + ("Align away from spine", "Alinhar no sentido oposto à lombada"), + ("Create New Style from Selection", "Criar novo estilo a partir da seleção"), + ("No text selected", "Nenhum texto selecionado"), + ("Essentials", "Essencial"), + ("Advanced", "Avançado"), + ("Book", "Livro"), + ("Digital Publishing", "Publicação digital"), + ("Interactive for PDF", "Interativo para PDF"), + ("Printing and Proofing", "Impressão e prova"), + ("Typography", "Tipografia"), + ("No errors", "Nenhum erro"), + ("error", "erro"), + ("errors", "erros"), + ("Display All Blacks as Rich Black", "Exibir todos os pretos como preto rico"), + ("Display All Blacks Accurately", "Exibir todos os pretos com precisão"), + ("Output All Blacks as Rich Black", "Gerar todos os pretos como preto rico"), + ("Output All Blacks Accurately", "Gerar todos os pretos com precisão"), + ("Top Margin", "Margem superior"), + ("Top of Page", "Topo da página"), + ("Paragraph Styles panel", "Painel de estilos de parágrafo"), + ("Character Styles panel", "Painel de estilos de caractere"), + ("Create new style", "Criar novo estilo"), + ("Clear overrides", "Limpar substituições"), + ("Accept", "Aceitar"), + ("Reject", "Rejeitar"), + ("Add Selection", "Adicionar seleção"), + ("Add the selection", "Adicionar a seleção"), + ("Add to Swatches", "Adicionar às amostras"), + ("Align Stroke:", "Alinhamento do traçado:"), + ("All Caps", "Maiúsculas"), + ("Angle", "Ângulo"), + ("Angle:", "Ângulo:"), + ("Baseline", "Linha de base"), + ("Cap:", "Ponta:"), + ("Cell Text", "Texto da célula"), + ("Cell Type", "Tipo de célula"), + ("Column Width", "Largura da coluna"), + ("Composer", "Compositor"), + ("Convert Selection to Multi-State Object", "Converter seleção em objeto de vários estados"), + ("Convert to Text", "Converter em texto"), + ("Create New Layer", "Criar nova camada"), + ("Default", "Padrão"), + ("Delete DTD", "Excluir DTD"), + ("Delete Layer", "Excluir camada"), + ("Delete Page", "Excluir página"), + ("Digits", "Dígitos"), + ("Distance", "Distância"), + ("Distribute ↔", "Distribuir ↔"), + ("Distribute ↕", "Distribuir ↕"), + ("Drop Shadow", "Sombra projetada"), + ("Effects: Drop Shadow", "Efeitos: sombra projetada"), + ("Favorites", "Favoritos"), + ("First Spread", "Primeira página dupla"), + ("Frame Fitting", "Ajuste de quadro"), + ("Gap Color:", "Cor do espaço:"), + ("Go To", "Ir para"), + ("Graphic…", "Gráfico…"), + ("Home", "Início"), + ("Hyperlink from the selected text or frames", "Hiperlink a partir do texto ou dos quadros selecionados"), + ("Include when exporting", "Incluir ao exportar"), + ("Join:", "Junção:"), + ("Last Spread", "Última página dupla"), + ("Linked Stories", "Artigos vinculados"), + ("Load DTD…", "Carregar DTD…"), + ("Location:", "Local:"), + ("Miter Limit:", "Limite de chanfro:"), + ("Move", "Mover"), + ("New Article from Selection", "Novo artigo a partir da seleção"), + ("New Note at Cursor", "Nova nota no cursor"), + ("New condition name", "Nome da nova condição"), + ("New group…", "Novo grupo…"), + ("New tag", "Nova marca"), + ("Next Spread", "Próxima página dupla"), + ("Note text", "Texto da nota"), + ("Offset", "Deslocamento"), + ("Offsets", "Deslocamentos"), + ("Open", "Abrir"), + ("Paragraph alignment", "Alinhamento de parágrafo"), + ("Pick a file in the folder holding the moved graphics", "Escolha um arquivo na pasta que contém os gráficos movidos"), + ("Place", "Inserir"), + ("Previous Spread", "Página dupla anterior"), + ("Release to Objects", "Liberar para objetos"), + ("Reverse the gradient", "Inverter o gradiente"), + ("Rotate 90° Counterclockwise", "Girar 90° no sentido anti-horário"), + ("Rotation Angle", "Ângulo de rotação"), + ("Row Height", "Altura da linha"), + ("Run", "Executar"), + ("Scale X Percentage", "Percentual de escala X"), + ("Scale Y Percentage", "Percentual de escala Y"), + ("Search commands", "Pesquisar comandos"), + ("Search fonts", "Pesquisar fontes"), + ("Search styles and commands…", "Pesquisar estilos e comandos…"), + ("Search: character or U+code", "Pesquisar: caractere ou código U+"), + ("Separated by commas", "Separados por vírgulas"), + ("Share", "Compartilhar"), + ("Shear X Angle", "Ângulo de inclinação X"), + ("Show / hide text with this condition", "Mostrar/ocultar texto com esta condição"), + ("Show Favorites Only", "Mostrar apenas favoritos"), + ("Show in the text", "Mostrar no texto"), + ("Spreads of up to 10 pages", "Páginas duplas de até 10 páginas"), + ("Stop Color:", "Cor de parada:"), + ("Strikethrough", "Tachado"), + ("Structure", "Estrutura"), + ("Style", "Estilo"), + ("Style source", "Origem do estilo"), + ("Styles panel", "Painel de estilos"), + ("Superscript", "Sobrescrito"), + ("Tag the selection", "Marcar a seleção"), + ("The structure is valid.", "A estrutura é válida."), + ("Toggle double column", "Alternar coluna dupla"), + ("Type:", "Tipo:"), + ("Underline", "Sublinhado"), + ("Untag Selection", "Remover marca da seleção"), + ("Update", "Atualizar"), + ("Validate", "Validar"), + ("Weight", "Peso"), + ("Subtract", "Subtrair"), + ("Intersect", "Intersecção"), + ("Exclude Overlap", "Excluir sobreposição"), + ("Minus Back", "Subtrair plano de trás"), + ("Make Compound", "Criar composto"), + ("Release", "Liberar"), + ("Paths", "Caminhos"), + ("Align left edges", "Alinhar bordas esquerdas"), + ("Align horizontal centers", "Alinhar centros horizontais"), + ("Align right edges", "Alinhar bordas direitas"), + ("Align top edges", "Alinhar bordas superiores"), + ("Align vertical centers", "Alinhar centros verticais"), + ("Align bottom edges", "Alinhar bordas inferiores"), + ("Align to Selection", "Alinhar à seleção"), + ("Align to Key Object", "Alinhar ao objeto-chave"), + ("Align to Margins", "Alinhar às margens"), + ("Align to Page", "Alinhar à página"), + ("Align to Spread", "Alinhar à página dupla"), + ("No Text Wrap", "Sem quebra de texto"), + ("Wrap Around Bounding Box", "Quebra em torno da caixa delimitadora"), + ("Wrap Around Object Shape", "Quebra em torno do formato do objeto"), + ("Jump Object", "Objeto de salto"), + ("Jump to Next Column", "Pular para a próxima coluna"), + ("Bulleted List", "Lista com marcadores"), + ("Numbered List", "Lista numerada"), + ("UPPERCASE", "MAIÚSCULAS"), + ("Small Caps", "Versalete"), + ("Butt", "Reta"), + ("Round", "Arredondada"), + ("Projecting", "Saliente"), + ("Miter", "Ângulo vivo"), + ("Bevel", "Chanfro"), + ("Center", "Centralizar"), + ("Start/End:", "Início/Fim:"), + ("First", "Primeiro"), + ("Last", "Último"), + ("Before", "Antes"), + ("After", "Depois"), + ("Shading", "Sombreamento"), + ("Border", "Borda"), + ("Overprint Fill", "Sobreimpressão do preenchimento"), + ("Overprint Stroke", "Sobreimpressão do traçado"), + ("Overprint Gap", "Sobreimpressão do espaço"), + ("Nonprinting", "Não imprimível"), + ("At Least", "No mínimo"), + ("Exactly", "Exatamente"), + ("Justify", "Justificar"), + ("Underline Options", "Opções de sublinhado"), + ("Strikethrough Options", "Opções de tachado"), + ("Discretionary Ligatures", "Ligaduras discricionárias"), + ("Fractions", "Frações"), + ("Ordinal", "Ordinal"), + ("Swash", "Floreios"), + ("Titling Alternates", "Alternativas de títulos"), + ("Contextual Alternates", "Alternativas contextuais"), + ("Slashed Zero", "Zero barrado"), + ("Tabular Lining", "Alinhados tabulares"), + ("Proportional Oldstyle", "Proporcionais estilo antigo"), + ("Proportional Lining", "Proporcionais alinhados"), + ("Tabular Oldstyle", "Tabulares estilo antigo"), + ("Default Figure Style", "Estilo de algarismo padrão"), + ("Not a button", "Não é um botão"), + ("Go To Next Page", "Ir para a próxima página"), + ("Go To Previous Page", "Ir para a página anterior"), + ("Go To First Page", "Ir para a primeira página"), + ("Go To Last Page", "Ir para a última página"), + ("Go To Page", "Ir para a página"), + ("Go To URL", "Ir para o URL"), + ("Not a form field", "Não é um campo de formulário"), + ("Text Field", "Campo de texto"), + ("Check Box", "Caixa de seleção"), + ("Radio Button", "Botão de opção"), + ("Combo Box", "Caixa de combinação"), + ("List Box", "Caixa de listagem"), + ("Signature", "Assinatura"), + ("Resize width", "Redimensionar largura"), + ("Resize height", "Redimensionar altura"), + ("Pin top", "Fixar superior"), + ("Pin bottom", "Fixar inferior"), + ("Pin left", "Fixar esquerda"), + ("Pin right", "Fixar direita"), + ("Inward", "Para dentro"), + ("Metrics", "Métricas"), + ("Optical", "Óptico"), + ("Applies to fill", "Aplica-se ao preenchimento"), + ("Applies to text", "Aplica-se ao texto"), + ("Gray", "Cinza"), + ("Linear", "Linear"), + ("Radial", "Radial"), + ("Arabic", "Árabe"), + ("Beveled Rectangle", "Retângulo chanfrado"), + ("Chinese", "Chinês"), + ("Constrain proportions for width and height", "Restringir proporções entre largura e altura"), + ("Custom", "Personalizado"), + ("Czech", "Tcheco"), + ("Danish", "Dinamarquês"), + ("Dashed", "Tracejado"), + ("Dotted", "Pontilhado"), + ("Dutch: 2005 Reform", "Holandês: reforma de 2005"), + ("Embedded", "Embutido"), + ("English: Canadian", "Inglês: canadense"), + ("English: UK", "Inglês: Reino Unido"), + ("English: USA", "Inglês: EUA"), + ("Farsi", "Persa"), + ("Finnish", "Finlandês"), + ("French", "Francês"), + ("French: Canadian", "Francês: canadense"), + ("German: 2006 Reform", "Alemão: reforma de 2006"), + ("German: Swiss 2006 Reform", "Alemão: reforma suíça de 2006"), + ("Greek", "Grego"), + ("Hebrew", "Hebraico"), + ("Hindi", "Hindi"), + ("Hungarian", "Húngaro"), + ("Inverse Rounded Rectangle", "Retângulo arredondado invertido"), + ("Italian", "Italiano"), + ("Japanese", "Japonês"), + ("Korean", "Coreano"), + ("Make all settings the same", "Tornar todas as configurações iguais"), + ("Native", "Nativo"), + ("Norwegian: Bokmål", "Norueguês: Bokmål"), + ("Orthogonal Line", "Linha ortogonal"), + ("Polish", "Polonês"), + ("Portuguese", "Português"), + ("Portuguese: Brazilian", "Português: Brasil"), + ("Rounded Rectangle", "Retângulo arredondado"), + ("Russian", "Russo"), + ("Solid", "Sólido"), + ("Spanish", "Espanhol"), + ("Swedish", "Sueco"), + ("Thick - Thick", "Espesso - Espesso"), + ("Thick - Thin", "Espesso - Fino"), + ("Thin - Thick", "Fino - Espesso"), + ("Thin - Thin", "Fino - Fino"), + ("Thin - Thick - Thin", "Fino - Espesso - Fino"), + ("Thick - Thin - Thick", "Espesso - Fino - Espesso"), + ("Wavy", "Ondulado"), + ("Straight Hash", "Traço reto"), + ("Stripes", "Listras"), + ("Triangle", "Triângulo"), + ("Turkish", "Turco"), + ("Ukrainian", "Ucraniano"), + ("Cell Style", "Estilo de célula"), + ("Table Style", "Estilo de tabela"), + ("Col ←", "Col ←"), + ("Col →", "Col →"), + ("Row ↑", "Linha ↑"), + ("Row ↓", "Linha ↓"), + ("Hide Others", "Ocultar outras"), + ("Lock Others", "Bloquear outras"), + ("Show All Layers", "Mostrar todas as camadas"), + ("Unlock", "Desbloquear"), + ("Unlock All Layers", "Desbloquear todas as camadas"), + ("Added", "Adicionado"), + ("Deleted", "Excluído"), + ("Guide-based", "Baseado em guias"), + ("Loop", "Repetir"), + ("Object-based", "Baseado em objetos"), + ("Play on Page Load", "Reproduzir ao carregar a página"), + ("Re-center", "Recentralizar"), + ("Show Controls", "Mostrar controles"), + ("Signature Field", "Campo de assinatura"), + ("Select text to apply a condition", "Selecione o texto para aplicar uma condição"), + ("Blinds", "Persianas"), + ("Box", "Caixa"), + ("Comb", "Pente"), + ("Cover", "Cobertura"), + ("Dissolve", "Dissolver"), + ("Fade", "Desvanecer"), + ("Push", "Empurrar"), + ("Split", "Dividir"), + ("Uncover", "Descobrir"), + ("Wipe", "Varrer"), + ("Simple", "Simples"), + ("Simple - Wide", "Simples - Largo"), + ("Triangle - Wide", "Triângulo - Largo"), + ("Barbed", "Farpeado"), + ("Curved", "Curvo"), + ("Circle", "Círculo"), + ("Circle - Solid", "Círculo - Sólido"), + ("Square", "Quadrado"), + ("Square - Solid", "Quadrado - Sólido"), + ("Bar", "Barra"), + ("Hide", "Ocultar"), + ("Show", "Mostrar"), + ("Select", "Selecionar"), + ("Fit Text", "Ajustar texto"), + ("Preview (W)", "Visualização (W)"), + ("Normal (W)", "Normal (W)"), + ("Conveyor", "Esteira"), + ("objects", "objetos"), + ("pages", "páginas"), + ("stories", "artigos"), + ("items", "itens"), + ("warnings", "avisos"), + ("Render", "Renderizar"), + ("Set", "Definir"), + ("unused", "não utilizado"), + ("⚠ Missing", "⚠ Ausente"), + ("▲ Modified", "▲ Modificado"), + ("(Text Color)", "(Cor do texto)"), + ("{count} glyphs — click to insert at the text cursor", "{count} glifos — clique para inserir no cursor do texto"), + ("{pages} Pages in {spreads} Spreads", "{pages} páginas em {spreads} páginas duplas"), + ("Text Frame", "Quadro de texto"), + ("Text Style", "Estilo de texto"), + (" (auto)", " (automático)"), + ("Absolute Numbering (instead of Section Numbering)", "Numeração absoluta (em vez de numeração de seção)"), + ( + "Add and remove pages as the primary text frame's story grows and shrinks", + "Adicionar e remover páginas conforme o artigo do quadro de texto principal cresce e encolhe", + ), + ("Added Text (highlighted)", "Texto adicionado (realçado)"), + ("Adjust Layout (objects follow the new page size)", "Ajustar layout (os objetos acompanham o novo tamanho de página)"), + ("Auto-Fit", "Ajuste automático"), + ("Case sensitive", "Diferenciar maiúsculas de minúsculas"), + ("Custom Tracking/Kerning", "Espaçamento/kerning personalizados"), + ("Dimensions Include Stroke Weight", "Dimensões incluem peso do traçado"), + ("Enable Autocorrect", "Ativar correção automática"), + ("Enable Dynamic Spelling (underline misspelled words)", "Ativar verificação ortográfica dinâmica (sublinhar palavras incorretas)"), + ("Grids in Back", "Grades atrás"), + ("H&J Violations", "Violações de hifenização e justificação"), + ("Include Bleed", "Incluir sangria"), + ("Include Stroke Weight", "Incluir peso do traçado"), + ("Keep Violations", "Violações de opções de manutenção"), + ("Overprint [Black] Swatch at 100%", "Sobreimpressão da amostra [Preto] a 100%"), + ("Printer's Marks", "Marcas da impressora"), + ("Remove Styles and Formatting from Text and Tables", "Remover estilos e formatação de textos e tabelas"), + ("Ruby", "Ruby"), + ("Show Note Anchors in Layout View", "Mostrar âncoras de notas na exibição de layout"), + ("Spreads", "Páginas duplas"), + ("Substituted Fonts", "Fontes substituídas"), + ("Transformations are Totals", "Transformações são totais"), + ("Use Black Point Compensation", "Usar compensação de ponto preto"), + ("Use Typographer's Quotes", "Usar aspas tipográficas"), + ("Whole word", "Palavra inteira"), + (" Close ", " Fechar "), + ("(Top Level)", "(Nível superior)"), + ("(import)", "(importação)"), + ("Accept All", "Aceitar tudo"), + ("Add Document…", "Adicionar documento…"), + ("Add to Previous Spread", "Adicionar à página dupla anterior"), + ("Align From:", "Alinhar de:"), + ("Align to baseline grid", "Alinhar à grade de linha de base"), + ("All Spots to Process", "Converter todas as cores especiais em cores de processo"), + ("Allow Spread to Shuffle", "Permitir reorganização de páginas duplas"), + ("Alternating Row Fills", "Preenchimentos de linha alternados"), + ("Apply Gradient", "Aplicar gradiente"), + ("Apply None (/)", "Aplicar nenhum (/)"), + ("Apply Parent", "Aplicar página mestra"), + ("Apply to All Spreads", "Aplicar a todas as páginas duplas"), + ("Apply to Stroke", "Aplicar ao traçado"), + ("Art", "Arte"), + ("Baseline Grid", "Grade de linha de base"), + ("Bleed and Slug", "Sangria e área de informações"), + ("Body Rows", "Linhas do corpo"), + ("Break Link to Style", "Romper vínculo com o estilo"), + ("Bullets and Numbering", "Marcadores e numeração"), + ("Business Card", "Cartão de visita"), + ("Buttons act in interactive PDF export.", "Os botões agem na exportação de PDF interativo."), + ("Change to:", "Alterar para:"), + ("Character Settings", "Configurações de caracteres"), + ("Characters", "Caracteres"), + ("Checked by default", "Marcado por padrão"), + ("Choices", "Opções"), + ("Choose Image…", "Escolher imagem…"), + ("Choose a link.", "Escolha um link."), + ("Content Fitting", "Ajuste de conteúdo"), + ("Convert Shape", "Converter forma"), + ("Copies:", "Cópias:"), + ("Count", "Contagem"), + ("Create", "Criar"), + ("Create Alternate Layout", "Criar layout alternativo"), + ("Create Table…", "Criar tabela…"), + ("Crop", "Corte"), + ("Crop Amount", "Quantidade de corte"), + ("Crop to:", "Cortar para:"), + ("Cross-Reference Format", "Formato de referência cruzada"), + ("Cursor Key:", "Tecla do cursor:"), + ("Default Fill and Stroke (D)", "Preenchimento e traçado padrão (D)"), + ("Default View:", "Exibição padrão:"), + ("Delete Column", "Excluir coluna"), + ("Delete Condition", "Excluir condição"), + ("Delete Row", "Excluir linha"), + ("Delete Swatch", "Excluir amostra"), + ("Delete Unused Layers", "Excluir camadas não utilizadas"), + ("Delete Workspace", "Excluir área de trabalho"), + ("Document Grid", "Grade do documento"), + ("Document Recovery Data", "Dados de recuperação do documento"), + ("Duplicate Spread", "Duplicar página dupla"), + ("Email", "E-mail"), + ("Embed", "Embutir"), + ("Fast", "Rápido"), + ("Fill (X)", "Preenchimento (X)"), + ("Fill with Placeholder Text", "Preencher com texto de espaço reservado"), + ("Find All", "Localizar tudo"), + ("Find what:", "Localizar:"), + ("Fit frame height to text", "Ajustar altura do quadro ao texto"), + ("Fitting:", "Ajuste:"), + ("Flip Horizontal", "Virar horizontalmente"), + ("Flip Vertical", "Virar verticalmente"), + ("Font Size:", "Tamanho da fonte:"), + ("Form Field", "Campo de formulário"), + ("Format:", "Formato:"), + ("Height:", "Altura:"), + ("Hide Bar", "Ocultar barra"), + ("High Quality", "Qualidade alta"), + ("Highlight", "Realçar"), + ("Horizontal Gridline Every:", "Linha de grade horizontal a cada:"), + ("Horizontal Margins:", "Margens horizontais:"), + ("Import XML…", "Importar XML…"), + ("Increment Every:", "Incremento a cada:"), + ("Ink Manager", "Gerenciador de tintas"), + ("Insert Column", "Inserir coluna"), + ("Insert Page After", "Inserir página depois"), + ("Insert Row", "Inserir linha"), + ("Inset spacing", "Espaçamento interno"), + ("Intent:", "Finalidade:"), + ("Invert", "Inverter"), + ("Keyboard Increments", "Incrementos do teclado"), + ("Kind:", "Tipo:"), + ("Link To:", "Vincular a:"), + ("Liquid guides", "Guias líquidas"), + ("Load Profile…", "Carregar perfil…"), + ("Load Swatches…", "Carregar amostras…"), + ("Load…", "Carregar…"), + ("Media", "Mídia"), + ("Merge into Active Layer", "Mesclar na camada ativa"), + ("Misspelled word → Correction (one per line):", "Palavra incorreta → Correção (uma por linha):"), + ("Mobile", "Dispositivo móvel"), + ("More ArtCraft apps", "Mais aplicativos ArtCraft"), + ("Move Selection Here", "Mover a seleção para cá"), + ("Move to Color Group", "Mover para grupo de cores"), + ("Move to Group", "Mover para grupo"), + ("Multiline", "Multilinha"), + ("Multiple Objects", "Vários objetos"), + ("Name:", "Nome:"), + ("New Bookmark for This Page", "Novo marcador para esta página"), + ("New Book…", "Novo livro…"), + ("New Color Group", "Novo grupo de cores"), + ("New Color Group with Swatch", "Novo grupo de cores com amostra"), + ("New Library…", "Nova biblioteca…"), + ("New Workspace…", "Nova área de trabalho…"), + ("No articles: exports follow page order.", "Nenhum artigo: as exportações seguem a ordem das páginas."), + ("No book open.", "Nenhum livro aberto."), + ("No bookmarks. They become the PDF outline.", "Nenhum marcador. Eles se tornam o contorno do PDF."), + ("No changes.", "Nenhuma alteração."), + ( + "No conditions. Create one, then select text and click it to apply.", + "Nenhuma condição. Crie uma, selecione o texto e clique nela para aplicá-la.", + ), + ("No document.", "Nenhum documento."), + ( + "No hyperlinks. Select text or a frame, type a URL and click New.", + "Nenhum hiperlink. Selecione texto ou um quadro, digite um URL e clique em Novo.", + ), + ("No library open.", "Nenhuma biblioteca aberta."), + ("No notes in this document.", "Nenhuma nota neste documento."), + ("No placed graphics.", "Nenhum gráfico inserido."), + ("No scripts. Add one below.", "Nenhum script. Adicione um abaixo."), + ("No spot inks", "Nenhuma tinta especial"), + ("No text anchors in this document.", "Nenhuma âncora de texto neste documento."), + ("Number of Pages:", "Número de páginas:"), + ("Number of Sides:", "Número de lados:"), + ("Number of columns", "Número de colunas"), + ("OK changes all: text and paragraph/character styles.", "OK altera tudo: texto e estilos de parágrafo/caractere."), + ("OVERSET", "EXCEDENTE"), + ("On Release", "Ao soltar"), + ("On Screen:", "Na tela:"), + ("Open source. No telemetry.", "Código aberto. Sem telemetria."), + ("Options for Black on RGB and Grayscale Devices", "Opções para preto em dispositivos RGB e em grayscale"), + ("Orientation:", "Orientação:"), + ("Page Numbering", "Numeração de página"), + ("Page · PPI", "Página · PPI"), + ("Pages:", "Páginas:"), + ("Part of the ArtCraft suite of open creative tools.", "Parte do conjunto ArtCraft de ferramentas criativas abertas."), + ("Pasteboard Options", "Opções da área de transferência"), + ("Path", "Caminho"), + ("Place the insertion point in a table, or create one.", "Coloque o ponto de inserção em uma tabela ou crie uma."), + ("Plain Text", "Texto simples"), + ("Plays in EPUB export; print and PDF show the poster.", "Reproduz na exportação EPUB; impressão e PDF mostram o pôster."), + ("Poster", "Pôster"), + ("Printer:", "Impressora:"), + ("Printing / Exporting:", "Impressão / Exportação:"), + ("Properties Panel", "Painel de propriedades"), + ("Recently Used", "Usados recentemente"), + ("Reject All", "Rejeitar tudo"), + ("Relative To:", "Em relação a:"), + ("Relink to Folder…", "Vincular novamente à pasta…"), + ("Relink…", "Vincular novamente…"), + ("Remove Conditions from Selection", "Remover condições da seleção"), + ("Repeat Header Rows", "Repetir linhas de cabeçalho"), + ("Replace With", "Substituir por"), + ("Required", "Obrigatório"), + ("Reset All to Defaults", "Redefinir tudo para padrões"), + ("Reverse", "Reverter"), + ("Right-click: stroke, groups", "Clique direito: traçado, grupos"), + ("Rotate 90° Clockwise", "Girar 90° no sentido horário"), + ("Ruler Units", "Unidades da régua"), + ("Save Swatches for Exchange…", "Salvar amostras para intercâmbio…"), + ("Save recovery data every:", "Salvar dados de recuperação a cada:"), + ("Saves which bars show and where the panels are.", "Salva quais barras são exibidas e onde ficam os painéis."), + ("Search:", "Pesquisar:"), + ("Select an object to color its fill or stroke.", "Selecione um objeto para colorir seu preenchimento ou traçado."), + ("Select an object to give its fill a gradient.", "Selecione um objeto para dar um gradiente ao seu preenchimento."), + ("Select an object to make it a button.", "Selecione um objeto para transformá-lo em botão."), + ("Select an object.", "Selecione um objeto."), + ("Select text or a text frame.", "Selecione texto ou um quadro de texto."), + ( + "Select two or more objects (or a group) to make a multi-state object.", + "Selecione dois ou mais objetos (ou um grupo) para criar um objeto de vários estados.", + ), + ("Show Tag Markers", "Mostrar marcadores de marcação"), + ("Single-line", "Linha única"), + ("Smart Text Reflow", "Redistribuição inteligente de texto"), + ("Speed", "Velocidade"), + ("Star Inset:", "Recuo da estrela:"), + ("Start Page #:", "Iniciar página nº:"), + ("Start:", "Início:"), + ("Status", "Status"), + ("Stroke (X)", "Traçado (X)"), + ("Style Mapping", "Mapeamento de estilos"), + ("Style Name Conflicts", "Conflitos de nomes de estilos"), + ("Stylistic Sets", "Conjuntos estilísticos"), + ("Subdivisions:", "Subdivisões:"), + ("Swap Fill and Stroke (Shift+X)", "Alternar preenchimento e traçado (Shift+X)"), + ("Synchronize", "Sincronizar"), + ("System default", "Padrão do sistema"), + ("Table Dimensions", "Dimensões da tabela"), + ("Text Display Options", "Opções de exibição de texto"), + ("Text Message", "Mensagem de texto"), + ("The fill isn't a gradient.", "O preenchimento não é um gradiente."), + ("The library is empty. Select objects and Add Selection.", "A biblioteca está vazia. Selecione objetos e clique em Adicionar seleção."), + ("This PDF has no layers.", "Este PDF não tem camadas."), + ("Track Changes in All Stories", "Controlar alterações em todos os artigos"), + ("Transitions play in interactive PDF (full-screen mode).", "As transições são reproduzidas em PDF interativo (modo tela cheia)."), + ("Trim", "Extremidades"), + ("Type Options", "Opções de tipo"), + ("Typical", "Típico"), + ("UI Size:", "Tamanho da interface:"), + ("Ungroup Color Group", "Desagrupar grupo de cores"), + ("Unmerge", "Desmesclar"), + ("Update Numbering", "Atualizar numeração"), + ("Use Global Light", "Usar luz global"), + ("User Dictionary (this document)", "Dicionário do usuário (este documento)"), + ("Variable Font", "Fonte variável"), + ("Vertical Gridline Every:", "Linha de grade vertical a cada:"), + ("Vertical Margins:", "Margens verticais:"), + ("Vertical justification", "Justificação vertical"), + ("View Threshold:", "Limiar da exibição:"), + ("Web", "Web"), + ("Web Hyperlink", "Hiperlink da web"), + ("When Scaling", "Ao dimensionar"), + ("Words, one per line:", "Palavras, uma por linha:"), + ("[Basic] (working) ▾", "[Básico] (em uso) ▾"), + ("[Black] Overprint", "Sobreimpressão do [Preto]"), + ("[No Group]", "[Sem grupo]"), + ("[None] — choose a colour on the ramp.", "[Nenhum] — escolha uma cor na rampa."), + ("⌥-click clears overrides", "⌥-clique limpa substituições"), + ("⚠ out of date", "⚠ desatualizado"), + ("Interface", "Interface"), + ("Advanced Type", "Tipo avançado"), + ("Composition", "Composição"), + ("Units & Increments", "Unidades e incrementos"), + ("Grids", "Grades"), + ("Guides & Pasteboard", "Guias e área de transferência"), + ("Dictionary", "Dicionário"), + ("Spelling", "Ortografia"), + ("Autocorrect", "Correção automática"), + ("Track Changes", "Controlar alterações"), + ("Story Editor Display", "Exibição do editor de artigo"), + ("Display Performance", "Desempenho de exibição"), + ("Appearance of Black", "Aparência do preto"), + ("File Handling", "Tratamento de arquivos"), + ("Formatting affects container (J)", "Formatação afeta o contêiner (J)"), + ("Formatting affects text (J)", "Formatação afeta o texto (J)"), + ("Apply Color", "Aplicar cor"), + ("Apply None", "Aplicar nenhum"), + ("Height Only", "Somente altura"), + ("Width Only", "Somente largura"), + ("Height and Width", "Altura e largura"), + ("Height only", "Somente altura"), + ("Width only", "Somente largura"), + ("Height and width", "Altura e largura"), + ("Version", "Versão"), + ("{count} characters don't fit", "{count} caracteres não cabem"), + ("document", "documento"), + ("story", "artigo"), + ("selection", "seleção"), + ("top", "superior"), + ("center", "centro"), + ("bottom", "inferior"), + ("justify", "justificar"), + ("text", "texto"), + ("graphic", "gráfico"), + ("unassigned", "não atribuído"), + ("Perceptual", "Perceptual"), + ("Relative Colorimetric", "Colorimétrico relativo"), + ("Saturation", "Saturação"), + ("Absolute Colorimetric", "Colorimétrico absoluto"), + ("No conflicts", "Nenhum conflito"), + ("Page (1–{count}):", "Página (1–{count}):"), + ("Fonts in Document: {count} Missing: {missing}", "Fontes no documento: {count} Ausentes: {missing}"), +]; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 04a7ab3..034577d 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -38,7 +38,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ "app.language", "Interface Language", None, - "{lang: \"\"|de|fr|es|ja|zh|ar} — menus and panel names (the macOS menu bar follows on the next launch)", + "{lang: \"\"|de|fr|es|ja|zh|ar|pt-br} — menus and panel names (the macOS menu bar follows on the next launch)", ), ( "app.flattener", @@ -233,6 +233,7 @@ pub const MENUS: &[(&str, &[&str])] = &[ "ui:app.language|日本語|{\"lang\": \"ja\"}", "ui:app.language|简体中文|{\"lang\": \"zh\"}", "ui:app.language|العربية|{\"lang\": \"ar\"}", + "ui:app.language|Português (Brasil)|{\"lang\": \"pt-br\"}", "<", ">Transparency Flattener Presets", "ui:app.flattener|None (keep transparency)|{\"preset\": \"\"}", @@ -2127,6 +2128,17 @@ mod tests { for (title, _) in menu_tree() { assert_ne!(crate::i18n::tr("ja", title), title, "{title}"); } + + run_ui(&mut app, "app.language", &json!({"lang": "pt-br"})).unwrap().unwrap(); + assert_eq!(app.ui.language, "pt-br"); + assert_eq!(checked(&app, "app.language", &json!({"lang": "pt-br"})), Some(true)); + for (title, _) in menu_tree() { + // "Layout" is the natural Brazilian Portuguese word, so it stays identical + // to English; every other title must be translated. + if title != "Layout" { + assert_ne!(crate::i18n::tr("pt-br", title), title, "{title}"); + } + } } #[test] @@ -2156,7 +2168,7 @@ mod tests { crate::dialogs::confirm(&mut app).unwrap(); frame(&mut app); assert_eq!(app.session.documents().len(), 1); - for lang in ["zh", "", "ar"] { + for lang in ["zh", "", "ar", "pt-br"] { app.run("app.language", json!({"lang": lang})).unwrap(); frame(&mut app); }